> For the complete documentation index, see [llms.txt](https://mute-sh.gitbook.io/mute.sh/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://mute-sh.gitbook.io/mute.sh/api/api-reference/sports.md).

# Sports (Enterprise)

The enriched sports surface — HIP-4 sports markets joined with live fixture data (scores, events, kickoff status), plus fixture news/highlights, real-time streams, and match-event webhooks. **Enterprise tier only** (the auth model is spelled out at the bottom).

## GET /v1/sports/match-markets

The main course: sports outcomes grouped into **match** (head-to-head) and **tournament** (futures ladder) cards, with team codes, flags, live match status, and per-outcome prices — everything a sportsbook-style UI needs in one call.

| Query param               | Type   | Default  | Description                                                                        |
| ------------------------- | ------ | -------- | ---------------------------------------------------------------------------------- |
| `type`                    | enum   | `all`    | `match` \| `tournament` \| `all`                                                   |
| `status`                  | enum   | `active` | `active` \| `settled` \| `all`                                                     |
| `limit`                   | int    | `20`     | 1–50 — caps **groups**, not outcomes (a tournament group carries its whole ladder) |
| `category` / `underlying` | string | —        | exact-match against raw metadata values; prefer `type`/`status` for filtering      |

```bash
curl -H "X-API-Key: $MUTE_KEY" "https://api.mute.sh/v1/sports/match-markets?type=match"
```

```json
{
  "count": 16,
  "groups": [
    {
      "type": "match",
      "questionName": "World Cup Round of 16: Mexico vs England",
      "homeTeam": "Mexico",
      "awayTeam": "England",
      "homeCode": "MEX",
      "awayCode": "ENG",
      "homeLogo": "https://flagcdn.com/mx.svg",
      "awayLogo": "https://flagcdn.com/gb-eng.svg",
      "competition_logo_url": "https://img.mute.sh/competitions/world-cup-2026.webp",
      "stage": "Round of 16",
      "kickoff_at": "2026-07-06T01:00Z",
      "kickoff_status": "STATUS_FULL_TIME",
      "is_live": false,
      "is_concluded": true,
      "match_minute": "FT",
      "fixture": null,
      "fixtureConfidence": null,
      "outcomes": [
        {
          "outcomeId": 738,
          "side": 0,
          "title": "Mexico",
          "coinYes": "#7380",
          "coinNo": "#7381",
          "lastPxYes": 0,
          "lastPxNo": 1,
          "volumeUsd": 2796885.55,
          "volume24h": 2722071.95,
          "traders": 819,
          "trades": 9592,
          "lastTradeAt": "2026-07-06 06:25:51.015",
          "isSettled": false,
          "logo_url": "https://flagcdn.com/mx.svg"
        },
        {
          "outcomeId": 738,
          "side": 1,
          "title": "England",
          "coinYes": "#7381",
          "coinNo": "#7380",
          "lastPxYes": 1,
          "lastPxNo": 0,
          "volumeUsd": 2796885.55,
          "volume24h": 2722071.95,
          "traders": 819,
          "trades": 9592,
          "lastTradeAt": "2026-07-06 06:25:51.015",
          "isSettled": false,
          "logo_url": "https://flagcdn.com/gb-eng.svg"
        }
      ],
      "totalVolume24h": 2722071.95,
      "totalTraders": 819
    }
  ]
}
```

The load-bearing semantics:

* **`is_concluded` runs ahead of `isSettled`.** Match status comes from the live sports feed (full-time seconds after the whistle); `isSettled` flips only when HIP-4 settles on-chain, minutes later. The Mexico row above shows exactly that gap: `match_minute: "FT"`, price collapsed to 0, `isSettled: false`. Use `is_concluded` for UI state, `isSettled` for accounting.
* **Match groups** have one outcome per team (each with its own Yes/No coin pair); **tournament groups** are futures ladders (one outcome per contender — `status=settled` shows the eliminated ones at `lastPxYes: 0`).
* **Two-sided (knockout) markets emit both teams.** A knockout fixture is a single two-sided market upstream; we emit it as **two symmetric outcome entries** (home first, matching `homeTeam`/`awayTeam` order) sharing one `outcomeId` and differing by `side` (`0` | `1`). The coin pairs mirror each other (`side: 1` has `coinYes`/`coinNo` and `lastPxYes`/`lastPxNo` flipped — the two `lastPxYes` values sum to 1). Key outcomes by `outcomeId:side`, not `outcomeId` alone. **Volume/trader stats on each side entry are full-market figures** (they describe the shared market, not one side) — group `totalVolume24h`/`totalTraders` already count each market once.
* **Coin format:** coins are always the full concatenated number — `#(outcomeId × 10 + sideIndex)`, e.g. `#7610`/`#7611` for outcome 761. There is no dash form (`#761-0` is not a valid coin anywhere in the API or on the exchange).
* `fixture` is a best-effort deep join to the fixture provider (`fixtureConfidence` scores the name-match); it's `null` when no binding is available — always render from the ESPN-derived fields (`kickoff_*`, `is_live`, `match_minute`) first and treat `fixture` as bonus detail.
* Logos come as ready URLs: flags for teams, `img.mute.sh` badges for competitions.

**Caching:** `public, max-age=15` — match-pace data.

## GET /v1/sports/tournament-stage

Returns the current stage/round of a tournament (Group Stage, Round of 32, Round of 16, Quarterfinals, Semifinals, Third Place, Final), derived in real time from listed markets — no client-side hardcoding needed. The latest round with unsettled markets is `active`; earlier rounds are `completed`; later rounds are `upcoming`.

```bash
curl -H "X-API-Key: $MUTE_KEY" "https://api.mute.sh/v1/sports/tournament-stage?tournament=World%20Cup"
```

| Query param  | Default     | Description                                             |
| ------------ | ----------- | ------------------------------------------------------- |
| `tournament` | `World Cup` | Tournament name, substring-matched against market names |

```json
{
  "tournament": "World Cup",
  "currentStage": "Quarterfinals",
  "stages": [
    { "stage": "Group Stage",   "status": "completed", "matchCount": 72, "settledCount": 72 },
    { "stage": "Round of 32",   "status": "completed", "matchCount": 16, "settledCount": 14 },
    { "stage": "Round of 16",   "status": "completed", "matchCount": 8,  "settledCount": 5 },
    { "stage": "Quarterfinals", "status": "active",    "matchCount": 4,  "settledCount": 0 },
    { "stage": "Semifinals",    "status": "upcoming",  "matchCount": 0,  "settledCount": 0 },
    { "stage": "Third Place",   "status": "upcoming",  "matchCount": 0,  "settledCount": 0 },
    { "stage": "Final",         "status": "upcoming",  "matchCount": 0,  "settledCount": 0 }
  ],
  "asOf": "2026-07-09T12:00:00.000Z"
}
```

* `matchCount` counts distinct match markets in that round; `settledCount` counts matches fully settled on-chain (settlement can lag the final whistle).
* `status: "upcoming"` rounds may have `matchCount: 0` — markets for the next round are listed as the tournament progresses.
* **Caching:** 60 s — stage is slow-moving.

## GET /v1/sports/markets/:coin

Binds a single coin to its live fixture: give it either side's coin, get `{coin, outcomeId, confidence, sideA, sideB, fixture}`. Only meaningful for **match-type** markets (a tournament futures coin has `Yes`/`No` sides, nothing to bind against — those `404`). Distinct `404` messages tell you what failed: no metadata for the coin, no fixture match for the team pair, or fixture not retrievable upstream. **Caching:** 15 s.

## Fixture enrichment

Three routes keyed by the fixture id (as found in stream/webhook payloads and fixture bindings). All three depend on the upstream sports-data provider — when it's unreachable they return `503 "Sports data is temporarily unavailable."`; treat that as retryable, not as a bad request.

### GET /v1/sports/fixtures/:id

The enriched fixture: `{id, name, league, state, status, startingAt, home, away, score, events[], source}` — participants with logos/short codes, current score, and a minute-sorted event timeline (goals, cards, subs). Invalid ids (non-numeric, ≤0, >10^12) are a `400 Invalid id` before any upstream call.

### GET /v1/sports/fixtures/:id/news

Recent news for the fixture's teams: `{fixtureId, query, source: "google-news", articles: [{id, title, link, source, publishedAt}]}`. The `query` field shows exactly what was searched. An empty `articles[]` is an honest empty — never fabricated content. **Caching:** 30 min.

### GET /v1/sports/fixtures/:id/highlights

Highlight embeds from Scorebat: `{fixtureId, matched, source: "scorebat", thirdParty: true, providerLabel, title, competition, videos: [{id, embedUrl}]}`. `matched: false` + empty `videos` means the feed has no coverage for that competition — check `matched` before rendering a highlights section. Videos are embed URLs only (third-party content, marked as such). **Caching:** 15 s.

***

## Real-time fixture streams

Both streams deliver the same payloads — **snapshot on connect, then per-change updates** — over your transport of choice. Pass `fixtures` (comma-separated ids) and your key as `?apikey=` (neither transport can set headers from a browser).

### GET /v1/sports/stream (SSE)

```bash
curl -N "https://api.mute.sh/v1/sports/stream?fixtures=19700202&apikey=$MUTE_KEY"
```

```
: connected 2026-07-06T08:54:10.450Z

event: snapshot
data: [ …EnrichedFixture[]… ]

event: update
data: { …one EnrichedFixture… }

: ping 1783330475123
```

* `snapshot` is an **array** (one entry per requested fixture the poller has data for — `[]` is valid for fixtures not yet tracked or without data); `update` frames carry **one** fixture object, sent only when something actually changed.
* Missing `fixtures` param → clean `400` before the stream opens.
* Heartbeat comment every \~25 s; standard caps (500 global, 3 per principal, 30-min lifetime → reconnect).

### /v1/sports/ws (WebSocket)

`wss://api.mute.sh/v1/sports/ws?fixtures=…&apikey=…` — JSON text frames:

```json
{ "type": "snapshot", "fixtures": [] }
{ "type": "update", "fixture": { "…": "…" } }
{ "type": "ping", "ts": 1783330475123 }
{ "type": "error", "error": "…" }
```

The server closes with an `error` frame at the 30-minute lifetime — reconnect as routine. **Auth quirk to know:** WS auth failures are rejected at the socket level before the handshake completes, and the edge surfaces that as an opaque **`502`** rather than a `401` — if your WS client reports 502, check the `apikey` before suspecting an outage.

***

## Match-event webhooks: /v1/sports/webhooks/subscriptions

Push `match.*` events (kickoff, goals, cards, subs, VAR, status changes, full-time) to your endpoint, filtered to the fixtures you care about. Creation is **Enterprise-gated**; management uses the standard webhooks permission.

### POST /v1/sports/webhooks/subscriptions

```bash
curl -X POST -H "X-API-Key: $MUTE_KEY" -H "Content-Type: application/json" \
  -d '{
    "url": "https://your-server.example/hooks/sports",
    "event_types": ["match.goal", "match.card"],
    "fixtures": [19700202]
  }' \
  "https://api.mute.sh/v1/sports/webhooks/subscriptions"
```

```json
{
  "id": "59569498-0c81-44ea-9378-def764c11b98",
  "orgId": "5958b5b9-…",
  "url": "https://your-server.example/hooks/sports",
  "secret": "4657d88cc565dca6326b741947ed3dd217eb86e7f898243fb275d96211c55bbf",
  "event_types": ["match.goal", "match.card"],
  "fixtures": [19700202],
  "createdAt": "2026-07-06T08:54:19.877Z",
  "paused": false
}
```

* **`secret` is returned once, at creation only** (list/get responses strip it). It's the HMAC-SHA256 signing key for deliveries; omit it and the server generates a strong one for you.
* `event_types` values: `match.kickoff`, `match.goal`, `match.card`, `match.substitution`, `match.var`, `match.status_change`, `match.ft`. **Spell them exactly** — the body is stored as-sent, and an unrecognized string doesn't error, it just never matches an event (a typo is a silently dead subscription). Empty array = all event types.
* `fixtures: []` = all fixtures your org's subscriptions track; listing fixtures also registers them with the live poller.
* Non-Enterprise keys pass auth but get a `403` explaining the Enterprise gate here.

### GET (list) · GET /:id · DELETE /:id

Standard org-scoped management: list returns all your subscriptions (secrets stripped, no pagination), get-by-id `404`s identically for missing and other-org ids (no existence leak), delete returns `204` and unregisters the fixtures from the poller.

***

## The sports auth model (all routes)

| You send              | Result                                                   |
| --------------------- | -------------------------------------------------------- |
| Enterprise-tier key   | full access                                              |
| Valid key, lower tier | `403` naming the Enterprise requirement                  |
| Invalid/revoked key   | `401`                                                    |
| No credential at all  | `403` (there is no anonymous tier on the sports surface) |


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
GET https://mute-sh.gitbook.io/mute.sh/api/api-reference/sports.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
