Live odds and results for your sportsbook, as they move.
Cricket, soccer, tennis and more, from pre-match to the last ball: every market with its prices, pushed to you the moment it changes, and every result kept for you to collect.
Quickstart
From a new key to live prices in a few minutes.
- Send us your server IPs. Requests are accepted only from addresses on your allowlist. We issue your sandbox key once they are added.
- Check your key.
GET /v1/mereturns your name and the sports on your plan. - List what is in play with
/v1/events?state=inplay, then fetch one event in full. - Open a stream and keep your copy current from what it sends.
# Your key goes in a header, never in the URL. export DIAMONDEXCHAPI_KEY="fg_test_…" export BASE="https://api-sandbox.diamondexchapi.live" curl -s -H "X-API-Key: $DIAMONDEXCHAPI_KEY" "$BASE/v1/me" curl -s --compressed -H "X-API-Key: $DIAMONDEXCHAPI_KEY" "$BASE/v1/events?sport=cricket&state=inplay" # A live stream: one connection, kept open. Sandbox keys list up to 5 event ids; # a live key may use events=all. curl -N -H "X-API-Key: $DIAMONDEXCHAPI_KEY" "$BASE/v1/stream?events=ev_k9wtucdybikxygkh,ev_zizcp33rpuucwacg"
// Node 18+ or any modern runtime. The key stays on your server. const BASE = 'https://api-sandbox.diamondexchapi.live' const headers = { 'X-API-Key': process.env.DIAMONDEXCHAPI_KEY } const me = await (await fetch(BASE + '/v1/me', { headers })).json() console.log(me.client, me.sports) const { events } = await (await fetch(BASE + '/v1/events?state=inplay', { headers })).json() const event = await (await fetch(BASE + '/v1/events/' + events[0].id, { headers })).json() console.log(event.name, event.markets.length, 'markets')
import os, requests BASE = "https://api-sandbox.diamondexchapi.live" s = requests.Session() s.headers["X-API-Key"] = os.environ["DIAMONDEXCHAPI_KEY"] print(s.get(BASE + "/v1/me").json()) events = s.get(BASE + "/v1/events", params={"state": "inplay"}).json()["events"] event = s.get(BASE + "/v1/events/" + events[0]["id"]).json() print(event["name"], len(event["markets"]), "markets")
Keys and IP allowlist
Every request needs both a valid key and a source address on your allowlist. A key on its own, from anywhere else, is refused.
| Header | Value |
|---|---|
X-API-Key | Your key, for example fg_live_k7q2…. Keys are never accepted in the URL, where they would end up in logs. |
Accept-Encoding | gzip is supported and recommended: event snapshots shrink about tenfold. |
- A key is shown once, when it is issued. We keep only a hash of it and cannot show it again; if it is lost, we issue a new one.
- Keys can overlap: ask for a second key, move your servers to it, then ask us to revoke the old one.
- Keep keys on your servers. The API is built for server-to-server use, not for browsers or apps.
Sandbox and live
| Environment | Keys | Base URL |
|---|---|---|
| Sandbox | fg_test_… | https://api-sandbox.diamondexchapi.live |
| Live | fg_live_… | https://api.diamondexchapi.live |
The sandbox carries real, current markets, so you can build against real behaviour: prices that move, markets that suspend, results that arrive. A key works only in its own environment, and the sandbox has no uptime commitment.
The sandbox is for building, not for running a site, so every sandbox key has these limits, whatever its plan says:
| Sandbox limit | Value |
|---|---|
| Odds delay | Prices are 60 seconds old. Results are not delayed. |
| Open streams | 1 at a time |
| Events per stream | 5, listed by id (events=ev_a,ev_b). events=all needs a live key. |
| Requests | 2 per second |
GET /v1/me shows the limits your key has.
Sports, events, markets
Four kinds of thing, each with an id that never changes. Store them as you receive them.
| Id | What | Example |
|---|---|---|
sport | A sport, by name: cricket, soccer, tennis, basketball, boxing. You receive the sports on your plan. | cricket |
cp_… | A competition | One Day Internationals |
ev_… | An event: a match or a fixture | Pakistan v Sri Lanka |
mk_… | A market on that event | Match Odds, Bookmaker, 10 over run PAK |
rn_… | A runner (selection) in that market | Pakistan, The Draw, Yes |
Event
| Field | Meaning |
|---|---|
state | upcoming before the start, inplay once it is live |
start | Scheduled start, ISO 8601 UTC |
updatedAt | When any price in it last changed, in epoch milliseconds |
markets | Every open market. A market leaves the event when it is settled or closed. |
Market
| Field | Meaning |
|---|---|
type | match_odds, bookmaker or fancy |
format | How to read its prices: decimal, rate, line or range. See Reading prices. |
status | Whether it can be traded now. See Market status. |
runners[] | Each with back and lay ladders, best price first: {"price", "size"} |
stale | Present and true when we have stopped receiving prices for it. The market is shown as SUSPENDED until prices flow again. |
Reading prices
Markets quote prices in four ways. Always read format before using price and size.
decimalMatch odds, Yes/No, Odd/EvenDecimal odds and the amount available. A stake of 100 at 2.88 returns 288.
rateBookmakerProfit per 100 staked. A rate of 13 is decimal 1.13; a rate of 625 is 7.25. Decimal = 1 + rate / 100.
lineFancy: runs, wickets, totalsprice is the line and size the rate. Back is Yes: wins if the result reaches the line. Lay is No: wins if it stays below.
rangeKhadoA line with a width: Yes wins if the result is one of the width scores from price upward. size is the rate. Back only.
Market status
| Status | Meaning |
|---|---|
OPEN | Prices are live. |
SUSPENDED | Temporarily not tradeable: a goal, a wicket, a review, or our own feed check ("stale": true). Prices may still be shown; do not take bets on them. |
BALL_RUNNING | Cricket: the ball is in play. Not tradeable until it is dead. |
CLOSED | Finished. A closed market leaves the event shortly after; its result follows on the results feed. |
Your account
{
"client": "Acme Sportsbook",
"sports": ["cricket", "soccer", "tennis"],
"limits": { "maxStreams": 5, "minPollMs": 1000, "maxEventsPerStream": 0, "delayMs": 0 },
"serverTime": "2026-11-09T21:08:31Z"
}limits are what your key may do: open streams, events per stream and how old prices are (delayMs). 0 means no limit or no delay.
Sports
{
"sports": [
{ "sport": "cricket", "events": 17, "inplay": 3, "upcoming": 14 },
{ "sport": "soccer", "events": 99, "inplay": 17, "upcoming": 82 },
{ "sport": "tennis", "events": 143, "inplay": 11, "upcoming": 132 }
]
}Competitions
| Parameter | Meaning |
|---|---|
sport | Optional. One of your sports. |
{
"competitions": [
{ "id": "cp_uumfx68hmdwne54u", "name": "One Day Internationals", "sport": "cricket", "events": 2 },
{ "id": "cp_snvzngyxyb44tc94", "name": "South Africa T20 Challenge", "sport": "cricket", "events": 3 }
]
}Events
| Parameter | Meaning |
|---|---|
sport | Optional. cricket, soccer, … |
state | Optional. inplay or upcoming |
competition | Optional. A cp_… id |
{
"events": [
{
"id": "ev_k9wtucdybikxygkh",
"name": "Pakistan v Sri Lanka",
"sport": "cricket",
"competition": { "id": "cp_uumfx68hmdwne54u", "name": "One Day Internationals" },
"start": "2026-11-09T04:30:00Z",
"state": "inplay",
"inplay": true,
"updatedAt": 1794258468524,
"marketCount": 41
}
]
}One event, in full
Responses carry an ETag: send it back as If-None-Match and you get 304 Not Modified while nothing has changed. X-Resume-Token lets you continue from this snapshot with /v1/changes.
{
"id": "ev_k9wtucdybikxygkh",
"name": "Pakistan v Sri Lanka",
"sport": "cricket",
"competition": { "id": "cp_uumfx68hmdwne54u", "name": "One Day Internationals" },
"start": "2026-11-09T04:30:00Z",
"state": "inplay",
"inplay": true,
"updatedAt": 1794258468524,
"markets": [
{
"id": "mk_vf86825bva46g3qq", "name": "Match Odds",
"type": "match_odds", "format": "decimal", "status": "OPEN", "inplay": true,
"updatedAt": 1794258468524,
"runners": [
{ "id": "rn_svsqaaiuqdfnetsf", "name": "Pakistan", "status": "OPEN",
"back": [{ "price": 1.49, "size": 1525.4 }, { "price": 1.48, "size": 980 }],
"lay": [{ "price": 1.52, "size": 640.1 }] },
{ "id": "rn_jz7r79c78pm4y3dp", "name": "Sri Lanka", "status": "OPEN",
"back": [{ "price": 2.88, "size": 257.53 }],
"lay": [{ "price": 3, "size": 25 }] }
]
},
{
"id": "mk_3hb7tq2wc9xzv5ne", "name": "Bookmaker",
"type": "bookmaker", "format": "rate", "status": "OPEN",
"runners": [
{ "id": "rn_q3m8x2wte6kd7bpa", "name": "Pakistan", "status": "OPEN",
"back": [{ "price": 48, "size": 100000 }], "lay": [{ "price": 52, "size": 100000 }] }
]
},
{
"id": "mk_juc56fmwfmqx7gag", "name": "10 over run PAK",
"type": "fancy", "format": "line", "status": "BALL_RUNNING",
"runners": [
{ "id": "rn_p8vd2hsn4c6xqe9w", "name": "10 over run PAK", "status": "OPEN",
"back": [{ "price": 61, "size": 90 }], "lay": [{ "price": 61, "size": 110 }] }
]
}
]
}Odds for several events
The same objects as /v1/events/{id}, in the order you asked. Ids you do not receive, or that have ended, are listed in missing. next is a token for /v1/changes.
{
"events": [ { "id": "ev_k9wtucdybikxygkh", "name": "Pakistan v Sri Lanka", "markets": [ … ] } ],
"missing": ["ev_zizcp33rpuucwacg"],
"next": "9f2c41d0.18342"
}Live stream
The fastest and cheapest way to stay current. You receive each event in full once, then only what changes: usually within a fraction of a second of the price moving.
| Parameter | Meaning |
|---|---|
events | all (default): every event you receive, including ones that start later. Or a list of up to 500 ev_… ids. |
Messages
snapshotAn event in full, as from /v1/events/{id}. Sent for each event when you connect, and when a new event joins.
readyThe opening snapshots are done. From here on you receive changes.
market-update{"event", "market"}: one market, whole, as it is now. Replace your copy of that market.
market-remove{"event", "market"}: the market closed or was settled. Its result follows as a result.
event-updateThe event's name, start or state changed (it went in play). Same fields as in /v1/events.
event-remove{"id"}: the event has ended or is no longer offered. Drop it.
resultA market's result. See Results.
reconnectWe are restarting this server: reconnect now, with your last id.
error{"code", "message"}, then the stream closes: for example your key was revoked or your stream allowance is used up.
Resuming after a disconnect
Every message has an id. Reconnect with the last one in the Last-Event-ID header and you receive exactly what you missed, results included. If it is too old to resume, you receive fresh snapshots instead, so your copy is always whole. Standard EventSource clients do this for you.
A comment line (: heartbeat) arrives every 15 seconds so you can tell a quiet market from a dead connection. If you hear nothing for 45 seconds, reconnect. The stream also sets retry: to spread reconnects out after a restart.
If your server falls behind, you are not sent every tick you missed: you get the latest state of each market. You never have to process a backlog of stale prices.
// npm install eventsource import { EventSource } from 'eventsource' const book = new Map() // event id -> event, kept current const es = new EventSource('https://api-sandbox.diamondexchapi.live/v1/stream?events=all', { fetch: (url, init) => fetch(url, { ...init, headers: { ...init.headers, 'X-API-Key': process.env.DIAMONDEXCHAPI_KEY } }), }) es.addEventListener('snapshot', (m) => { const ev = JSON.parse(m.data); book.set(ev.id, ev) }) es.addEventListener('market-update', (m) => { const { event, market } = JSON.parse(m.data) const ev = book.get(event); if (!ev) return const i = ev.markets.findIndex((x) => x.id === market.id) i >= 0 ? (ev.markets[i] = market) : ev.markets.push(market) }) es.addEventListener('market-remove', (m) => { const { event, market } = JSON.parse(m.data) const ev = book.get(event); if (ev) ev.markets = ev.markets.filter((x) => x.id !== market) }) es.addEventListener('event-remove', (m) => book.delete(JSON.parse(m.data).id)) es.addEventListener('result', (m) => settle(JSON.parse(m.data))) // your settlement // EventSource reconnects by itself and sends Last-Event-ID.
# pip install requests sseclient-py import json, os, requests, sseclient url = "https://api-sandbox.diamondexchapi.live/v1/stream?events=all" headers = {"X-API-Key": os.environ["DIAMONDEXCHAPI_KEY"], "Accept": "text/event-stream"} last_id = None book = {} while True: if last_id: headers["Last-Event-ID"] = last_id resp = requests.get(url, headers=headers, stream=True, timeout=(10, 45)) try: for m in sseclient.SSEClient(resp).events(): last_id = m.id or last_id data = json.loads(m.data) if m.event == "snapshot": book[data["id"]] = data elif m.event == "market-update": upsert(book, data["event"], data["market"]) elif m.event == "event-remove": book.pop(data["id"], None) elif m.event == "result": settle(data) except requests.RequestException: pass # reconnect with Last-Event-ID
event: snapshot id: 9f2c41d0.18342.17493 data: {"id":"ev_k9wtucdybikxygkh","name":"Pakistan v Sri Lanka","sport":"cricket", … ,"markets":[…]} event: ready id: 9f2c41d0.18342.17493 data: {"events":1} event: market-update id: 9f2c41d0.18347.17493 data: {"event":"ev_k9wtucdybikxygkh","market":{"id":"mk_vf86825bva46g3qq","name":"Match Odds","type":"match_odds","format":"decimal","status":"OPEN", … }} : heartbeat event: market-remove id: 9f2c41d0.18390.17493 data: {"event":"ev_k9wtucdybikxygkh","market":"mk_juc56fmwfmqx7gag"} event: result id: 9f2c41d0.18390.17494 data: {"seq":17494,"action":"SETTLED","sport":"cricket","market":{"id":"mk_juc56fmwfmqx7gag","name":"10 over run PAK","type":"fancy"},"result":64, … }
Changes, for pollers
If you cannot hold a stream open, take a snapshot with /v1/odds, keep its next token, and call /v1/changes?since= with it every second or two. You receive the latest state of each market that changed and a new next. A token too old to continue answers 410 resync: take a fresh snapshot and carry on from its token.
{
"changes": [
{ "type": "market-update", "data": { "event": "ev_k9wtucdybikxygkh", "market": { "id": "mk_vf86825bva46g3qq", … } } },
{ "type": "event-remove", "data": { "id": "ev_qfsu3ku7736ttn69" } }
],
"next": "9f2c41d0.18390"
}How results work
Every result is recorded in order, kept for at least 48 hours, and reaches you three ways: pushed on your stream, listed by /v1/results, and looked up by market or event.
| Action | Meaning | What to do |
|---|---|---|
SETTLED | The market's first result | Settle bets |
CORRECTED | The result changed: the earlier one was wrong | Re-settle to the new result |
VOIDED | The market was cancelled, or corrected to void | Refund stakes |
UNSETTLED | The result was withdrawn; a new one will follow | Hold settlement |
A market can have several entries over time. The one with the highest seq is current. Each entry carries a winner (winner), a number (result, for line and range markets) or neither (void).
{
"seq": 17327,
"action": "SETTLED",
"sport": "cricket",
"event": { "id": "ev_k9wtucdybikxygkh", "name": "Pakistan v Sri Lanka", "start": "2026-11-09T04:30:00Z" },
"market": { "id": "mk_ywxwfni42227wbyh", "name": "To Win The Toss", "type": "bookmaker" },
"winner": { "id": "rn_jz7r79c78pm4y3dp", "name": "Sri Lanka" },
"settledAt": "2026-11-09T04:23:43Z",
"at": "2026-11-09T04:23:45Z"
}Results are kept for 7 days and guaranteed for at least 48 hours after settlement. Collect them well within that window.
Results since your last call
| Parameter | Meaning |
|---|---|
since | The next from your previous call; 0 for everything kept |
limit | Up to 1000 per page; default 500 |
{
"results": [ { "seq": 17326, … }, { "seq": 17327, … } ],
"next": 17327,
"more": false
}Keep next and pass it as since next time: you never miss or repeat an entry. While more is true, call again at once. If since is older than what we keep, you get 410 resync: start again from since=0.
One market's result
{
"market": { "id": "mk_r4nd8w2qk6t3pyvh", "name": "29.3 over run NZ", "type": "fancy" },
"event": { "id": "ev_zizcp33rpuucwacg", "name": "New Zealand v India" },
"status": "SETTLED",
"result": 91,
"settledAt": "2026-11-07T05:27:16Z",
"seq": 17001,
"corrected": false
}status is SETTLED, VOIDED or PENDING (no result yet, or withdrawn). corrected is true once a result has been changed. A market that is not yours, or whose result is older than we keep, answers 404.
All results of an event
{
"event": "ev_k9wtucdybikxygkh",
"markets": [
{ "market": { "id": "mk_ywxwfni42227wbyh", "name": "To Win The Toss", "type": "bookmaker" },
"status": "SETTLED", "winner": { "id": "rn_jz7r79c78pm4y3dp", "name": "Sri Lanka" }, … },
{ "market": { "id": "mk_vf86825bva46g3qq", "name": "Match Odds", "type": "match_odds" },
"status": "PENDING", "corrected": false }
]
}Limits and allowances
Your plan sets how much you can use. /v1/me shows your limits.
| Limit | When you reach it |
|---|---|
| Requests per second | 429 rate_limited with Retry-After. A stream counts as one request however long it stays open. |
| Open streams | 429 too_many_streams for a new stream. Close one first. |
| Events per stream | 400 when you name more than your plan allows. |
| Daily requests, monthly stream hours and data | Depends on your plan: refused with 429 quota_exceeded, served with a delay, or billed as overage. Daily counts reset at 00:00 UTC, monthly ones on the 1st. |
Every response on a plan with allowances carries what is left, for example:
X-Quota-Remaining: requests-today=187234; stream-minutes-month=118400Some plans receive odds after a short delay. The delay applies to prices only; results are never delayed.
Errors
Errors come with a code you can act on and a sentence you can log. Every response carries X-Request-Id: quote it when you contact us.
{ "code": "forbidden", "message": "request IP is not on this client's allowlist" }| Status | Code | Meaning |
|---|---|---|
| 400 | bad_request | A parameter is wrong; the message says which. |
| 401 | unauthorized | No key, or a key we do not know. |
| 403 | forbidden | The IP is not on your allowlist, the key is for the other environment, or the account is suspended. |
| 404 | not_found | No such id, or not one of yours. |
| 410 | resync | Your token is too old to continue: take a fresh snapshot. |
| 429 | rate_limited | Too many requests per second. Wait for Retry-After. |
| 429 | too_many_streams | All your stream slots are in use. |
| 429 | quota_exceeded | An allowance on your plan is used up. |
| 503 | starting | The server is loading the feed after a restart. Retry in a few seconds. |
| 503 | maintenance | Planned maintenance; the message says until when. |
Good practice
- Stream, do not poll. One stream replaces hundreds of requests a minute and delivers changes sooner.
- Treat updates as replacements. A
market-updatecarries the whole market: replace your copy, do not merge it. - Trade only on
OPEN. Close your book onSUSPENDED,BALL_RUNNINGor"stale": true. - Read
formatbefore you use a price. A bookmaker rate of 13 is not decimal 13. - Settle on
resultentries, and handleCORRECTEDandVOIDEDas well asSETTLED. - Send
Accept-Encoding: gzipandIf-None-Matchon REST calls. - Store our ids, not names. Names can change; ids never do.