Skip to content
Back to skills

magicmarkets-magic-api

ASecurity

MagicMarkets API assistant: sports markets exchange with zero fees. Covers the REST API at magicmarkets.com/v2/ for market data (GET /v2/events/, GET /v2/offers/), betslips, placing and closing orders, positions, balance and heartbeats, and the WebSocket stream at magicmarkets.com/v2/stream for live offers, private betslip quotes and order updates. Use this skill when the user mentions MagicMarkets, the Magic API, magicmarkets.com, betslips, placing orders, back/lay/parlay, heartbeats, the pr...

  • 2 stars
  • 0 votes
  • 0 copies
  • 0 views
  • Added October 4, 2026
businesspythonrustgoapidocumentation

Works with

  • terminal
  • cli
  • api

Security analysis

A100/100

Pro scans all 20 files and shows the line behind each finding

Scanned October 4, 2026

npx -y skills add magicmarkets/magic-api --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of magicmarkets-magic-api?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for magicmarkets-magic-api
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/magicmarkets-magicmarkets-magic-api/badge)](https://www.skillsdirectory.com/skills/magicmarkets-magicmarkets-magic-api)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: magicmarkets-magic-api
description: >
  MagicMarkets API assistant: sports markets exchange with zero fees.
  Covers the REST API at magicmarkets.com/v2/ for market data
  (GET /v2/events/, GET /v2/offers/), betslips, placing and closing
  orders, positions, balance and heartbeats, and the WebSocket stream at
  magicmarkets.com/v2/stream for live offers, private betslip quotes and
  order updates. Use this skill when the user mentions MagicMarkets, the
  Magic API, magicmarkets.com, betslips, placing orders, back/lay/parlay,
  heartbeats, the price stream, register_event, USDT stakes, X-Api-Key
  auth, betslip_id, order_id, request_uuid, or building trading tools
  against MagicMarkets. Not for the read-only data feed at
  data.magicmarkets.com, which is a separate product.
---

# MagicMarkets API

MagicMarkets is a **sports markets exchange** with zero fees. Every order
matches peer liquidity or liquidity-provider liquidity in one order book.
MagicMarkets never takes the other side. All stakes are USDT.

| Detail | Value |
|--------|-------|
| **REST base** | `https://magicmarkets.com/v2/` |
| **WebSocket** | `wss://magicmarkets.com/v2/stream?api_key=<key>` |
| **REST auth** | `X-Api-Key: <key>` header |
| **WS auth** | `?api_key=<key>` query param (same key, URL-encoded) |
| **Stakes** | `["USDT", <amount>]` tuples, always USDT in responses |
| **Prices** | Decimal odds on a fixed tick schedule |

Keys are created on the website: **Settings, API, Add API Key**. The value
is shown once at creation. There is no endpoint for key management.

---

## The published docs are the source of truth

MagicMarkets publishes complete, current documentation. **Fetch it rather
than trusting memory.** This skill is a working guide, not a replacement:

| URL | What |
|-----|------|
| `https://magicmarkets.com/docs` | Documentation site: guides, account rules, API pages |
| `https://magicmarkets.com/llms.txt` | Index of the machine-readable docs |
| `https://magicmarkets.com/llms-full.txt` | Full API reference in one Markdown file, about 150 KB |
| `https://magicmarkets.com/v2/openapi.yaml` | OpenAPI 3.1 spec: 20 paths |
| `https://magicmarkets.com/v2/openapi.json` | The same spec as JSON |

Fetch `llms-full.txt` whenever you need exact request and response schemas,
the full sport-code table, per-endpoint error codes, or anything this file
summarises.

**Retired: do not use.** `pro.magicmarkets.com` (301s to the root domain),
the old `/magic-cpricefeed/v2` WebSocket (returns 502) and
`/web/offerhist/...` (404). Any code or doc that still names these is stale.

---

## Glossary

**Event**: a match (`normal`) or an outright (`multirunner`), keyed by
`(sport, event_id)`. Only events that currently have prices are listed.

**Offer**: one priced selection, keyed by `(sport, event_id, bet_type)`.
`for` and `against` on the same selection are two separate offers.

**Betslip**: a *quote request*. You name a selection (`sport`, `event_id`,
`bet_type`) and get a `betslip_id`. The prices arrive separately, as `pmm`
messages on the WebSocket, or by polling the betslip. Betslips are
short-lived: watch `expiry_ts`.

**Order**: a *commitment*. A `betslip_id` plus a limit `price` and a
`stake`. See [Order lifecycle](#order-lifecycle).

**pmm**: your *private* quote for an open betslip, delivered on the WS.

**Heartbeat**: a deadman's switch. If you stop refreshing it, the server
closes every open order on the account, across all sessions and API keys.
Matched bets are unaffected.

---

## Two ways to read the market

| | REST market data | WebSocket stream |
|---|---|---|
| Discover events | `GET /v2/events/` (paged, filterable) | Initial sync: `["event", ...]` until `["sync", ...]` |
| Read prices | `GET /v2/offers/` (up to 20 events a call) | `register_event`, then `offer` / `remove_offer` pushes |
| Freshness | Can be about 4 s behind | Live |
| Use for | Scripts, reports, checks, a first look | Trading, quoting, anything price-sensitive |

Both list the same events: only events that currently have prices. Private
quotes (`pmm`) and order updates come only on the stream. **Trade from the
stream.** A `bet_type` from either source goes into a betslip unchanged.

### The trading flow

```
1. GET /v2/balance/            verify the key over REST first
2. connect wss://.../v2/stream collect ["event", ...] until ["sync", ...]
3. ["register_event", sport, event_id]   -> ["offer", ...] snapshot
4. pick an offer: its sport/event_id/bet_type is all you need
5. POST /v2/betslips/          -> betslip_id
6. wait for ["pmm", ...] on the WS matching your betslip_id -> prices
7. POST /v2/orders/            -> order_id
8. watch ["order", ...] / ["bet", ...] on the WS until a terminal status
```

**Verify the key over REST before opening the socket.** A bad key fails the
WebSocket *handshake* with a non-101 status, which a client library shows as
a handshake error. `GET /v2/balance/` turns that into a clear
`401 auth_error`.

**Check `start_time` as well as `ir_status`.** An event can still show
`pre_event` after its start time. Do not treat such an event as pre-match.

---

## REST market data

### `GET /v2/events/`

Lists the events that currently have prices. Each entry is the same object
the stream sends as `["event", {...}]`.

- Filters: `sport`, `competition_id`, `event_id`, `ir_status`
  (`pre_event` / `in_running`), `start_time_from`, `start_time_to`, `lang`.
  List filters repeat the parameter: `?sport=fb&sport=tennis`.
- `event_id` and `competition_id` need `sport`, or you get a 400.
- Paging: `limit` (default 500) and `after`. Pass the last entry of the
  previous page as `after="{sport},{event_id}"`. A page shorter than
  `limit` is the last. There is no total count.

### `GET /v2/offers/`

The stake available at each price, for each market on the named events.
Each entry is the same object the stream sends as `["offer", {...}]`.

- `sport` and `event_id` are required. Name **up to 20 events** in one call.
- Filters: `bet_type`, `market_type` (families such as `ah`, `ou`, `wdw`),
  `min_liquidity` (USDT summed over a market's `price_list`).
- Paging: `limit` and `after="{event_id},{bet_type}"`.
- Markets with no stake are left out. An unknown or unpriced event returns
  no offers, and the other events still come back.
- `Cache-Control: private, max-age=2`. A reply can be about 4 seconds behind.
- **Only 4 calls can be open at once** for each account. One call that names
  20 events costs the same as one that names 1, so batch.

---

## Wire format: read this before writing any WS code

**Every** server frame is a batch envelope:

```json
{"ts": 1586042815.269000, "data": [ <message>, <message>, ... ]}
```

Each `data[]` entry is an array whose first element is a type tag:

```python
frame = json.loads(ws.recv())
for entry in frame["data"]:
    tag = entry[0]          # "event" | "offer" | "pmm" | "order" | "sync" | ...
    payload = entry[1]
```

Batching boundaries carry **no meaning**. Never rely on ordering, grouping,
or a type appearing exactly once per frame. Always iterate `data[]` and
dispatch on `entry[0]`.

### Discovery on the stream

On connect the server sends the currently priced events as `["event", {...}]`
entries, ending with `["sync", {"session_id": ...}]`. The snapshot may span
several envelopes. After sync, changed events re-arrive as `["event", ...]`
and events that lose their prices arrive as `["remove_event", ...]`.

Two event shapes: dispatch on `event_type`.
- `normal`: a match; carries `home` and `away`.
- `multirunner`: outright or futures; no home/away, carries a `teams` array
  (`[{"team_id", "name"}, ...]`) and an `end_time`.

### Commands

```json
["register_event", "<sport>", "<event_id>"]     -> offer snapshot, then ok response
["unregister_event", "<sport>", "<event_id>"]   -> ok (idempotent)
["list_registered_events"]                       -> {"registered_events": [[sport, id], ...]}
["echo", "anything"]                             -> echoed back verbatim
```

Registering an event with no prices is **not** an error: you get an empty
snapshot. There is no "unknown event" error. Register event ids you saw in
the sync or in `GET /v2/events/`.

### Message types you will see

`event`, `remove_event`, `sync`, `offer`, `remove_offer`, `pmm`, `betslip`,
`betslip_closed`, `order`, `bet`, `balance`, `xrate`, `info`, `response`,
`clear_events`, plus in-play state (`event_time`, `event_score`,
`event_red_cards`, `ir_info`, `remove_ir_info`,
`event_exchange_dark_liquidity`).

Two that matter for correctness:
- **`clear_events`**: the server lost its upstream feed. Discard all event,
  offer and live-state data you hold; a fresh snapshot follows.
- **`betslip_closed`**: `{betslip_id, close_reason}`. No further `pmm` will
  arrive; create a new betslip to re-quote.

### Offers

```json
["offer", {
  "sport": "fb", "event_id": "2026-06-15,1001,2002",
  "bet_type": "for,ah,h,-4", "market_type": "ah", "in_running": false,
  "price_list": [
    {"effective": {"price": 2.0, "min": ["USDT", 5.0], "max": ["USDT", 150.0]}},
    {"effective": {"price": 1.99, "min": null, "max": ["USDT", 80.0]}}
  ]
}]
```

`price_list` is sorted by decimal price **descending**, one entry per price.
`min` is `null` when there is no minimum; `max` is the stake available at that
price. An empty `price_list` means no liquidity: pick another offer.

---

## Bet types

`bet_type` is a comma-separated string. The first token is the direction:
`for` (back) or `against` (lay). The rest encodes the market and its
parameters. **Handicaps always refer to the home team.**

**Asian handicap lines are integers equal to 4 x the real line**:
`for,ah,h,-4` is home -1.0, `for,ah,h,2` is home +0.5, and
`for,ahover,7` is over 1.75. The integer is the home line, and the side
token picks the team.

**Never construct a `bet_type` by hand.** Read it off an offer and pass it
through verbatim. To check one, call
`GET /v2/sports/{sport}/bet_types/{bet_type}/`. That endpoint cannot check
outright bet types (`for,win,...`, `for,top,...`): they always return 400.

`sport` codes are lowercase and period-scoped (`fb`, `fb_ht`, `fb_htft`,
`fb_corn`, `basket_q1`, `tennis`, `af`, `horse`, `esports`, `politics`, ...).
One match can appear under several codes. Treat the list as **open**: new
codes are added; do not hard-fail on an unknown one. On parlays, `sport` is
the literal string `parlay` and per-leg sports sit inside `legs[]`.

Full sport table and market grammar: fetch `llms-full.txt`.

---

## Prices and ticks

All single-market prices sit on a fixed tick schedule that widens as the
price grows: 0.01 up to 2, then 0.02 to 3, 0.05 to 4, 0.10 to 6, 0.20 to 10,
0.50 to 20, 1 to 30, 2 to 50, 5 to 100 and 10 to 1000.

- Prices from the feed are **already on tick**: quote them straight through.
- An order `price` is a limit. A bet is taken only at that price or better:
  higher for `for`, lower for `against`.
- An off-tick limit moves to the **first tick that honours it**: **up** for
  `for`, **down** for `against`. A back limit of 7.15 becomes 7.20, and is
  not filled at 7.00. The moved price is the one the order runs with.
- Parlay prices are the product of the legs and are on no tick schedule.

---

## Order lifecycle

`POST /v2/orders/` returns **201** with `order_id` and `status: "open"`.

- Statuses: `open`, `pending`, `done`, `failed`, `partial_void`,
  `full_void`, `reconciled`. Treat all but `open` and `pending` as terminal.
- `done` means **filled, not settled**. The final `profit_loss` lands after
  the event.
- `failed` usually means a limit order that expired without a match. Read
  `close_reason` for the cause.
- `exchange_mode`: `make_and_take` (default), `take_only`, `dark`. Every mode
  takes crossing liquidity first. There is **no post-only mode**.
- `duration` is in seconds, default 15.

---

## Errors

REST errors share one envelope:

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

Always check `status` before reading `data`. Branch on `code`, not the HTTP
status alone.

| HTTP | `code` | Meaning |
|------|--------|---------|
| 400 | `validation_error` | `data.validation_errors` is `{field: [reason]}`; cross-field issues in `non_field_errors`; a list field is index-keyed |
| 400 | `order_closed` | Order exists but is already closed or settled |
| 401 | `auth_error` | Key missing, malformed or rejected |
| 403 | `forbidden` | Valid key, action not allowed |
| 403 | `invalid_customer` | Market data: the feed has no record of your account. Contact support |
| 404 | `not_found` | Unknown, or not visible to this key |
| 409 | `order_already_created` | `request_uuid` reused; `data` has the existing `order_id` |
| 409 | `limit_reached` | Per-customer cap hit |
| 429 | `throttled` | Honour `Retry-After` / `data.retry_after` |
| 500 | `server_error` | `data` carries a support token: quote it |
| 503 | `warming` | Market data is loading. Retry with backoff |
| 503 | *(no envelope)* | Body `{"detail": "Service unavailable"}`. Retry with backoff |

The 401 detail text is the same for a missing key and a wrong key. Check both.

**Stream handshake.** A refused upgrade returns a non-101 status with a short
text body: `400 missing_credentials` or `invalid_lang`, `401 auth_rejected`,
`503 unavailable`. Do not retry a 401 with the same key.

**Stream command errors** arrive in-band and leave the socket open:
`["response", {"status": "error", "code": "..."}]`. Codes: `bad_json`,
`invalid_input`, `already_registered`, `customer_event_limit_exceeded`,
`invalid_customer`, `system_error`. Treat any other code as opaque: log and
retry with backoff.

**Dropped connections.** A slow reader is closed with code **1008**: read
faster or register fewer events, because an immediate reconnect overflows
again. An I/O error or an internal error closes the connection with a raw
TCP close: no close frame and no error. Always reconnect with backoff and
re-register your events. Do not assume a quiet socket is a healthy one.

---

## Rate limits

Per **account**: all keys share one budget. Each limit is a token bucket
that refills continuously.

| Applies to | Limit |
|---|---|
| All endpoints | 100 req/s burst, 1200 req/min sustained |
| `POST /v2/betslips/` | 10 req/s, and 2000 betslips a day |
| `POST /v2/orders/` | 5 req/s |
| `GET /v2/offers/` | 4 requests open at once |

The betslip and order limits are their own budgets. Market data shares the
general budget. There is no per-IP limit. Success responses carry **no**
remaining-quota header, so track your own rate. The WS stream has no
message-rate limit.

---

## Idempotency

`POST /v2/orders/` accepts an optional `request_uuid`. Reusing one returns
`409 order_already_created` with the existing `order_id`, so a retry after a
timeout is safe and will not double-place. **Always send one** from
automated code. `GET /v2/orders/tracked/{uuid}/` retrieves an order by its
`request_uuid` for up to 6 hours after placement.

---

## Endpoint index

Market data: `GET /v2/events/` · `GET /v2/offers/`

Betslips: `GET|POST /v2/betslips/` · `GET /v2/betslips/{betslip_id}/` ·
`POST /v2/betslips/{betslip_id}/refresh/`

Orders: `GET|POST /v2/orders/` · `GET /v2/orders/{order_id}/` ·
`GET /v2/orders/updates/` · `GET /v2/orders/tracked/{uuid}/` ·
`POST /v2/orders/{order_id}/close/` · `POST /v2/orders/close_many/` ·
`POST /v2/orders/close_all/` · `GET /v2/orders/position/`

Account: `GET /v2/balance/` · `GET /v2/xrates/`

Heartbeats: `GET|POST /v2/heartbeats/` · `GET|DELETE /v2/heartbeats/{heartbeat_id}/` ·
`POST /v2/heartbeats/{heartbeat_id}/refresh/`

Reference: `GET /v2/sports/{sport}/bet_types/{bet_type}/`

Stream: `GET /v2/stream` (WebSocket upgrade)

`GET /v2/orders/` is paged with `page` and `page_size` (25 by default). Read
every page before you conclude that an order is not there.

Exact schemas, parameters and per-endpoint error codes: fetch
`https://magicmarkets.com/llms-full.txt`.

---

## Deeper reference

- [`references/rest.md`](references/rest.md): REST endpoints, shapes, errors, limits.
- [`references/streaming.md`](references/streaming.md): full WebSocket protocol.
- [`references/recipes.md`](references/recipes.md): task-shaped patterns.
- [`examples/`](examples/): runnable Python scripts. 03 and 04 are dry runs by default.

---

## Common mistakes

- **Using a retired host or endpoint.** See the list at the top.
- **Trading from `GET /v2/offers/`.** It can be 4 s behind. Quote and trade
  from the stream.
- **One `/v2/offers/` call per event.** Name up to 20 events in each call;
  only 4 calls can be open at once.
- **Taking `events[0]` as a match.** It is often an outright. Filter on
  `event_type == "normal"` and check `start_time`.
- **Reading one page of `GET /v2/orders/`.** It holds 25 orders by default.
- **Treating a frame as a message.** Frames are batch envelopes; iterate `data[]`.
- **Expecting prices in the `POST /v2/betslips/` response.** They arrive as
  `pmm` messages on the WebSocket.
- **Constructing `bet_type` strings, or reading a handicap integer as the
  line.** Read them off offers; the line is the integer divided by 4.
- **Rounding a back limit down to the tick below.** The server moves it up.
- **Treating `done` as settled.** It means filled; `profit_loss` finalises later.
- **Assuming a quiet socket is healthy.** Reconnect with backoff and
  re-register.
- **Omitting `request_uuid`** on automated order placement.

Files in this skill

  • CHANGELOG.md2.7 KB
  • CONTRIBUTING.md1.3 KB
  • Makefile639 B
  • SECURITY.md755 B
  • SKILL.md17.1 KB
  • examples/01-discover.py2.3 KB
  • examples/02-price-book.py2.5 KB
  • examples/03-market-making.py6.1 KB
  • examples/04-bulk-close.py2.1 KB
  • examples/05-rest-offers.py2.4 KB
  • examples/06-multi-stream.py3.2 KB
  • examples/README.md2.9 KB
  • examples/_common.py10.7 KB
  • examples/requirements.txt132 B
  • pyproject.toml739 B
  • references/recipes.md9.8 KB
  • references/rest.md14.1 KB
  • references/streaming.md11.9 KB
  • requirements-dev.txt213 B
  • scripts/check_copy.py3.5 KB

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…