Skip to content

Build / query / stream

Build on the live league.

Start with REST to request Major League Pickleball fixtures, lineups, standings and history. When you need updates without repeatedly asking for them, connect to the WebSocket feed. Every response uses JSON, and the examples below are designed to be copied and adapted.

Your first five minutes

  1. 1. Create and verify your account

    Registration starts a 14-day Pro trial. Once your email is verified, your API key becomes active.

  2. 2. Copy the key from your dashboard

    Keep the key on your server or in a secure environment variable. Do not place it in public browser code or commit it to a repository.

  3. 3. Request in-progress fixtures

    Copy one of the examples below and replace pbl_live_YOUR_KEY with your own key. A successful response returns JSON containing the fixtures currently in progress.

    cURL

    curl -s "https://pickleball-api.com/v1/fixtures?status=in_progress" \
      -H "Authorization: Bearer pbl_live_YOUR_KEY"

    JavaScript

    const response = await fetch(
      'https://pickleball-api.com/v1/fixtures?status=in_progress',
      {
        headers: {
          Authorization: 'Bearer pbl_live_YOUR_KEY',
        },
      },
    );
    const body = await response.json();
    console.log(body.data);

    Python

    import requests
    
    response = requests.get(
        'https://pickleball-api.com/v1/fixtures',
        params={'status': 'in_progress'},
        headers={'Authorization': 'Bearer pbl_live_YOUR_KEY'},
    )
    response.raise_for_status()
    print(response.json()['data'])
  4. 4. Open one fixture

    Use the returned fixture id with the fixture and live endpoints to retrieve its lineup matches and the latest observation for each.

  5. 5. Add live updates

    During the trial or on Pro, have your server create a short-lived ticket. It can hand that single-use ticket to a browser, which connects to the WebSocket feed and subscribes to a fixture or match topic for a full snapshot followed by live changes.

Overview

The base URL for all requests is your regional API host. Examples on this page use a placeholder:

Base URL   https://pickleball-api.com
Version    /v1  (in the path — breaking changes ship under a new version)
Format     application/json; UTF-8
Streaming  wss://pickleball-api.com/v1/live
Coverage   Major League Pickleball, exclusively, for now

Each account receives one API key. You can still invite teammates to the dashboard, and there is no charge per person. Your plan limits how many requests and live messages the account can use. Every new verified account begins with a 14-day Pro trial, with no card required.

Authentication

Authenticate with your API key as a Bearer token. Keys are prefixed pbl_live_. Send it in the Authorization header on every request — never in a query string or client-side code you ship to end users.

cURL

curl -s "https://pickleball-api.com/v1/fixtures?status=in_progress" \
  -H "Authorization: Bearer pbl_live_YOUR_KEY"

JavaScript

const response = await fetch(
  'https://pickleball-api.com/v1/fixtures?status=in_progress',
  {
    headers: {
      Authorization: 'Bearer pbl_live_YOUR_KEY',
    },
  },
);
const body = await response.json();
console.log(body.data);

Python

import requests

response = requests.get(
    'https://pickleball-api.com/v1/fixtures',
    params={'status': 'in_progress'},
    headers={'Authorization': 'Bearer pbl_live_YOUR_KEY'},
)
response.raise_for_status()
print(response.json()['data'])

Missing or invalid credentials return 401 unauthorized. A valid key on a plan that lacks the requested feature (for example, WebSockets or prediction markets on Basic) returns 403.

Browser-direct REST requests are not supported: API responses do not include CORS permission headers. Call REST endpoints and mint realtime tickets from your server. If your user interface needs a direct live connection, return only the short-lived ticket to it; never return the API key.

Response format

Successful responses wrap the payload in a consistent envelope. data is the resource (object or array); meta always carries the request id and a server timestamp, plus pagination and source freshness metadata when they apply to that resource.

{
  "data": [ /* resource or list */ ],
  "meta": {
    "request_id": "b1f6…",
    "generated_at": "2026-07-14T11:16:18Z",
    "observed_at": "2026-07-14T11:16:15Z",
    "freshness_seconds": 3,
    "completeness": "full",
    "confidence": "high",
    "pagination": { "next_cursor": null, "has_more": false }
  }
}

meta.observed_at is the source observation time, not the time our server answered the request. meta.freshness_seconds is the rounded difference between the current time and that observation — use it as a staleness indicator rather than assuming every successful response reflects this exact instant. These fields are omitted when a resource has no meaningful source-observation clock. meta.completeness is full or partial (for example, a fixture created by live scoring before its lineup metadata has fully resolved — see is_provisional on the fixture object). Completeness describes field resolution, not recency, and confidence describes source confidence rather than a probability that the score is correct. Always log meta.request_id — quote it in support requests and it lets us trace a single call end to end.

Operationally, discover in-progress fixtures no more than once every 60 seconds and refresh an actively watched fixture every 10–15 seconds. These are expected integration cadences, not a data-delivery SLA. Show the observation time in user-facing live products and mark data delayed whenever its age exceeds the cadence your product promises.

Errors

Errors are designed to be useful

Every error response includes a stable code and a request_id. Your application can react to the code, while support can use the request ID to trace the individual call.

A 401 usually means the key is missing or invalid. A 403 means the account does not include the requested feature (WebSocket access or prediction markets). A 429 means a rate or monthly limit has been reached. For live streams, resync_required means your app should fetch the latest state through REST before continuing.

Errors use application/problem+json (RFC 9457) with a stable machine readable code, an HTTP status, and the request_id. Branch on code, not on human-readable text.

{
  "type": "urn:pickleball-api:error:unauthorized",
  "title": "Missing or invalid API key",
  "status": 401,
  "code": "unauthorized",
  "request_id": "8f9dcae7-…"
}
  • 400 invalid_request — malformed parameters or body.
  • 401 unauthorized — missing or invalid API key.
  • 403 websocket_not_entitled — WebSocket access not on your plan.
  • 403 markets_not_entitled — prediction markets not on your plan.
  • 404 not_found — no such resource.
  • 429 quota_exceeded — rate or monthly volume cap reached.
  • 500 internal — unexpected server failure; safe to retry a GET within a bounded retry budget.

Retry policy

Do not retry 400, 401, 403, or 404 without changing the request. Retry 429 only after the number of seconds in Retry-After. For timeouts, network failures, 500, 502, 503, and 504, use exponential backoff with full jitter (for example 1s, 2s, 4s, capped at 30s) and stop after a small request or time budget. Retry-After and RateLimit-Reset are integer seconds, not HTTP dates. Authentication, rate-limited, 304, and 5xx responses do not consume the monthly REST allowance; other completed authenticated 2xx–4xx requests do.

The API returns X-Request-Id on routed REST responses as well as the matching request_id in problem bodies. Preserve the header when a proxy or body-decoding failure prevents you from reading the JSON.

Pagination

Catalogue list endpoints use opaque cursors. Pass limit and follow meta.pagination.next_cursor until has_more is false. Do not construct or modify these cursors, and do not reuse one with a different filter set.

curl -s "https://pickleball-api.com/v1/fixtures?limit=100&cursor=CURSOR" \
  -H "Authorization: Bearer pbl_live_YOUR_KEY"

Two history APIs use typed cursors: match observations and domain events use the last seen numeric sequence; standings history uses the returned RFC 3339 capture timestamp. Their generated OpenAPI parameter schemas record this distinction.

Caching & conditional requests

List responses set Cache-Control: private, max-age=5 so you can safely coalesce bursts of reads.

Do not connect each website visitor directly to the upstream API. Make requests from your server, cache the latest state and share that cached state with your own users.

Rate limits & usage

Each plan has both a short-term request-rate limit and a monthly allowance. Basic includes 25,000 REST requests per month at up to 60 requests per minute. Pro includes 100,000 REST requests per month at up to 120 requests per minute, plus a separate allowance for outbound WebSocket messages and access to prediction-market endpoints.

For a typical MLP event, polling in-progress fixtures every 60 seconds and each actively-followed match's live state every 10–15 seconds fits comfortably inside Basic's monthly allowance. Pro customers should use WebSockets for genuinely live updates instead.

The list and live endpoints have very different recommended cadences — this is the single most common quota mistake. GET /v1/fixtures?status=in_progress is a full snapshot of every in-progress fixture; poll it at most once every 60 seconds, and only for as long as an event is actually running. Reserve 10–15 second polling for GET /v1/fixtures/{id}/live, and only for fixtures a viewer is actively watching.

Limits are enforced server-side and shown in response headers: RateLimit-Limit / RateLimit-Remaining / RateLimit-Reset (per-minute rate limit, seconds until it resets) and X-Usage-Limit / X-Usage-Remaining (monthly REST allowance). When a rate or monthly cap is reached, the API returns 429 quota_exceeded with a Retry-After header telling you how many seconds to wait. Self-serve plans have hard caps, so there are no automatic overage charges. Check current consumption at any time with GET /v1/usage.

GET /v1/usage

{
  "data": {
    "plan": "pro",
    "period_start": "2026-07-01T00:00:00Z",
    "period_end": "2026-08-01T00:00:00Z",
    "rest_calls_used": 12840,
    "rest_calls_limit": 100000,
    "outbound_messages_used": 0,
    "outbound_messages_limit": 100000,
    "sockets_active": 0,
    "sockets_limit": 3,
    "markets_entitled": true
  }
}

REST endpoints

All endpoints require authentication and are prefixed with the base URL.

Catalogue

GET/v1/competitionsList competitions.
GET/v1/competitions/{id}Competition detail.
GET/v1/seasonsList seasons. Filter: competition_id.
GET/v1/eventsList events. Filters: season_id, active_only, cursor, limit.
GET/v1/events/{id}Event detail.
GET/v1/events/{id}/teamsTeams competing at an event (discovery hop for rosters).
GET/v1/venues/{id}Venue detail.

Teams & players

GET/v1/teamsList teams.
GET/v1/teams/{id}Team detail.
GET/v1/teams/{id}/rosterTeam roster. Requires ?event_id=.
GET/v1/teams/{id}/fixturesA team's fixtures.
GET/v1/teams/{id}/matchesA team's lineup matches.
GET/v1/playersList players.
GET/v1/players/{id}Player detail.

Player DUPR fields. Every player returned directly, in a team roster, or in match participants includes dupr_id, dupr_rating_singles, and dupr_rating_doubles. These nullable values are MLP-published snapshots, not live reads from DUPR. A null value means MLP has not published it for that player; it never means a zero rating.

{
  "id": "…",
  "display_name": "Abbigal Hatton",
  "country_code": "USA",
  "dupr_id": "3OJ24P",
  "dupr_rating_singles": 4.38532,
  "dupr_rating_doubles": 5.39732
}

Fixtures, matches & games

GET/v1/fixturesList fixtures. Filters: event_id, status, cursor, limit.
GET/v1/fixtures/{id}Fixture detail, with sides.
GET/v1/fixtures/{id}/matchesLineup matches within the fixture.
GET/v1/fixtures/{id}/liveLatest observation for every match in the fixture.
GET/v1/matches/{id}Match detail (includes participants).
GET/v1/matches/{id}/participantsMatch lineup participants.
GET/v1/matches/{id}/gamesPer-game scores.
GET/v1/matches/{id}/observationsObservation time series. Cursor = sequence.
GET/v1/matches/{id}/eventsDomain-event log for the match (lifecycle transitions etc.).

Standings

GET/v1/events/{id}/standings/latestLatest standings for an event.
GET/v1/events/{id}/standings/historyStandings snapshot history.
GET/v1/seasons/{id}/standings/latestLatest standings for a season.
GET/v1/seasons/{id}/standings/historyStandings snapshot history.

Prediction markets (Pro plan / markets entitlement)

GET/v1/fixtures/{id}/marketsVerified prediction markets for a fixture.
GET/v1/fixtures/{id}/market-contextFixture + markets with independent freshness clocks.

Account & realtime

GET/v1/usageCurrent-period usage vs. limits.
POST/v1/realtime/ticketsMint a short-lived WebSocket ticket (Pro / trial).

status on /v1/fixtures accepts scheduled, in_progress, completed, or cancelled. How far back completed fixtures/matches are visible depends on your plan — each account has a historyDays entitlement (see your plan on the pricing page); requesting further back than that returns an empty page rather than an error, so paginate with has_more rather than assuming a fixed depth. In-progress and scheduled fixtures are never subject to this cutoff.

Fixtures, matches & observations

MLP plays a team fixture (sometimes called a "tie"): two teams meet across several individual lineup matches (women's, men's, and mixed doubles, then a singles DreamBreaker if required). We model that structure directly: a fixture has two sides (each with a team and a running matches_won count) and the individual matches belonging to it carry the fixture's fixture_id, lineup_slot/lineup_sequence (play order within the fixture) and category (mens_doubles, womens_doubles, mixed_doubles, or singles).

cURL

curl -s "https://pickleball-api.com/v1/fixtures/FIXTURE_ID" \
  -H "Authorization: Bearer pbl_live_YOUR_KEY"

JavaScript

const response = await fetch(
  'https://pickleball-api.com/v1/fixtures/FIXTURE_ID',
  {
    headers: {
      Authorization: 'Bearer pbl_live_YOUR_KEY',
    },
  },
);
const body = await response.json();
console.log(body.data);

Python

import requests

response = requests.get(
    'https://pickleball-api.com/v1/fixtures/FIXTURE_ID',
    headers={'Authorization': 'Bearer pbl_live_YOUR_KEY'},
)
response.raise_for_status()
print(response.json()['data'])
{
  "data": {
    "id": "0190…0030",
    "event_id": "0190…0004",
    "status": "in_progress",
    "scheduled_at": "2026-07-14T18:00:00Z",
    "court": "Championship Court",
    "stage": "group_play",
    "is_provisional": false,
    "sides": [
      { "side": 0, "team": { "id": "…", "name": "Team A" }, "label": "Team A", "matches_won": 1 },
      { "side": 1, "team": { "id": "…", "name": "Team B" }, "label": "Team B", "matches_won": 0 }
    ]
  }
}

is_provisional is true while live scoring has created this fixture ahead of our catalogue crawl resolving its event and sides — treat event_id/sides as provisional until it flips to false (also reflected as meta.completeness: "partial" on the fixture response).

Live state: observations

A match's live scoreboard is a series of observations — score ticks with a monotonically increasing sequence, rather than a single mutable "current state" row. GET /v1/fixtures/{id}/live returns each match in the fixture paired with its latest observation; poll this for a live scoreboard. GET /v1/matches/{id}/observations returns the full retained time series (cursor = sequence) — a real, queryable history of how the match unfolded.

cURL

curl -s "https://pickleball-api.com/v1/fixtures/FIXTURE_ID/live" \
  -H "Authorization: Bearer pbl_live_YOUR_KEY"

JavaScript

const response = await fetch(
  'https://pickleball-api.com/v1/fixtures/FIXTURE_ID/live',
  {
    headers: {
      Authorization: 'Bearer pbl_live_YOUR_KEY',
    },
  },
);
const body = await response.json();
console.log(body.data);

Python

import requests

response = requests.get(
    'https://pickleball-api.com/v1/fixtures/FIXTURE_ID/live',
    headers={'Authorization': 'Bearer pbl_live_YOUR_KEY'},
)
response.raise_for_status()
print(response.json()['data'])
{
  "data": {
    "sequence": 42,
    "observed_at": "2026-07-14T18:41:03Z",
    "lifecycle": "in_progress",
    "current_game": 1,
    "serving_side": 1,
    "server_number": 2,
    "side_0_points": 4,
    "side_1_points": 6,
    "side_0_games_won": 0,
    "side_1_games_won": 0,
    "winner_side": null
  }
}

WebSocket streaming

The live feed is included with Pro and the free trial. Pro allows up to 3 concurrent connections, 25 subscribed topics per connection and 100,000 outbound messages each month. Connect to wss://pickleball-api.com/v1/live and choose what you want to follow. The service first sends a full current snapshot, then sends smaller update messages whenever something changes.

1. Authenticate

A server-side WebSocket can send your API key as a Bearer token on the upgrade request. For a browser, where you can't set that header, mint a single-use ticket on your server and pass only the ticket to the browser as a query parameter. Tickets expire in 60 seconds and are consumed on use.

cURL

# Mint a ticket
curl -s -X POST "https://pickleball-api.com/v1/realtime/tickets" \
  -H "Authorization: Bearer pbl_live_YOUR_KEY"
# -> { "data": { "ticket": "…", "expires_in_seconds": 60,
#                "stream_url": "wss://pickleball-api.com/v1/live" } }

# Connect
wss://pickleball-api.com/v1/live?ticket=TICKET

JavaScript

// Server: mint a single-use ticket (never expose the API key to the browser)
const ticketResponse = await fetch(
  'https://pickleball-api.com/v1/realtime/tickets',
  {
    method: 'POST',
    headers: {
      Authorization: 'Bearer pbl_live_YOUR_KEY',
    },
  },
);
const { data } = await ticketResponse.json();

// Browser or server: connect with the ticket
const socket = new WebSocket(`${data.stream_url}?ticket=${data.ticket}`);
socket.addEventListener('open', () => {
  socket.send(JSON.stringify({ type: 'subscribe', topics: ['fixtures.live'] }));
});

Python

import json
import requests
import websocket

ticket_response = requests.post(
    'https://pickleball-api.com/v1/realtime/tickets',
    headers={'Authorization': 'Bearer pbl_live_YOUR_KEY'},
)
ticket_response.raise_for_status()
ticket = ticket_response.json()['data']

socket = websocket.create_connection(
    f"{ticket['stream_url']}?ticket={ticket['ticket']}"
)
socket.send(json.dumps({'type': 'subscribe', 'topics': ['fixtures.live']}))
print(socket.recv())

2. Subscribe

On connect the server sends connection.ready. Send a subscribe message with one or more topics; the server replies subscription.ack and then sends a snapshot per topic.

// client -> server
{ "type": "subscribe", "topics": ["fixtures.live"] }

// topics
fixtures.live            // one snapshot per in-progress match
fixture.<fixture_id>     // one fixture's lifecycle/sides
match.<match_id>         // one match's latest observation
event.<event_id>         // one event's metadata
markets.<fixture_id>     // verified market prices (Pro / markets entitlement only)

3. Receive updates

  • snapshot — full current state for a topic, sent right after you subscribe.
  • match.observation — a new observation for a subscribed match.
  • market.price_changed / market.book_changed — a prediction-market price or order-book update.
  • resync_required — discard local assumptions, refetch the named REST snapshot, then subscribe again.
  • ping / rate_limit / error — control messages.

4. Ordering, duplicates & recovery

Delivery on a connected socket is at-most-once: the service does not wait for client acknowledgements and does not currently replay missed messages. Messages are queued in send order per connection, but there is no global ordering across topics or connections. A data update carries a stable event_id for deduplication and, where the source provides it, a monotonic sequence scoped to that topic's upstream channel. Treat unknown message types and added object fields as forward-compatible additions.

A snapshot can overlap a newly arriving update. For match.*, retain the observation with the highest data.observation.sequence (snapshot) or data.sequence (update), and ignore duplicate event_ids. For other topics, apply messages in connection order and use the object's source timestamps when present. Sending unsubscribe returns a subscription.ack whose topic list is the complete set still active.

TopicAuthoritative REST recovery
fixtures.liveGET /v1/fixtures?status=in_progress, then each active /v1/fixtures/{id}/live
fixture.<id>GET /v1/fixtures/{id}
match.<id>GET /v1/matches/{id}/observations using the last sequence, or start again without a cursor
event.<id>GET /v1/events/{id}
markets.<fixture_id>GET /v1/fixtures/{id}/market-context

On resync_required, a sequence gap, or any disconnect, fetch the authoritative REST resource, open a new connection, and subscribe again. Do not send a prior cursor expecting replay: connection.ready.data.replay_minutes is currently 0. Reconnect network failures and codes 1012 or 1013 with full-jitter exponential backoff, capped at 30 seconds, and a finite retry budget. A successful reconnect does not preserve old subscriptions.

5. Heartbeats & close codes

The server sends ping roughly every 25s; reply with { "type": "pong", "at": "…" }. Miss the pong window (~60s) and the connection closes. Application close codes:

4401  unauthorized        (bad/absent credentials; ticket_invalid_or_expired close reason for a bad, expired, or consumed ticket)
4403  forbidden           (plan lacks WebSocket access)
4408  quota_exceeded      (socket or topic limit hit)
1012  service_restart     (deploy — reconnect with backoff)
1013  try_again_later     (transient; reconnect with backoff)

Prediction markets

Pro plans (and the trial) get verified prediction-market context alongside the sports data: current price, best bid/ask and spread for each outcome of a market linked to a fixture. Only markets our provider has verified as matching a real fixture are ever returned — a candidate match our provider is still reviewing is never surfaced through this API.

curl -s "https://pickleball-api.com/v1/fixtures/FIXTURE_ID/markets" \
  -H "Authorization: Bearer pbl_live_YOUR_KEY"

{
  "data": [
    {
      "id": "0190…0050",
      "question": "Will Team A win this fixture?",
      "status": "active",
      "resolved_outcome_id": null,
      "outcomes": [
        { "id": "…", "name": "Team A", "latest_price": 0.54, "best_bid": 0.53, "best_ask": 0.55, "spread": 0.02 },
        { "id": "…", "name": "Team B", "latest_price": 0.46, "best_bid": 0.45, "best_ask": 0.47, "spread": 0.02 }
      ]
    }
  ]
}

GET /v1/fixtures/{id}/market-context returns the fixture and its markets together with two independent freshness clocks — sports_freshness_seconds for the score data and market_freshness_seconds for the stalest returned outcome price — since sports and markets update on different cadences. Each outcome also has independent price_observed_at and book_observed_at source timestamps. Prices are decimal probabilities in the range supplied by the source; they are not a promise that a trade is executable at that value. On WebSocket, subscribe to markets.<fixture_id> for live price/book-depth updates.

Market data is sourced and verified for informational and analytical use. It is not an official settlement source and may occasionally be delayed, incomplete or unavailable — always check the market status, per-outcome timestamps, nullable bid/ask fields, and market_freshness_seconds before acting on a price.closed and resolved markets may retain a last observed price; do not present that value as current or executable.

Machine-readable specs

The downloadable specification describes the same fields used by the live service. It includes OpenAPI for REST, AsyncAPI for WebSockets and JSON Schema for the main data objects. You can use these files to explore the API in development tools or generate a client in your preferred language.

Download the current versioned artifacts: OpenAPI v1, AsyncAPI v1, fixture schema, and WebSocket server-message schema. These URLs are public and unauthenticated. The v1 filenames are stable; breaking contracts will be published under a new version rather than replacing them.

Compatibility policy

Within /v1, adding optional object fields, adding endpoints, and adding WebSocket message types are compatible changes. Clients must ignore unknown fields and message types. Removing or renaming a field, making an optional field required, changing nullability or a field type, changing sequence scope or deterministic sort order, or removing an enum value is breaking and requires a new API version.

Enum additions can still surprise clients even when the surrounding schema is unchanged, so exhaustive decoders should include an unknown fallback. Planned endpoint retirement will be announced in the v1 changelog and returned with Deprecation and Sunset headers for at least 90 days before removal. Security fixes may require a shorter window; if so, the changelog will state why.

Support

Check the status page for uptime and incidents, or contact support. Include your request_id and we'll trace it.