diff --git a/CHANGELOG.md b/CHANGELOG.md index e32cde0..fc45cf4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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). diff --git a/content/en/api-reference/odds.mdx b/content/en/api-reference/odds.mdx index 4efc2fd..51280b0 100644 --- a/content/en/api-reference/odds.mdx +++ b/content/en/api-reference/odds.mdx @@ -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). | diff --git a/content/en/api-reference/stream.mdx b/content/en/api-reference/stream.mdx index 7fa92dc..f47431a 100644 --- a/content/en/api-reference/stream.mdx +++ b/content/en/api-reference/stream.mdx @@ -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 | @@ -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 | @@ -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. diff --git a/content/en/api-reference/websocket.mdx b/content/en/api-reference/websocket.mdx index 69f6709..8fdbdaa 100644 --- a/content/en/api-reference/websocket.mdx +++ b/content/en/api-reference/websocket.mdx @@ -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). diff --git a/public/openapi.json b/public/openapi.json index 322b842..d63bd23 100644 --- a/public/openapi.json +++ b/public/openapi.json @@ -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", @@ -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",