Skip to content

Risk controls

Orders rest on the book whether or not you are watching them. These controls decide what happens to yours when you are not the one deciding: when your connection drops, when the game starts, when a time you picked passes, and when play goes live. Most you set yourself; the in-play delay the exchange applies to you.

None of them are on by default. An order placed with no expiration and no cancel_on_disconnect rests on the book until you cancel it or the market settles, whether or not your process is still running.

expiration is an optional field on POST /api/v1/orders.

expiration The order expires
omitted Never. It rests until you cancel it or the market resolves. This is the default.
good_till_start When the event starts.
good_till_time At expiration_time.

expiration_time is Unix microseconds, not milliseconds or seconds, and is required when expiration is good_till_time. Microseconds is unusual enough that a value in milliseconds looks plausible and puts the expiry in 1970, which expires the order immediately.

good_till_start is the one to reach for if you place orders pre-game and do not want to trade in-play. It is keyed on the event, not the market, so every market on one game expires together.

If your process dies, your host sleeps or the network drops, the orders you left on the book keep trading without you. The cancel_on_disconnect flag has the exchange pull them when it stops hearing from your client, so a connection failure does not leave orders working that you can no longer manage.

Unlike the others, you arm it over the WebSocket rather than on the order alone, and it takes both halves to work.

Enabled when you join, not per request:

["1", "1", "orders:{user_id}", "phx_join",
{ "cancel_on_disconnect": true, "ping_timeout": 5000 }]
Parameter Notes
cancel_on_disconnect Boolean. Defaults to false.
ping_timeout Milliseconds. Must be an integer; a non-integer fails the join with {"ping_timeout": "Must be an integer"}. Clamped to 5000–20000; values outside the range are silently pulled to the nearest bound.

Once joined, send a ping on the channel before each ping_timeout elapses. The server replies {"ping": "pong", "ttl": <ping_timeout>} and resets the clock. Ping at roughly 60% of the window so a single slow round trip does not cost you the connection.

This timer is not the socket’s own keep-alive. Phoenix closes an idle socket after 60 seconds; ping_timeout is a separate, much shorter deadline that only governs cancellation.

POST /api/v1/orders
{
"market_id": "855faa81-7115-4f4f-b492-389fbd8c1ed8",
"order_type": "limit",
"action": "buy",
"price": "0.46",
"quantity": "25",
"cancel_on_disconnect": true
}

The body is flat. Wrapping the order in user_order, as some older examples do, returns 400 market_id is required, which reads like the field is missing when the real problem is the wrapper. See Place an order.

The server echoes back the ping_timeout it actually applied, which is not always the one you asked for:

{"cancel_on_disconnect": true, "ping_timeout": 15000}

Read the value from that reply rather than assuming your request was honored.

  1. The deadline passes and the server closes the orders channel: your client receives a phx_error event on that topic. The socket itself stays open.
  2. A 20-second grace period starts. Reconnect inside it and your orders are untouched, which is what makes a brief network blip safe.
  3. If the grace expires, every order whose own cancel_on_disconnect is true is cancelled. Orders without the flag stay on the book.

An order flagged after your last ping is still covered: it is cancelled when that ping_timeout expires. The same applies to orders placed once the channel is already considered dead, so placing new orders does not stop a cancellation that is already running.

The manual equivalent, for when you are shutting down deliberately rather than failing:

DELETE /api/v1/orders/all Everything resting on your account.
DELETE /api/v1/orders/batched A named list. The body takes orders, an array of objects each carrying an order_id, not a flat array of ids.

A cancel is a request, not a guarantee. An order can fill in the moment between you sending the cancel and the exchange processing it, so reconcile against fills rather than assuming a cancel left you flat.

This one you do not set. While an event is in_progress, incoming orders on its markets are held in a queue before reaching the book. It exists so a participant with a faster feed than the exchange cannot pick off prices that have not caught up with play yet.

An order sitting in that queue reads as delayed: accepted, holding liability, not yet working.

Whether it applies to a given order depends on:

  • The event must be in_progress. There is no delay pre-game.
  • The market sets the duration. Read in_play_delay_sec from GET /api/v1/markets; it is also pushed on the market_updates channel. The value is configured per sport, so it differs between a football market and a cricket one. Read the field rather than assuming a number.
  • The account must be subject to it. It can be switched off per account, in which case orders are never delayed regardless of the market’s value.

Because the duration is per market and the exemption is per account, in_play_delay_sec on a market is the ceiling, not a promise. If you need to know whether your own orders are being delayed, watch how long they sit at delayed rather than computing it.

v1.5.9Changelogllms.txtllms-full.txt