> ## Documentation Index
> Fetch the complete documentation index at: https://docs.magicmarkets.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Concepts

> Betslips, orders, offers, pmm messages, heartbeats, events and the sync snapshot. Read this before you write code.

This page defines the objects the API is built on and the rules that connect them. The [glossary](/platform/glossary) maps the app's terms to the API's.

## Event

Something with markets on it. `GET /v2/events/` lists every event that currently has prices, with filters for sport, competition, event, start time and `ir_status`. The WebSocket carries the same objects: on connect the server sends every currently priced event as `["event", ...]` entries, then a `["sync", ...]` marker, and after that it pushes each change as it happens.

`ir_status` can stay `pre_event` after `start_time` passes. Check `start_time` before you treat an event as not started.

Two shapes, told apart by `event_type`:

* `normal`: a match, with `home` and `away`.
* `multirunner`: an outright or futures market, with a `teams` array and an `end_time`.

An event id looks like `2026-09-13,1001,2002`: an event date, the home team id, the away team id. A multirunner id looks like `2026-09-10,multirunner,100375008`. Treat the id as an opaque string. The date in it does not always match `start_time`.

## Offer

One priced selection on the public feed, keyed by `(sport, event_id, bet_type)`. It carries a `price_list` sorted by decimal price descending, with the stake available at each price. `for` and `against` on the same selection are two separate offers.

`GET /v2/offers/` returns a snapshot of the offers on up to 20 events per call. `event_id` is a repeated query parameter, one value for each event. The reply is paged. `limit` defaults to 500 (max 5000). Pass the last entry's `"{event_id},{bet_type}"` as `after` for the next page. A page shorter than `limit` is the last one. For live updates, `register_event` on the stream. On the stream, an empty `price_list` means no liquidity right now: pick another offer. The REST response leaves out markets with no available stake.

## Betslip

The first step to place an order on one selection. The app calls it an order ticket: see [Placing and managing orders](/traders/trading/placing-orders). You name a selection with `sport`, `event_id` and `bet_type`, and you get a `betslip_id`. The response carries no prices. The quote arrives separately as `pmm` messages on the WebSocket, usually within a couple of seconds.

Betslips are short-lived. A betslip closes about 45 seconds after creation unless you extend it with `POST /v2/betslips/{betslip_id}/refresh/`. Watch `expiry_ts`. An account can hold a limited number of open betslips; a create beyond the cap returns `403 too_many_open_betslips`. Close a betslip you no longer need with `DELETE /v2/betslips/{betslip_id}/`, which frees its slot. When a betslip closes, a `betslip_closed` message carries its `close_reason` and no further `pmm` arrives. An order already placed on it stays open until it fills, times out or you close it. Create a new betslip to re-quote.

`betslip_type` is `normal` unless you set it. To take the other side of an outcome, the usual way is a normal betslip with an `against` bet type: its price is odds on the outcome not happening, and higher is better, as with `for`. A `lay` betslip carries a lay price instead, where lower is better. Its prices are not on the same scale as a normal betslip's, so keep one convention in a strategy. See [Prices and ticks](/developers/prices-and-ticks#order-prices).

Nothing is at risk until you place an order against a betslip.

## pmm

Your private quote for an open betslip, delivered on the socket. It carries the `betslip_id` and a `price_list` for you specifically. Match it to your betslip by id.

## Order

A commitment: a `betslip_id` plus a `price` and a `stake`. Orders move `open`, then `pending`, then `done` or `failed`. Once the event settles, a filled order shows `reconciled` and its `profit_loss` is filled in. The spec also lists `partial_void` and `full_void`, so treat `status` as an open set.

<Note>
  `done` means **filled, not settled**. The order found a counterparty and you hold a position. `profit_loss` stays empty until the event finishes and the market settles.
</Note>

A closed order also carries a `close_reason`, such as `order_filled`, `timed_out`, `event_concluded` or `cancelled`. The app shows the same orders with its own status names. See [Order statuses](/traders/trading/order-statuses).

| App status | API |
| - | - |
| Filling | `open` or `pending` |
| Filled | `done`, with all of `want_stake` matched |
| Partial | `done`, with part of `want_stake` matched |
| Completed | `reconciled`, after the event settles |
| Not filled | `failed`, with `close_reason` `timed_out` or `event_concluded` |
| Cancelled | `failed`, with `close_reason` `cancelled` |
| Partial void | `partial_void` |
| Void | `full_void` |

Always send a `request_uuid` from automated code. Reusing one returns `409 order_already_created` with the existing `order_id`, so a retry after a timeout cannot double-place. You can look an order up by its `request_uuid` at `GET /v2/orders/tracked/{uuid}/` for several days after placement.

## Order options

`POST /v2/orders/` takes more than a price and a stake. The ones that change behaviour:

| Field | Default | What it does |
| - | - | - |
| `duration` | 15 seconds | How long the order stays open looking for a match, from 0.05 to 259,200 seconds (72 hours) |
| `exchange_mode` | `make_and_take` | `make_and_take` fills against available liquidity first and advertises the rest at your price. `take_only` never advertises the remainder. `dark` advertises it but hides your price from other customers until theirs crosses it. There is no post-only mode |
| `keep_open_ir` | false | Keep the order open when the event goes in-play |
| `accept_partial_fill` | true | Allow the order to fill in pieces |
| `accept_better_price` | true | Take a better price than you asked for when one is available |
| `current_score` | omitted | A `[home, away]` assertion. If the live score differs, the order is rejected with `validation_error` and `non_field_errors: ["event_scores_dont_match"]`, so you never place on a price that has not reacted to a goal. Football only: on other sports the server assumes `[0, 0]` and rejects any other value |
| `min_taker_want_stake` | omitted | `dark` orders only. The minimum stake another order must request to match yours, to stop small probes discovering your price |
| `user_data` | omitted | A free-text tag recorded against the order |
| `request_uuid` | omitted | The idempotency key. Always send one from automated code |

`live_score` on a betslip is for your reference only. Use `current_score` on the order for score protection.

The full field list is on the [Create order](/developers/api-reference/orders/create-order) reference page.

## Heartbeat

A dead-man's switch. Create one with a timeout between 10 and 300 seconds and keep refreshing it. If you stop, the server closes every open order on the account, across all sessions and API keys. Matched positions are unaffected. Any unattended strategy should run inside one. See [Safety](/developers/safety).

## The flow, end to end

```
GET /v2/balance/                        verify the key
connect /v2/stream                      collect events until sync
["register_event", sport, event_id]     receive offers
pick an offer                           pass its sport, event_id, bet_type through verbatim
POST /v2/betslips/                      -> betslip_id
["pmm", ...] arrives                    your quote
POST /v2/orders/                        -> order_id, with request_uuid
["order", ...] until done | failed      filled, not settled
```

The second and third lines also work over REST: `GET /v2/events/` in place of the sync, and `GET /v2/offers/` for a snapshot of an event's offers. A REST reply can be about 4 seconds behind live prices. The `pmm` quote and order updates still arrive on the socket, or you poll the betslip and the order.

## Combos

A combo joins 2 to 10 selections into one order. Create the betslip with `betslip_type: "parlay"` and a `legs` array of 2 to 10 selections, each `{sport, event_id, bet_type}`, in place of the top-level fields. On combo betslips and orders, `sport` and `bet_type` are both the literal `parlay`. A combo price is the product of its legs' prices, so it does not sit on the tick schedule.

## Sports and bet types

`sport` is a short lowercase code such as `fb` or `basket_q1`. `bet_type` is a comma-separated string that starts with `for` (back) or `against` (lay). Read both off an offer and pass them through unchanged. See [Sports and bet types](/developers/sports-and-bet-types).

## Money

Stakes and balances are `[currency, amount]` tuples, always USDT:

```json theme={"dark"}
["USDT", 115.38]
```

## Response envelope

Every JSON response uses one envelope. Check `status` before reading `data`, and branch on `code` rather than on the HTTP status alone.

```json theme={"dark"}
{"status": "ok", "data": ...}
{"status": "error", "code": "validation_error", "data": {...}}
```


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.