|
| 1 | +--- |
| 2 | +description: "SharpAPI response conventions — envelope shapes, timestamps, error format, field casing, and the rules that every REST endpoint follows." |
| 3 | +--- |
| 4 | + |
| 5 | +import { Callout } from 'nextra/components' |
| 6 | + |
| 7 | +# Response Conventions |
| 8 | + |
| 9 | +SharpAPI returns predictable response shapes across every REST endpoint. This page codifies those conventions so you can build generic parsers, not per-endpoint special cases. |
| 10 | + |
| 11 | +<Callout type="info"> |
| 12 | + This page describes the REST conventions. SSE streams are covered in [SSE Stream](/en/api-reference/stream), and the bidirectional WebSocket protocol has its own [AsyncAPI 3.0 spec](/asyncapi.yaml). |
| 13 | +</Callout> |
| 14 | + |
| 15 | +## HTTP status codes, not envelopes |
| 16 | + |
| 17 | +The HTTP status code is the source of truth for success or failure. Response bodies do **not** carry a `success` flag — it would duplicate information already on `response.status` and invites clients to check the wrong place. |
| 18 | + |
| 19 | +| Status | Meaning | |
| 20 | +|---|---| |
| 21 | +| `200` | Success with a body | |
| 22 | +| `201` | Created (API key creation) | |
| 23 | +| `204` | Success, no body (key deletion) | |
| 24 | +| `302` | Redirect (deeplinks) | |
| 25 | +| `400` | Validation error — client-side bug | |
| 26 | +| `401` | Missing or invalid API key | |
| 27 | +| `403` | Tier doesn't include the requested feature | |
| 28 | +| `404` | Resource doesn't exist (or opaque ID didn't match) | |
| 29 | +| `429` | Rate limit exceeded | |
| 30 | +| `5xx` | Upstream or internal server error | |
| 31 | + |
| 32 | +## Success envelope shapes |
| 33 | + |
| 34 | +Two top-level shapes. Both put `data` first and `updated_at` last, with an optional pagination block in between. |
| 35 | + |
| 36 | +### Non-paginated response |
| 37 | + |
| 38 | +Used for singletons, reference data, and summary endpoints. `data` is whatever the endpoint returns — a single resource, an array, or a map. |
| 39 | + |
| 40 | +```json |
| 41 | +{ |
| 42 | + "data": { ... }, |
| 43 | + "updated_at": "2026-04-16T19:29:38.920698424Z" |
| 44 | +} |
| 45 | +``` |
| 46 | + |
| 47 | +### Paginated list response |
| 48 | + |
| 49 | +Used for endpoints that support `limit` / `offset` / `cursor`. |
| 50 | + |
| 51 | +```json |
| 52 | +{ |
| 53 | + "data": [ ... ], |
| 54 | + "pagination": { |
| 55 | + "limit": 50, |
| 56 | + "offset": 0, |
| 57 | + "count": 50, |
| 58 | + "total": 1247, |
| 59 | + "has_more": true, |
| 60 | + "next_offset": 50 |
| 61 | + }, |
| 62 | + "updated_at": "2026-04-16T19:29:38.920698424Z" |
| 63 | +} |
| 64 | +``` |
| 65 | + |
| 66 | +`count` is the length of the current `data` page, `total` is the full matching set. `next_offset` is present only when `has_more` is `true`. |
| 67 | + |
| 68 | +### Endpoint-specific extensions |
| 69 | + |
| 70 | +A handful of endpoints add extra top-level keys alongside `data`. These are additive — generic parsers that read `data` + `pagination` + `updated_at` keep working. |
| 71 | + |
| 72 | +| Field | Emitted by | Meaning | |
| 73 | +|---|---|---| |
| 74 | +| `overflow: true` | `/odds`, `/odds/delta` | `total > 10_000` — consumer should pull a fresh snapshot via `/odds` instead of paginating through delta. | |
| 75 | +| `removed: [...]` | `/odds/delta` | IDs of odds removed since `?since=`. | |
| 76 | +| `missing: [...]` | `POST /odds/batch` | Event IDs that were requested but not found. | |
| 77 | + |
| 78 | +## Error envelope |
| 79 | + |
| 80 | +Every non-2xx response returns a single `error` object. |
| 81 | + |
| 82 | +```json |
| 83 | +{ |
| 84 | + "error": { |
| 85 | + "code": "rate_limited", |
| 86 | + "message": "Rate limit exceeded. Retry after 3 seconds.", |
| 87 | + "docs": "https://sharpapi.io/docs/rate-limits", |
| 88 | + "retryAfter": 3 |
| 89 | + } |
| 90 | +} |
| 91 | +``` |
| 92 | + |
| 93 | +| Field | Always present? | Notes | |
| 94 | +|---|---|---| |
| 95 | +| `code` | Yes | Stable string. Check this, not the prose message. | |
| 96 | +| `message` | Yes | Human-readable English. Safe to surface to end users. | |
| 97 | +| `docs` | Sometimes | Link to the relevant doc page. | |
| 98 | +| `retryAfter` | On 429 / 5xx | Seconds until the client should retry. | |
| 99 | +| `tier` | On 403 | The tier that *would* unlock the endpoint. | |
| 100 | + |
| 101 | +### Common error codes |
| 102 | + |
| 103 | +`missing_api_key`, `invalid_api_key`, `validation_error`, `invalid_request`, `tier_restricted`, `rate_limited`, `too_many_streams`, `not_found`, `upstream_error`, `internal_error`. |
| 104 | + |
| 105 | +## Timestamps |
| 106 | + |
| 107 | +Every timestamp in a SharpAPI response is **RFC 3339 / ISO 8601 with UTC** — string form like `2026-04-16T19:29:38.920698424Z`. Nanosecond precision from the server; clients can safely parse with second or millisecond precision. |
| 108 | + |
| 109 | +Two fields you'll see often: |
| 110 | + |
| 111 | +- **`updated_at`** — when the server emitted the response. Top-level on every success response. |
| 112 | +- **`fetched_at`** — when the upstream sportsbook was last polled (present in odds payloads). |
| 113 | + |
| 114 | +Older, stream-layer timestamps appear as Unix-seconds floats (`timestamp`) alongside their RFC 3339 counterpart (`ts`) on WebSocket messages. |
| 115 | + |
| 116 | +## Field casing |
| 117 | + |
| 118 | +**Snake case everywhere.** Fields inside `data`, `pagination`, `meta`, `error`, and every WebSocket / SSE message use `snake_case` — `event_id`, `market_type`, `profit_percent`, `detected_at`, `odds_american`, `stake_percent`. |
| 119 | + |
| 120 | +The few `camelCase` leftovers you might spot (e.g. `eventIds` in a WebSocket `subscribe` payload *input*) are client-to-server request shapes. Everything server-to-client is snake_case. |
| 121 | + |
| 122 | +## Canonical IDs |
| 123 | + |
| 124 | +Most identifiers are stable, opaque, and joinable across endpoints. |
| 125 | + |
| 126 | +| Field | Format | Example | |
| 127 | +|---|---|---| |
| 128 | +| `event_id` | `{league}_{home}_{away}_{date}` | `mlb_guardians_orioles_2026-04-16` | |
| 129 | +| `game_id` | Same as `event_id` in runner output | `nba_thunder_timberwolves_2026-03-15` | |
| 130 | +| `hash_id` (middles) | 16-char lowercase hex | `cc229ed94a2ee679` | |
| 131 | +| `betting_tool_id` | Human-readable canonical key | `middle:prematch:mlb_...:total_runs:...` | |
| 132 | +| API key | Prefixed token | `sk_live_...` | |
| 133 | + |
| 134 | +Join `/splits` and `/odds` by `event_id`. Track an opportunity across polls by `hash_id`. |
| 135 | + |
| 136 | +## Rate limit headers |
| 137 | + |
| 138 | +Every authenticated response includes: |
| 139 | + |
| 140 | +| Header | Meaning | |
| 141 | +|---|---| |
| 142 | +| `X-RateLimit-Limit` | Requests allowed per minute for your tier. | |
| 143 | +| `X-RateLimit-Remaining` | Requests remaining in the current window. | |
| 144 | +| `X-RateLimit-Reset` | Unix timestamp when the window resets. | |
| 145 | +| `X-Data-Delay` | Odds delay in seconds for your tier (`0` = real-time). | |
| 146 | +| `X-Request-Id` | Unique request identifier — include this in support requests. | |
| 147 | + |
| 148 | +## What this is *not* |
| 149 | + |
| 150 | +- **No `success` field** on responses. HTTP status codes do that job. |
| 151 | +- **No envelope-of-envelopes.** The top-level object is the envelope; `meta.updated_at` nesting is not used. |
| 152 | +- **No mixed casing.** Any response field is snake_case. Any WebSocket *input* field may be camelCase — everything else is snake_case. |
| 153 | + |
| 154 | +## Machine-readable source of truth |
| 155 | + |
| 156 | +- REST surface: [`openapi.json`](/openapi.json) (OpenAPI 3.1) |
| 157 | +- WebSocket surface: [`asyncapi.yaml`](/asyncapi.yaml) (AsyncAPI 3.0) |
| 158 | + |
| 159 | +Both files are published as static assets and can be diffed in CI. If this page drifts from the spec, the spec wins — the spec is generated from the deployed server. |
0 commit comments