Partner API
Approved clubs, leagues and venues can publish open-play schedules and closures, run club leagues and run round robins on Vara Performance from their own systems. The API is REST over HTTPS with JSON bodies, described by an OpenAPI document per version.
Open the v1 reference Download OpenAPI
| Base URL | https://api.varaperformance.com/v1 |
|---|---|
| Format | JSON, UTF-8. Times are local to the venue's city (HH:MM); dates are YYYY-MM-DD. |
| Spec | /api/openapi/versions.json lists versions; /api/openapi/{version}.json is each document. |
Getting access
- Apply as a partner (no account needed to start) with proof you run the club, league or venue (a venue contact, admin of the group, a DUPR club ID).
- Our team reviews the application and grants your organization the venues it manages.
- Your organization's owners and admins create API keys and webhook endpoints from the partner dashboard.
New partners' sessions are held for a quick review before they go live. After a handful of clean posts your organization is promoted to trusted and publishes immediately. Closures are always published immediately.
Authentication
Send your key as a bearer token. Keys are shown once at creation; we store only a hash. Live keys (vp_live_…) work against production, test keys (vp_test_…) only against the sandbox.
Authorization: Bearer vp_live_3fA9…
Missing, revoked, expired or wrong-environment keys get 401. Suspending an organization revokes all of its keys at once. Rotate a key by creating a new one, deploying it, then revoking the old one.
Scopes
| Scope | Allows |
|---|---|
venues:read | List the venues granted to your organization (with booking links), their sessions, community-posted ones included, and review summaries. |
venues:write | Update a granted venue's courts, lights, amenities, phone, website, cost / hours / access notes and booking link and notes. |
sessions:read | Read attendance for sessions at your granted venues: going / maybe / waitlisted counts, capacity and spots left per date. |
sessions:write | Create and archive recurring sessions at your granted venues. |
closures:write | Cancel a session or close a venue for a day (published immediately). |
announcements:write | Post announcements (coming soon). |
leagues:read | List your club leagues, their teams, schedule and standings. |
leagues:write | Create and update club leagues, enter teams, generate the schedule and post fixtures. |
league:results:write | Report club league results. TRUSTED partners' results count at once; NEW partners' after the opponent captain confirms or 48 hours pass undisputed. |
registrations:read | Read league and event registrations (coming soon). |
webhooks:manage | Manage webhook endpoints through the API (coming soon; use the dashboard today). |
groups:read | List your organization's groups, their members (public profiles named, others counted), updates boards, weekly games and each date's In / Out / Maybe / waitlist counts, note and host. |
groups:write | Create and update groups, invite players by handle, post and manage updates, add, change and stop weekly group games, set a date's note or host, and cancel a date (everyone going is told). |
roundrobins:read | List your organization's round robins, their rounds, courts, scores and standings (JSON or CSV), and the format catalog. |
roundrobins:write | Run round robins at your granted venues: create one, add late arrivals or take out leavers, advance rounds and override scores. |
events:read | List your organization's events. |
events:write | Post, update and remove events at your granted venues or other places; they show on /events and the court page. |
ladders:read | List your organization's ladders, their live standings, matches and seasons (with each finished season's frozen final standings). |
ladders:write | Create ladders at your granted venues, change their rules and season settings (audited), and end the running season early. |
queues:read | List your organization's drop-in queues and read one live: courts and the games on them, the line in order, and every player's state. |
queues:write | Start a live drop-in queue at a granted venue or for one of your open-play sessions, and end it. |
coaches:read | List your organization's staff coaches in the lessons directory, with their status. |
coaches:write | List, update and remove staff coaches (shown as Verified), set their photos, and link them to clinics with `coachId` on events. |
invites:write | Send email invitations to your groups (up to 500 a day across your organization, 200 per group; each address once per group, never again after it opts out). |
Writes are also limited to the venues granted to your organization: anything else answers 404, as if it didn't exist.
Quickstart
Set VP_API_KEY to your key, then pick your language. Your choice is remembered across the page.
1. List the venues you manage
curl https://api.varaperformance.com/v1/venues \
-H "Authorization: Bearer $VP_API_KEY"2. Post a weekly open-play session
curl -X POST https://api.varaperformance.com/v1/sessions \
-H "Authorization: Bearer $VP_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{"venueId":"<venue id>","title":"Tuesday Night Open Play","dayOfWeek":2,"startTime":"18:00","endTime":"20:00"}'3. Rained out? Cancel one day
curl -X POST https://api.varaperformance.com/v1/closures \
-H "Authorization: Bearer $VP_API_KEY" \
-H "Content-Type: application/json" \
-d '{"venueId":"<venue id>","date":"2026-11-03","reason":"Courts wet"}'Round robins
Run a round robin at one of your venues from your own front desk or club software: 11 formats (see GET /round-robin-formats for each one's description and recommended player count), players by Vara Performance handle or as guests, optional timed rounds, and players entering their own scores from their phones. Each player gets a "go to court" notification when a round starts, and everyone follows the courts and standings at the session's share link (url), including a big-screen view for the venue TV.
POST /v1/round-robins # create (setup): venue, date + startTime, kind, courts, players
POST /v1/round-robins/{id}/players # late arrivals; DELETE …/players/{playerId} for leavers
POST /v1/round-robins/{id}/advance # generate + round 1, then next round, then finalize
PUT /v1/matches/{id}/score # your override: {"a": 11, "b": 7}
GET /v1/round-robins/{id}/standings # JSON; …/standings.csv for a spreadsheetWebhooks: round_robin.round_started (courts and teams for the round), round_robin.score_reported (a confirmed score: by the other side, the auto-confirm clock or you), and round_robin.completed (final standings). Played rounds never change: adding or removing a player rebuilds only the rounds still to come.
Drop-in queues
Run open play live (the "paddle queue"): start a queue at one of your venues, or for one of your open-play sessions so its listing shows "Open play is live: join the queue". Players join from the link, QR or code and wait in line; each court that frees takes the next group by fair rotation (longest waiting first, fewest repeat partners and opponents, optional skill bands by rating) and its players are told their court. The stack rule decides who stays on: all_off (everyone rotates), winners_stay or winners_split, up to maxStay games in a row. The host (your organization's owner account on the website) can pause, add courts, move people in line and assign courts by hand; the big screen is at screenUrl. A queue with no activity for 45 minutes ends on its own.
POST /v1/queues # start: {"venueId": "…", "courts": 4, "stackRule": "winners_stay", "bandCuts": [3.5]}
GET /v1/queues/{id} # courts on now, the line in order, players and their state
POST /v1/queues/{id}/end # end it (harmless to repeat)
GET /v1/queues # yours, newest firstWebhooks: queue.started, queue.assignment (a game went on court: court, the two teams by player id) and queue.ended (why, games played, players). Rated queues count confirmed scores for players who accept, as with round robins.
Venues & attendance
Keep your venues' pages right from your own systems: court counts, lights, amenities, phone, website, notes and where players book a court. Only the fields you send change; name, slug and location never change through the API.
PATCH /v1/venues/{venueId} # venues:write
{ "reservationUrl": "https://secure.rec1.com/TN/clarksville-tn/catalog",
"reservationNotes": "$6/hour recreational, $8/hour tournament.",
"lights": true, "outdoorCourts": 8 }
GET /v1/venues/{venueId}/sessions # yours, other partners', official and community-posted
GET /v1/sessions/{id}/occurrences?from=2026-11-01&to=2026-11-30
GET /v1/venues/{venueId}/reviews/summary # average, count, stars, top tagsCommunity-posted sessions at your venues show with source: "community" and the organizer only as "Community organizer". They're read-only for you (editable: false), but a closure still cancels a date. Attendance is counts only: going, maybe, waitlisted, capacity and spots left, never who. Webhooks: session.rsvp_changed on every change and session.full once when a date fills (again only after a spot opens).
The daily duplicate cleanup sometimes merges two records of one place. Calls with the old venue id then answer 404 { "code": "VENUE_MERGED", "mergedInto": "…" }; your grants move to the surviving venue and venue.merged ({ from, into }) tells you which id to use.
Events
Post clinics, socials and tournaments at one of your venues (venueId) or anywhere else (place: name, address, state and city). Times take an offset or a local YYYY-MM-DDTHH:MM read in the venue's zone. Events show on /events, the city page and the court page, attributed to your organization. TRUSTED partners publish at once; a NEW partner's events wait for a quick review, like sessions.
POST /v1/events # { title, kind, startsAt, endsAt?, venueId | place, description?, url?, price? }
GET /v1/events # your organization's events
PATCH /v1/events/{id} # change details, or { "cancelled": true }
DELETE /v1/events/{id} # take it downLadders & seasons
Run a challenge ladder at one of your venues. Each ladder runs in seasons (12 weeks unless you set seasonWeeks, drawn in the venue's timezone so a season always ends at local midnight). When a season ends, on its date or early when you say so, the final standings are frozen and archived, the top 3 earn a profile badge, open challenges are cancelled (both sides are told), and with autoAdvance the next season starts at once. Players roll over unless they chose to sit out or played no matches all season; the starting order either carries over the final standings with the gaps closed (seedMode: "carry") or is reseeded by rating ("rating": Vara Performance Rating, else linked DUPR, else self-rated). Ratings themselves carry across seasons; only ladder positions reset.
POST /v1/ladders # create at a granted venue: name, format, venueId, rules, seasonWeeks, seedMode
PATCH /v1/ladders/{id} # rules and season settings (audited); status "closed" ends the season
GET /v1/ladders/{id}/standings # live standings for the running season
GET /v1/ladders/{id}/seasons # every season; …/seasons/{n} adds the frozen final standings
POST /v1/ladders/{id}/seasons/end # end the running season now
GET /v1/ladders/{id}/matches?season=2Webhooks: ladder.season_started, ladder.season_ended (final standings and the next season), and ladder.match_confirmed (a confirmed score, by the other side or the auto-confirm clock, or a forfeit). Players appear by handle only when their profile is public and shown on leaderboards.
Dates, times & time zones
Pickleball happens at a place, so times are the place's. Every venue, session, closure, event and league fixture carries the IANA timezone it's expressed in: the venue's state's zone, or the minority zone for venues past a zone line (Knoxville, TN is America/New_York; Nashville is America/Chicago; El Paso, TX is America/Denver).
- Local fields are wall clock in that zone.
dateisYYYY-MM-DD;startTimeandendTimeareHH:MM. They're unchanged and stay in every response. - Instants come alongside.
startsAt/endsAt(andnextStartsAton weekly sessions) are ISO 8601 with the zone's offset at that moment, DST included, so you never compute it yourself. Stored timestamps (scheduledAt,createdAt,publishedAt) are UTC (Z). - Weekly sessions don't shift with DST. “Tuesdays 18:30” is 18:30 at the venue all year; the UTC instant moves. A weekly time in a spring-forward gap (02:30 on the second Sunday of March) runs an hour later that day; in the fall-back overlap it's the first (daylight) one. Arizona and Hawaii don't observe DST.
- Closures take the venue-local
date; the response says which instants that day covers (midnight to midnight, 23 or 25 hours on DST days). - Fixture times (
scheduledAt) accept an instant with an offset orZ(exact), or a localYYYY-MM-DDTHH:MMread in the fixture venue's zone (else the league's). A local time that doesn't exist (spring forward) or happens twice (fall back) gets400with both candidate offsets: send one of them. - Webhook payloads follow the same rules:
session.published,closure.createdandleague.result_reportedcarrytimezoneand offset instants.
GET /v1/venues/{venueId}/sessions
{ "data": [{
"id": "…", "dayOfWeek": 3, "startTime": "18:30", "endTime": "20:30",
"timezone": "America/Chicago",
"nextStartsAt": "2026-11-04T18:30:00-06:00", // CST: after the Nov 1 fall back
"nextEndsAt": "2026-11-04T20:30:00-06:00", …
}] }
POST /v1/closures { "venueId": "…", "date": "2027-03-14", "reason": "Gym floor refinish" }
201 { "id": "…", "kind": "venue_closed", "date": "2027-03-14", "timezone": "America/Chicago",
"startsAt": "2027-03-14T00:00:00-06:00", "endsAt": "2027-03-15T00:00:00-05:00" } // a 23-hour day
PATCH /v1/fixtures/{fixtureId} { "scheduledAt": "2026-11-01T01:30" }
400 { "code": "INVALID_INPUT", "message": "2026-11-01 01:30 happens twice in America/Chicago (clocks fall back): send it with an offset, 2026-11-01T01:30:00-05:00 or 2026-11-01T01:30:00-06:00." }Idempotency
Send an Idempotency-Key header (≤ 128 characters, unique per operation) on every POST and DELETE. Retrying with the same key returns the original response with Idempotent-Replayed: true for 7 days. A retry while the first request is still running gets 409; reusing a key for a different request gets 422. Server errors (5xx) and 429 release the key so you can retry.
Rate limits
Per key: 600 reads and 60 writes per minute. Over the limit you get 429; back off and retry with the same idempotency key.
Errors
Errors are JSON with a stable machine-readable code:
{ "code": "FORBIDDEN", "status": 403, "message": "This key lacks the closures:write scope." }| 400 | BAD_REQUEST / INVALID_INPUT | The body failed validation; `data.issues` lists each problem. |
| 401 | UNAUTHORIZED | Missing, invalid, revoked or expired key. |
| 403 | FORBIDDEN | The key lacks a scope, or your organization isn't approved. |
| 404 | NOT_FOUND | No such resource, or not granted to your organization. |
| 404 | VENUE_MERGED | That venue was merged into another: use `mergedInto` from the body. |
| 409 | CONFLICT | An idempotent request with this key is still in progress. |
| 422 | – | Idempotency-Key reused for a different request. |
| 429 | TOO_MANY_REQUESTS | Rate limit reached. |
Webhooks
We POST JSON to your HTTPS endpoint when something happens. Deliveries follow Standard Webhooks, so their verification libraries work as-is. Events: session.published, session.rejected, closure.created, partner.trust_changed, league.result_reported, league.standings_updated, group.member_joined, group.member_left, group_game.invites_sent, group_game.rsvp_changed, group_game.full, group_game.cancelled, group.update_posted, group_game.note_changed, group_game.host_changed, round_robin.round_started, round_robin.score_reported, round_robin.completed, session.rsvp_changed, session.full, venue.merged, ladder.season_started, ladder.season_ended, ladder.match_confirmed, queue.started, queue.assignment, queue.ended.
POST /your/endpoint
webhook-id: msg_2f1c… # stable across retries: dedupe on it
webhook-timestamp: 1793000000 # unix seconds
webhook-signature: v1,K5oZ…= # base64 HMAC-SHA256 of "{id}.{timestamp}.{body}"
{"type":"closure.created","timestamp":"2026-11-03T13:02:11Z","data":{…}}- Verify against the raw body, compare in constant time, and reject timestamps more than 5 minutes off.
- Respond 2xx within 10 seconds. We retry failures with backoff for about 17 hours, then mark the delivery failed.
- When you rotate a secret, deliveries carry two signatures (new and old) for 24 hours.
- Endpoints must be public HTTPS URLs; redirects are not followed.
- Prefer not to hold a shared secret? Ask for Ed25519 signing (
v1a,signatures, verified with a public key we publish for your endpoint).
import { createHmac, timingSafeEqual } from "node:crypto";
// secret: your endpoint's "whsec_…" value; body: the RAW request body string.
export function verify(secret, headers, body) {
const id = headers["webhook-id"];
const ts = Number(headers["webhook-timestamp"]);
if (Math.abs(Date.now() / 1000 - ts) > 300) return false; // replay window
const key = Buffer.from(secret.replace(/^whsec_/, ""), "base64");
const expected = createHmac("sha256", key).update(`${id}.${ts}.${body}`).digest();
// Two signatures are sent during a secret rotation.
return headers["webhook-signature"].split(" ").some((part) => {
const [version, sig] = part.split(",");
const got = Buffer.from(sig ?? "", "base64");
return version === "v1" && got.length === expected.length && timingSafeEqual(got, expected);
});
}Versioning
The version is in the path (/v1). Within a version we only make additive changes: new endpoints, new optional fields, new enum values you should tolerate. Breaking changes ship as a new version, documented side by side in the reference. A deprecated version answers with Deprecation and Sunset headers for at least 6 months before it stops.
| Version | Status | Released | Sunset |
|---|---|---|---|
| v1 | stable | 2026-10-28 | Not set |
Changelog
2026-11-04 · v1
- Additive: venue `access` can be `unknown`: map data that doesn't say who may play (shown as "Access not confirmed: check before you go", never free). Map-only venues that used to default to `public` are now `unknown` unless their tags or name say otherwise: country clubs, golf, resorts, HOAs and apartments read `membership`; schools, colleges, churches and YMCAs read `restricted`; parks, rec and community centers read `public`. Treat unknown values you don't recognize as not confirmed.
- New: `PATCH /venues/{venueId}` accepts `access` (`public`, `membership`, `pay_per_visit`, `restricted` or `unknown`). Your value counts as confirmed: imports and our access rules never change it.
2026-11-02 · v1
- Additive: venues carry `restroomType`: `building` (restrooms in a building), `portable` (portable toilets) or null (not known, or `restrooms` isn't true). `restrooms` itself is unchanged.
- New: `PATCH /venues/{venueId}` accepts `restroomType`. Setting a type also sets `restrooms` to true; null clears the type and leaves `restrooms` alone; `restrooms` false or null clears the type. A type sent with `restrooms` false or null is refused (400).
2026-11-01 · v1
- New: website widgets. `GET /embeds` (scope `venues:read` for your granted venues, `groups:read` for your public groups; either one is enough) returns ready-made code for each: `embedUrl`, an auto-sizing `script` snippet, a plain `iframe` snippet, a `badge` (light and dark) and the `pageUrl` it links back to, plus `loaderUrl` and the badge images. Optional `theme` (`light`, `dark`), `limit` (rows, 1 to 20) and `accent` (hex colour) apply to every URL and snippet. Venue widgets list the next 7 days (local times, level, spots left, an RSVP link per session); group widgets list upcoming games. Private groups are never embeddable and are left out. The widgets themselves are public pages (no key, no cookies, no tracking).
2026-11-01 · v1
- New: group logos (scope `groups:write`). `PUT /groups/{id}/logo` takes a multipart `file` (JPEG, PNG or WebP, up to 8 MB; 30 uploads per organization per hour), scanned and re-encoded to a square 512 px WebP with metadata stripped; `DELETE /groups/{id}/logo` goes back to the initials mark. Both answer with the group.
- Additive: groups carry `logoUrl` (null when there's none). A private group's URL includes its invite code, so it stops working when the invite link is replaced; fetch the group again for the new one.
- New: a group's updates board (scopes `groups:read` / `groups:write`). `GET /groups/{id}/updates` (newest first, `cursor`/`limit` paging, the pinned update apart in `pinned`, updates hidden by members' reports flagged `hidden`), `POST /groups/{id}/updates` (plain text up to 1,000 characters, posted under your organization's name, optional `pinned`; every active member is notified per their own settings), `PATCH` and `DELETE /groups/{id}/updates/{updateId}` for any update on your group's board. 30 updates per group per 24 hours.
- New: one date's note. `PUT /games/{id}/occurrences/{date}/note` sets a one-line note (up to 200 characters; null clears it), shown on the date on the group page, in its invitation, on players' dashboards and in calendar feeds. A new note notifies everyone In or Maybe for that date, once per distinct note.
- New: a host for one date. `PUT /games/{id}/occurrences/{date}/host` with a member's `handle` or `playerId` (public profiles only); `DELETE` clears it. For that date only, the host can set its note, see its roster, check players in, move people off the waitlist, start its round robin and cancel it (with a reason); their role ends after the date. The host is notified.
- Additive: `GET /games/{id}/occurrences` rows carry `note` and `host` (`handle` only for a public profile).
- New webhooks: `group.update_posted`, `group_game.note_changed` and `group_game.host_changed` (`playerId`, null when cleared).
2026-10-31 · v1
- Additive: venues carry `possiblyClosed` (`since`, `source`, `sourceName`), set when the open-data source that first listed a venue (Overture Maps, a city's parks data) no longer lists it, else null. It clears when the source lists the venue again; after two complete runs without it and 60 days with no activity at the venue, the venue's `status` becomes `closed`. Verified, partner-held or staff-edited venues are never marked or closed this way.
- Additive: `dataSources[].license` can be `No explicit license; <terms>` for city datasets published under only their terms of use or a disclaimer (Seattle, Montgomery Parks MD, Phoenix, NYC Parks, Mesa); `licenseUrl` then links those terms. Show the `attribution` as for any other source.
- New: drop-in queues for open play (scopes `queues:read` / `queues:write`). `POST /queues` starts a live queue at a granted venue or for one of your open-play sessions (`sessionId`; one live queue per session date), with `courts`, `format`, `stackRule` (`all_off`, `winners_stay`, `winners_split`), `maxStay`, skill `bandCuts` and optional `gameMinutes`. `GET /queues`, `GET /queues/{id}` (courts on now, the line in order, players' states) and `POST /queues/{id}/end`. Queues end on their own after 45 minutes without activity.
- New webhooks: `queue.started`, `queue.assignment` (a game went on court) and `queue.ended`. Queues never appear under `/round-robins`, and their scores don't fire `round_robin.score_reported`.
- New: coaches in the lessons directory (scopes `coaches:read` / `coaches:write`). `GET`/`POST /coaches`, `GET`/`PATCH`/`DELETE /coaches/{id}`, and `PUT /coaches/{id}/photo` (multipart `file`, up to 8 MB). Your staff coaches publish at once, shown as Verified, on varaperformance.com/lessons, the lessons city pages for their area, the court pages where they teach (`venueIds`) and their own page (`pageUrl`). Listing only: no booking or payments. A key only ever sees its own organization's coaches.
- Additive: events accept and return `coachId` (one of your organization's coaches): a clinic shows on that coach's page. On `PATCH /events/{id}`, null unlinks it.
- New: `POST /groups/{id}/email-invites` (new scope `invites:write`) invites up to 200 contacts by email in one call. Each address gets one invitation per group, ever, with a join link, a "not for me" link and a one-click opt-out from all organizer invitations. Phone-only contacts are saved for texting by hand (no SMS is sent). Members, earlier invitations, opted-out addresses, duplicates and invalid entries are skipped with a reason per row. Caps per rolling 24 hours: 500 per organization, 200 per group (`overLimit` counts the rest).
- New: `GET /groups/{id}/email-invites` (scope `groups:read`): each contact's status (queued, sent, joined, declined, opted_out, bounced, failed, text), totals, and what today's caps still allow.
- New: `POST /sessions/bulk` (scope `sessions:write`) posts up to 50 weekly sessions, each under the same rules as `POST /sessions`; per-item results with the session or an error.
- New: rosters as JSON or CSV (with a Checked in column): `GET /groups/{id}/roster` and `/roster.csv` (groups:read), `GET /games/{id}/occurrences/{date}/roster` and `.csv` (groups:read), `GET /sessions/{id}/occurrences/{date}/roster` and `.csv` (sessions:read), `GET /ladders/{id}/roster` and `.csv` (ladders:read), `GET /leagues/{id}/roster` and `.csv` (leagues:read). Only public profiles are named, with level, VPR and DUPR ID only when the player shows them; everyone else is `A player`, kept in place so waitlist order stays true. No emails.
2026-10-31 · v1
- Additive: venues carry `dataSources`, the open-data sources behind their facts (OpenStreetMap, Overture Maps, city parks open data), each with `key`, `name`, `license`, `licenseUrl`, `attribution` and `url`. Show the `attribution` wherever you show those facts. `source` gains the value `open_data` for venues first listed from Overture Maps or a city's open data.
- New: `GET /calendar` (scope `venues:read`) returns your organization's public iCalendar feed (`icsUrl`, `webcalUrl`): every published session at your granted venues for the next 4 weeks, one event per date, cancellations marked. No key is needed to read the feed itself.
- Additive: events carry `icsUrl` (the event as a text/calendar file, null until published); sessions from `GET /venues/{venueId}/sessions` carry `icsUrl` (that session's next 4 weeks of dates), `skillMin` and `skillMax`.
- Additive: `GET /venues/{venueId}/sessions` accepts `levelMin` / `levelMax`: only sessions whose skill range overlaps are listed; sessions with no range (all levels) always are.
- Additive: every event has its own page on Vara Performance. Event payloads carry `slug` (fixed when the event is created) and `pageUrl` (https://varaperformance.com/events/{slug}); listings across the site now link there.
- Additive: events accept and return `registrationUrl` (https; the event page's Register button) and tournament facts: `fees`, `skillMin`/`skillMax`, `formats`, `schedule`, `format`, `divisions`, `policies`, `registrationCloses`, `indoor`, `points`, `payout`. All optional on `POST`/`PATCH /events`; on `PATCH` a field sent as null clears it.
- New: `PATCH /venues/{venueId}` (new scope `venues:write`) updates a granted venue's court counts, lights, amenities (restrooms, water, surface, lines, parking, wheelchair access, permanent nets), phone, website, cost / hours / access notes and booking link. Name, slug and location never change through the API. Audited.
- Additive: venues carry `reservationUrl` (where to book a court, https) and `reservationNotes` (prices or rules); `GET /venues` and every venue payload include them.
- New: `GET /sessions/{id}/occurrences?from&to` (new scope `sessions:read`; `venues:read` also works) gives going / maybe / waitlisted counts, capacity, spots left and a cancelled flag per venue-local date. Counts only.
- New webhooks: `session.rsvp_changed` (counts, never who) and `session.full` (once, until a spot opens again) for sessions at venues your organization holds a grant for.
- Additive: `GET /venues/{venueId}/sessions` lists community-posted sessions there too (`source: "community"`, organizer shown only as "Community organizer"), plus `organizer` and `editable` on every session. Others' sessions are read-only; `POST /closures` still cancels a date. Other organizations' unpublished sessions are no longer listed.
- New: venues merged by the daily duplicate cleanup answer 404 `{ code: "VENUE_MERGED", mergedInto }` on any call with the old id; webhook `venue.merged` (`{ from, into }`) goes to every organization with a grant on either venue.
- New: events (scopes `events:read` / `events:write`). `GET`/`POST /events`, `GET`/`PATCH`/`DELETE /events/{id}`. At a granted venue or a free-text place, with offset or venue-local times; listed on /events, the city page and the court page, attributed to your organization. TRUSTED partners publish at once, NEW partners' wait for review.
- New: `GET /venues/{venueId}/reviews/summary`: average, count, star breakdown and top tags. No review text or authors.
- New: groups with weekly group games that invite every member automatically. Scopes `groups:read` and `groups:write`; a key only ever sees its own organization's groups.
- New: `GET`/`POST /groups`, `GET`/`PATCH /groups/{id}`, `GET /groups/{id}/members` (public profiles named, others counted), `POST /groups/{id}/invites` (by handle).
- New: `GET`/`POST /groups/{id}/games`, `PATCH`/`DELETE /games/{id}`, `GET /games/{id}/occurrences` (In / Out / Maybe / waitlist counts per venue-local date), `POST /games/{id}/occurrences/{date}/cancel`.
- New webhooks: `group.member_joined`, `group.member_left`, `group_game.invites_sent`, `group_game.rsvp_changed` (counts and the player id only), `group_game.full`, `group_game.cancelled`.
- New: round robins (scopes `roundrobins:read` / `roundrobins:write`). `GET /round-robin-formats` lists the 11 formats with descriptions and recommended player counts; `GET`/`POST /round-robins`, `GET /round-robins/{id}` (rounds, courts, matches, scores, standings), `POST /round-robins/{id}/players` and `DELETE /round-robins/{id}/players/{playerId}` (late arrivals and leavers; only rounds still to come change), `POST /round-robins/{id}/advance`, `PUT /matches/{id}/score` (organizer override), `GET /round-robins/{id}/standings` and `/standings.csv` (text/csv).
- Round robins are created at your granted venues and carry `timezone` with an offset `startsAt`; your keys see only your organization's.
- New webhooks: `round_robin.round_started`, `round_robin.score_reported`, `round_robin.completed` (final standings).
- New: ladders with seasons (scopes `ladders:read` / `ladders:write`). `GET`/`POST /ladders` (at your granted venues; Season 1 starts at once), `GET`/`PATCH /ladders/{id}` (rules, `seasonWeeks`, `autoAdvance`, `seedMode` carry or rating, status; audited), `GET /ladders/{id}/standings`, `GET /ladders/{id}/matches` (`?season=`).
- New: `GET /ladders/{id}/seasons` and `GET /ladders/{id}/seasons/{n}` (frozen final standings once a season ends), `POST /ladders/{id}/seasons/end` to end the running season early. Season dates are instants with the ladder's `timezone`; your keys see only your organization's ladders.
- New webhooks: `ladder.season_started`, `ladder.season_ended` (final standings and the next season), `ladder.match_confirmed` (a confirmed score or a forfeit).
2026-10-30 · v1
- Additive: sessions carry `capacity` (players per occurrence, or null for no limit), and `POST /sessions` accepts it. Past it, RSVPs join a waitlist and move up in order when a spot opens; the promoted player is notified.
- New: `PUT /sessions/{id}/capacity` (scope sessions:write) sets the weekly capacity, or one venue-local `date`'s. `null` clears it. Raising it fills spots from the waitlist at once; lowering it never removes anyone already going.
2026-10-28 · v1
- Additive: every venue, session, closure, event and league fixture now carries its IANA `timezone` (venues past a zone line, e.g. Knoxville TN, get the minority zone).
- Additive: `startsAt`/`endsAt` (ISO 8601 with the zone's offset) alongside local `date` + `startTime`/`endTime`; weekly sessions get `nextStartsAt`/`nextEndsAt`; closures answer with the local day as instants.
- Fixture `scheduledAt` accepts a local `YYYY-MM-DDTHH:MM` (venue zone) as well as an offset instant; local times in a DST gap or overlap are refused with 400.
- Webhooks `session.published`, `closure.created` and `league.result_reported` carry the same `timezone` and offset instants. See Dates, times & time zones.
2026-10-28 · v1
- Initial release: venues, recurring sessions, closures.
- Bearer API keys with scopes; Idempotency-Key on writes.
- Webhooks signed per Standard Webhooks (HMAC-SHA256); Ed25519 available on request.
