SharpRelayPinnacle data
HomeAPI referenceDashboard

Getting started

This guide takes you from zero to your first API call in about five minutes. The SharpRelay API delivers Pinnacle-sourced pre-game and live odds, opening & closing lines, results, limits, and fair (margin-free) odds over one REST API, authenticated with a single API key.

Clear provenance

The market data is sourced from Pinnacle and delivered through SharpRelay's independent API, authentication, metering, and reliability layer. SharpRelay is not affiliated with, endorsed by, or sponsored by Pinnacle.

Private-beta contract

Version 1 publishes 12 authenticated, read-only data routes. For props, discover markets on an event and request latest or available historical prices for one prop_id at a time. Discover outrights separately and use each returned special_id as the prop_id for its prices. Event-wide bulk prop prices and prop-specific opening, closing, results, and CLV operations are not exposed in the v1 private beta.

Activate your verified invitation

Private-beta access is issued only to a manually reviewed business. Open the single-use link supplied by the operator, review the exact displayed policies, confirm that you are authorized to bind the invited business, and complete the same-origin security challenge. The invitation arrives only in the URL fragment; the first-party page removes it from the address before activation and never sends it in a request target. Do not copy it into a query string, log, support ticket, or command line.

Successful activation opens the private one-time claim page. Save the API key immediately; neither the invitation nor the claim page starts a paid subscription, and beta access never converts to paid access automatically. The primary key has data:read and account:manage; create narrower data:read-only keys for deployed pollers.

A separate message asks you to verify the account mailbox for future lost-key recovery. Verification grants recovery authority but never reveals or changes an API key. Normal API use is available before it is complete.

Store your API key safely

Your key looks like sr_live_a1b2c3…. Keep it in a secrets manager or environment variable — never in source control, client-side code, or mobile apps. Full guidance in Authentication.

Important

Store the first successful response immediately. The exact claim handoff can recover an interrupted delivery for only 10 minutes. If you later lose one key, create or rotate a replacement; if you lose every management key, the secure recovery page can send a link only to the exact account mailbox you verified beforehand. Check the dashboard's Recovery email status while a management key still works.

Make your first call

Every request passes the key as a Bearer token:

$ curl -H "Authorization: Bearer sr_live_…" \
    https://api.worksinprod.dev/v1/catalog/sports

Response — a JSON array, plus exact metering and integrity headers:

HTTP/1.1 200 OK
Content-Type: application/json
X-Credits-Limit: 100000
X-Credits-Remaining: 99999
X-Credits-Period-Limit: 100000
X-Credits-Period-Remaining: 99999
X-Credits-Period-Reset: 2026-08-01T00:00:00Z
X-Credits-Daily-Limit: 10000
X-Credits-Daily-Remaining: 9999
X-Credits-Daily-Reset: 2026-07-23T00:00:00Z
X-Credits-Cost: 1
X-Result-Rows: 2
X-Result-Truncated: false
X-Next-Cursor: (present only when another live-event page exists)
X-Cache: MISS
X-Request-Id: req_9f12…
X-SharpRelay-Origin: https://api.worksinprod.dev

[{"sport_id": 29, "name": "Soccer"}, {"sport_id": 1, "name": "Tennis"}]
Metering

Ordinary routes use one credit per started block of up to 20 returned rows. Verified live=1 discovery can cost more because its charge is the greater of returned-row credits and bounded discovery/offer-check work; narrow filters and limit control that work. The X-Credits-* headers tell you where you stand on every metered response — see Quotas & metering.

Your first script

Discover ordinary pre-match events with /v1/events; without a kickoff window it returns only events whose start remains strictly after the request boundary and is still in the future when the response is delivered. Supplying an explicit starts_from/starts_to window narrows that lifecycle window. A present or future starts_from remains locally enforced as a future lower bound; only a genuinely past starts_from opts into historical discovery. For future discovery, starts_to is also locally enforced as an inclusive timestamp or complete UTC-day upper bound. An exact event_id or parent_id lookup is likewise lifecycle-specific. Use live=1 separately for currently verified in-play offers.

The odds call without full_history or since is the lowest-latency latest-snapshot profile. For event odds, this means every market row at the greatest positive line_id for each event and period; older off-board line IDs are excluded. This minimal poller switches to an explicit moving since cursor only after its first snapshot:

# Python (pip install requests)
import time, requests

API, KEY = "https://api.worksinprod.dev", "sr_live_…"
H = {"Authorization": f"Bearer {KEY}"}
event_id = 123456
cursor = None

while True:
    params = {}
    if cursor:
        params["since"] = cursor
    response = requests.get(f"{API}/v1/events/{event_id}/odds", headers=H, params=params)
    response.raise_for_status()
    rows = response.json()
    if response.headers.get("X-Result-Truncated") == "true":
        raise RuntimeError("history was truncated; do not advance the cursor")
    if rows:
        cursor = max(r["timestamp"] for r in rows)
        print(f"{len(rows)} new odds rows")
    time.sleep(5)
// Node.js 18+
const API = "https://api.worksinprod.dev", KEY = "sr_live_…";
const eventId = 123456;
let cursor = null;

for (;;) {
  const url = new URL(`${API}/v1/events/${eventId}/odds`);
  if (cursor) url.searchParams.set("since", cursor);
  const response = await fetch(url, {
    headers: { Authorization: `Bearer ${KEY}` }
  });
  if (!response.ok) throw new Error(`request failed: ${response.status}`);
  const rows = await response.json();
  if (response.headers.get("X-Result-Truncated") === "true") {
    throw new Error("history was truncated; do not advance the cursor");
  }
  if (rows.length) {
    cursor = rows.reduce((a, r) => a > r.timestamp ? a : r.timestamp, rows[0].timestamp);
    console.log(`${rows.length} new odds rows`);
  }
  await new Promise(r => setTimeout(r, 5000));
}

Next steps

TopicWhat you'll learn
Quotas & meteringCredit model, allowance headers, rate limits, monitoring usage
Polling & performanceCheap incremental polling, caching, freshness trade-offs
WorkflowsEnd-to-end recipes: fixtures → odds → closing, live loops, backfills
Errors & retriesError envelope, codes, retry strategy that stays within limits
API referenceEvery endpoint, parameter and response
DashboardLive quota, usage charts, key management