> ## 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.

# Errors and rate limits

> The error envelope, every REST and WebSocket error code, the rate limit budgets, and how to retry.

## The envelope

Every JSON response shares one shape:

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

```json theme={"dark"}
{"status": "error", "code": "validation_error", "data": {"validation_errors": {"bet_type": ["invalid_bet_type"]}}}
```

Always check `status` before reading `data`. Branch on `code`, a stable string, rather than on the HTTP status alone. The HTTP status conveys the category; `code` narrows it down.

## REST error codes

| HTTP | `code` | When |
| - | - | - |
| 400 | `validation_error` | The body or query failed validation. `data.validation_errors` is a map of field to reasons. Cross-field problems land in `non_field_errors`. A rejected list field is index-keyed |
| 400 | `order_closed` | Closing an order that exists but is already closed or settled. Distinct from `not_found` |
| 401 | `auth_error` | The server did not accept a key: it is missing, malformed, or not recognised. Check the header name and the key value |
| 403 | `forbidden` | Valid key, action not allowed for this account |
| 403 | `too_many_open_betslips` | `POST /v2/betslips/`: the account has reached its open-betslip cap. Close a betslip with `DELETE /v2/betslips/{betslip_id}/`, or wait for one to expire |
| 403 | `invalid_customer` | `GET /v2/events/` or `GET /v2/offers/`: the feed has no record of your account. A retry gets the same answer. Contact support |
| 404 | `not_found` | The betslip, order or heartbeat does not exist or is not visible to this key |
| 409 | `order_already_created` | A `request_uuid` was reused. Your order exists. Read `data.order_id` |
| 409 | `limit_reached` | A per-customer cap was hit. `data.detail` describes it |
| 429 | `throttled` | Rate limit hit. `data` carries `retry_after` in seconds and a `Retry-After` header is sent |
| 500 | `server_error` | Unexpected internal error. `data` carries a support token: quote it if you contact support |
| 503 | `warming` | `GET /v2/events/` or `GET /v2/offers/`: the feed is loading its market data or customer records. It clears by itself. Retry with backoff |
| 503 | none | The API could not reach its upstream. The body is `{"detail": "Service unavailable"}` with no envelope |

For `validation_error`, branch on the inner reason: the keys of `data.validation_errors`, or `non_field_errors` for cross-field rejections. Each endpoint in the [API reference](/developers/api-reference/introduction) documents the concrete codes it emits.

## WebSocket errors

Command errors arrive **in-band** and leave the socket open:

```json theme={"dark"}
["response", {"status": "error", "code": "invalid_input"}]
```

| `code` | Meaning |
| - | - |
| `bad_json` | The frame was not valid JSON, not a JSON array, or an empty array |
| `invalid_input` | The command name was not recognised, or its arguments had the wrong shape |
| `already_registered` | You already registered this event on this connection |
| `customer_event_limit_exceeded` | The registered-event cap, counted across all your connections, is full. Unregister first |
| `invalid_customer` | The stream does not recognise your account yet, for example just after a server restart. Retry after a short backoff. Contact support if it persists |
| `system_error` | Internal error. Retry with backoff |

Treat any other code as opaque: log it and retry with backoff.

**Handshake errors.** A refused connection returns a non-101 status with a short plain-text body:

| Status | Body | Do |
| - | - | - |
| 400 | `missing_credentials`, `invalid_lang` | Fix the request |
| 401 | `auth_rejected` | Do not retry with the same key. Check it over REST |
| 503 | `unavailable` | Retry with backoff |

Treat any other 4xx as 401 and any other 5xx as 503.

**Backpressure.** If you read too slowly, the server's outbound buffer overflows and it closes the connection with code `1008`. A client that has stopped reading never receives that frame and sees only a TCP close. Read faster or register fewer events: an immediate reconnect overflows again.

**Silent drops.** Two failures close an established connection with a raw TCP close and no close frame: an I/O error and an internal error. A quiet socket is not proof of a healthy one. Always reconnect with backoff and re-register your events. See [WebSocket stream](/developers/websocket-stream).

## Rate limits

Limits are per **account**. All of an account's API keys draw from one budget. Each limit is a token bucket that refills continuously, so capacity returns steadily rather than at a fixed reset, and an idle account can spend a full bucket in one burst.

| Applies to | Limit |
| - | - |
| All endpoints | 100 requests per second burst, 1200 requests per minute sustained |
| `POST /v2/betslips/` | 10 requests per second, and 2,000 betslips per rolling 24 hours |
| `POST /v2/orders/` | 5 requests per second |
| `GET /v2/offers/` | 4 requests in flight at once |

The two placement rows are dedicated budgets. A betslip or order creation counts only against its own limit, not the general one. For the betslip budget, every create counts, including a rejected one. A refresh does not count. The budget refills continuously, with no fixed reset. Every other endpoint, market data included, shares the general budget, so polling `GET /v2/events/` or `GET /v2/offers/` never throttles order placement but leaves less for other GETs. Budgets are keyed on the account, not the source address, so spreading an integration across several hosts does not add capacity. The WebSocket stream has no message-rate limit; its connection-level limits are the registered-event cap and the slow-reader disconnect.

The `GET /v2/offers/` row counts open requests, not a rate. One call can name up to 20 events at the same cost against the cap, so one call naming 20 events costs less than 20 calls naming one. A request over the cap gets `429 throttled` with a fixed `retry_after` of 1 second: retry when one of your open requests returns.

A rejected request gets `429 throttled` with the wait in `Retry-After` and in `data.retry_after`. Success responses carry no remaining-quota header, so track your own rate.

If your integration needs more headroom, email [support@magicmarkets.com](mailto:support@magicmarkets.com) and tell us the volume you expect.

## Common symptoms

| Symptom | Cause |
| - | - |
| `validation_error` with `bet_type` | The bet type is not valid for this event. Validate it, or take one off a live offer |
| Betslip has no `price_list` | Quotes arrive asynchronously. Wait for the `pmm` message or poll the betslip, watching `expiry_ts` |
| `order_closed` on close | The order is already closed or settled |
| `GET /v2/offers/` returns an empty `data` | The event has no prices now, `sport` does not match the event, or your account is not enabled for that sport |
| `order_already_created` | You reused a `request_uuid`. Your order exists. The response holds its `order_id` |
| Orders closed unexpectedly | A heartbeat expired. Every open order on the account closes when it does. See [Safety](/developers/safety) |

## Retry strategy

| Situation | Do |
| - | - |
| `429 throttled` | Sleep for `retry_after` seconds, then retry |
| `409 order_already_created` | Read `data.order_id`. Your order exists. Do not place again |
| `500 server_error` | Retry with exponential backoff. Keep the support token |
| `503` | Retry with backoff. The upstream is unavailable, or the market data feed is `warming` |
| Timeout on `POST /v2/orders/` | Retry with the **same** `request_uuid`, or look it up at `GET /v2/orders/tracked/{uuid}/` |
| Socket closed without a frame | Reconnect with backoff, wait for `sync`, re-register events |
| Socket closed with code `1008` | Read faster or register fewer events before you reconnect |
| `401 auth_error` or handshake `401 auth_rejected` | Do not retry. Check the header name and the key value |


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