Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ to bump. Every change to API paths or response schemas gets a one-line entry her
the [OpenAPI Version Check](.github/workflows/openapi-version-check.yml) CI job
enforces that a bump has a matching entry.

## 2.2.0 — 2026-05-31

- Add `is_active` field to the `Odds` schema (`false` = market suspended/closed, price frozen; mirrors OpticOdds locked-odds; absent treated as `true`). SHA-3803.

## 2.1.0 — 2026-05-21

- Align `/account` response schema with the live flat shape (#230).
Expand Down
1 change: 1 addition & 0 deletions content/en/api-reference/odds.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -329,6 +329,7 @@ X-Request-Id: req_abc123def456
| `wire_received_at` | string\|undefined | ISO 8601 timestamp of when SharpAPI first observed a content change for this row, carried forward across subsequent unchanged refreshes. **Use this field for ingest-latency benchmarking** — it isolates SharpAPI's pipeline contribution from the sportsbook's source-side publish cadence. Omitted on cold-start rows where no prior observation exists. See [Understanding Pinnacle's `odds_changed_at`](/en/concepts/pinnacle-odds-changed-at/#benchmarking-pipeline-latency). |
| `odds_changed_at` | string | ISO 8601 timestamp of the sportsbook's own source update for this line, when available. On Pinnacle, carries forward while the price/line/`is_live` flag are unchanged (see [Understanding Pinnacle's `odds_changed_at`](/en/concepts/pinnacle-odds-changed-at/)). Not suitable for SharpAPI pipeline-latency benchmarking — use `wire_received_at` for that. |
| `is_live` | boolean | Whether the event is currently live |
| `is_active` | boolean | `true` (default) = market open and bettable; `false` = market suspended/closed with the price **frozen** at its last value (so you can grey it out rather than trust a stale price). Mirrors OpticOdds' `locked-odds`, but as a queryable field you can also filter on. Absent is treated as `true`. An active→suspended transition is pushed on the [odds stream](/en/api-reference/stream/) as an `odds:update` carrying `is_active: false`. |
| `event_uuid` | string\|undefined | Stable canonical event UUID from the SharpAPI atlas, when the event is mapped. Where `event_id` carries the adapter's primary event identifier (often the originating sportsbook's), `event_uuid` is a feed-stable hash you can use for cross-feed joins. Absent for unmapped events. |
| `external_event_id` | string\|undefined | The sportsbook's own native event ID, when distinct from `event_id`. Useful for round-tripping rows back to the sportsbook's UI or API. |
| `deep_link` | string\|undefined | Resolver URL pointing to the sportsbook's event or bet-slip page. Pass `state=` (e.g. `state=nj`) on the request to route through state-specific subdomains for books that need them (BetMGM, Caesars, BetRivers). |
Expand Down
22 changes: 21 additions & 1 deletion content/en/api-reference/stream.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ https://api.sharpapi.io/api/v1/stream?api_key=sk_live_your_key

| Channel | Events Delivered | Use Case |
|---------|-----------------|----------|
| `odds` | `snapshot`, `odds:update`, `odds:removed`, `heartbeat` | Track odds movements |
| `odds` | `snapshot`, `odds:update`, `odds:locked`, `odds:removed`, `heartbeat` | Track odds movements |
| `opportunities` | `snapshot`, `ev:detected/expired`, `arb:detected/expired`, `middles:detected/expired`, `low_hold:detected/expired`, `heartbeat` | Alert on opportunities |
| `gamestate` | `gamestate:snapshot`, `gamestate:update`, `gamestate:removed`, `heartbeat` | Live scores, periods, clocks, and situational data per event. **Enterprise tier only.** See [Live Game State](/api-reference/gamestate/) for the full field catalog. |
| `all` | All event types | Full real-time picture |
Expand Down Expand Up @@ -132,8 +132,14 @@ data: {"odds":[{"id":"123456","odds_american":-150,"odds_decimal":1.667,"odds_pr
| `odds_probability` | number | Updated implied probability (e.g. `0.6`) |
| `line` | number \| null | Updated line/spread (e.g. `-3.5`), or `null` for moneyline |
| `is_live` | boolean | Whether the event is currently live |
| `is_active` | boolean | `true` = market open/bettable; `false` = market suspended/closed with the price frozen. A market suspending (e.g. after a goal) emits an `odds:update` with `is_active: false` — grey out the line rather than trusting the frozen price. See also the [`odds:locked`](#odds-locked) event. |
| `is_main_line` | boolean | `true` when this line is the consensus main line for its market; `false` for alternate lines. Can flip as the main line moves. |
| `is_alternate_line` | boolean | Positive-polarity sibling of `is_main_line` (mutually exclusive). |
| `is_stale_pregame_price` | boolean | `true` when a live row still carries a pre-game price that hasn't moved since kickoff. |
| `odds_changed_at` | string | ISO 8601 timestamp of the sportsbook's own source update for this line, when available. On Pinnacle, carries forward while the underlying price/line/`is_live` flag are unchanged — see [Understanding Pinnacle's `odds_changed_at`](/en/concepts/pinnacle-odds-changed-at/). |

Exchange books additionally carry the dynamic `volume`, `volume_24h`, `open_interest`, and `max_bet` fields when present. Everything else — `sportsbook`, `sport`, `league`, `home_team`, `away_team`, `market_type`, `selection`, `deep_link`, `event_start_time`, and the nested entity refs — is **static** and comes from the initial `snapshot`; merge each delta into your local map by `id` and never read a static field off a delta.

**Envelope fields:**

| Field | Type | Description |
Expand Down Expand Up @@ -223,6 +229,20 @@ id: evt_00050
data: {"expired":["lowhold_abc123"]}
```

### `odds:locked`

Fired when a market is **suspended/closed** (e.g. after a goal, during a line move, or a late-game lockout) — the price is **frozen** but the selection is no longer bettable. Carries the suspended subset of the current delta, same payload shape as `odds:update`, with `is_active: false`. Only sent on `odds` or `all` channels.

This is a 1:1 analogue of OpticOdds' `locked-odds` for easy migration. It is **supplementary** — the same rows also arrive in `odds:update` with `is_active: false`, so clients that already read `is_active` need not subscribe to `odds:locked` separately. Use it when you want a dedicated lock signal without parsing every `odds:update`.

```
event: odds:locked
id: evt_00052
data: {"odds":[{"id":"123456","odds_american":-2500,"is_live":true,"is_active":false,"odds_changed_at":"2026-02-08T18:47:38Z"}],"count":1,"book":"pinnacle","partial":false}
```

A market re-opening emits a normal `odds:update` with `is_active: true` (and a fresh price). Markets a book **removes entirely** come through [`odds:removed`](#odds-removed) instead.

### `odds:removed`

Odds removed by a sportsbook (e.g. market taken down, event settled). Only sent on `odds` or `all` channels.
Expand Down
17 changes: 17 additions & 0 deletions content/en/api-reference/websocket.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -283,6 +283,23 @@ Incremental odds update from a single sportsbook.
}
```

#### `odds:locked`

A market was **suspended/closed** (e.g. after a goal, a line move, or a late-game lockout) — the price is **frozen** and no longer bettable. Carries the suspended subset of the delta (same payload as `odds:update`, with `is_active: false`). A 1:1 analogue of OpticOdds' `locked-odds`.

Supplementary: the same rows also arrive in `odds:update` with `is_active: false`, so clients reading `is_active` need not subscribe separately. A re-open emits a normal `odds:update` with `is_active: true`; a full removal comes through `odds:removed`.

```json
{
"type": "odds:locked",
"seq": 48,
"source": "pinnacle",
"data": [ /* NormalizedOdds[] with is_active: false */ ],
"count": 1,
"timestamp": "2026-02-08T18:47:19.250Z"
}
```

#### `odds:removed`

Odds removed by a sportsbook (e.g. market taken down, event settled).
Expand Down
6 changes: 5 additions & 1 deletion public/openapi.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"openapi": "3.1.0",
"info": {
"title": "SharpAPI",
"version": "2.1.0",
"version": "2.2.0",
"description": "Real-time sports betting odds API with +EV detection, arbitrage, middles, and low-hold opportunities.\n\n## Spec Versioning\n\n`info.version` is bumped on every schema or path change. Minor version (`2.x.0`) for additive changes or breaking shape fixes that align the spec to the live response; major version (`x.0.0`) for backward-incompatible redesigns. Removed paths and renamed fields always bump the minor at minimum. Check `x-generated-at` and `x-commit-sha` for the build provenance of a given snapshot.\n\n## Authentication\n\nAll authenticated endpoints accept an API key via one of three methods:\n\n| Method | Header / Param | Use case |\n|--------|---------------|----------|\n| `X-API-Key` | `X-API-Key: sk_live_...` | Recommended for server-side |\n| `Authorization` | `Authorization: Bearer sk_live_...` | Standard Bearer token |\n| `api_key` query | `?api_key=sk_live_...` | SSE/EventSource (cannot set headers) |\n\n## Subscription Tiers\n\n| Tier | Rate Limit | Data Delay | Max Books | EV | Arb | Middles | Game State |\n|------|-----------|------------|-----------|-----|-----|---------|------------|\n| Free | 12/min | 60s | 2 (DK, FD) | - | - | - | - |\n| Hobby | 120/min | Real-time | 5 | - | Yes | - | - |\n| Pro | 300/min | Real-time | 15 | Yes | Yes | Yes | - |\n| Sharp | 1000/min | Real-time | All | Yes | Yes | Yes | - |\n| Enterprise | Custom | Real-time | All | Yes | Yes | Yes | Yes |\n\n## Rate Limit Headers\n\nEvery authenticated response includes:\n\n- `X-RateLimit-Limit` - Requests allowed per minute\n- `X-RateLimit-Remaining` - Requests remaining in current window\n- `X-RateLimit-Reset` - Unix timestamp when the window resets\n- `X-Data-Delay` - Odds delay in seconds for your tier (0 = real-time)\n- `X-Request-Id` - Unique request identifier for support\n\n## WebSocket Streaming\n\nThe WebSocket endpoint at `wss://ws.sharpapi.io` is documented separately in [`asyncapi.yaml`](./asyncapi.yaml) (AsyncAPI 3.0). OpenAPI 3.x cannot express WebSocket subprotocols and message channels, so the SSE endpoint (`/stream`) is the only stream covered by this document.\n\n## MCP Server\n\nThe `POST /mcp` endpoint is a Model Context Protocol server (JSON-RPC 2.0 over Streamable HTTP). Tools are self-described at runtime via `tools/list`, so it's documented as a setup guide rather than an OpenAPI path — see [`/sdks/mcp`](https://docs.sharpapi.io/sdks/mcp).\n",
"contact": {
"name": "SharpAPI Support",
Expand Down Expand Up @@ -4458,6 +4458,10 @@
"is_live": {
"type": "boolean"
},
"is_active": {
"type": "boolean",
"description": "true (default) = market open and bettable; false = market suspended/closed with the price frozen. Mirrors OpticOdds locked-odds but exposed as a queryable field. Absent is treated as true. An active->suspended transition is emitted on the odds stream (odds:update with is_active=false)."
},
"timestamp": {
"type": "string",
"format": "date-time",
Expand Down