OrganizedPlay
Run an event

API and webhooks

Free, on every plan, and able to write — not a read-only feed. Your website can take registrations, your till can seat a walk-in, and your Discord can announce round two the moment it starts.

The API: read your events, standings and results from software of your own. Discord, webhooks and the API

Getting a key

A store owner creates one in their store settings. It looks like opk_… and is shown once — we store only a hash of it, so we cannot show it to you again and neither can anybody who steals our database.

curl https://www.organizedplay.gg/api/v1/events \
  -H "Authorization: Bearer opk_your_key_here"

Bearer header only. We do not accept a key in the query string, because that puts it in every access log, proxy cache and browser history it passes through.

Use www.organizedplay.gg. The bare domain redirects there, and clients drop the Authorization header when they follow a redirect — so a key that is perfectly good will come back 401.

read
Look at your calendar and your rosters.
write
Everything read can do, plus seating players. You do not need both keys.

A key belongs to one store and can never reach another’s calendar.

Endpoints

Version 1 API endpoints
EndpointScopeWhat it does
GET /api/v1/eventsreadYour events, with real seat counts and the public URL of each.
POST /api/v1/eventswritePut a tournament on the calendar. Public by default; a paid plan may ask for anything else.
GET /api/v1/events/{id}/registrationsreadWho is on the roster and what state they are in.
POST /api/v1/events/{id}/registrationswriteSeat a player. Waitlists them if the room is full.
POST /api/v1/events/{id}/competitionwriteStart the tournament against whoever is checked in. A second call is safe.
POST /api/v1/events/{id}/registrations/{rid}/check-inwriteMark a registered player as physically present.
DELETE /api/v1/events/{id}/registrations/{rid}writeWithdraw. Frees the seat and promotes the earliest waitlisted player.
GET /api/v1/events/{id}/roundsreadEvery round and who sat where. Add ?round=latest for the one being played.
GET /api/v1/events/{id}/standingsreadThe table, derived on the call. Split by division where the event has them.
GET /api/v1/players/{id}readOne player’s public history across every event and organization — a public record, no key required.
curl -X POST https://www.organizedplay.gg/api/v1/events/EVENT_ID/registrations \
  -H "Authorization: Bearer opk_your_key_here" \
  -H "content-type: application/json" \
  -d '{"displayName": "Ada Lovelace", "email": "ada@example.com"}'

Posting the same player twice returns 409 rather than seating them again, so a retried request is safe. The path is versioned: /api/v1 will not change under code you have already shipped.

Webhook events

Register an HTTPS endpoint in your store settings and we POST to it when these happen. Subscribe to a list, or to none and get all of them — including ones we add later.

event.created
A night was put on the calendar, published or not.
registration.created
Somebody took a seat — online, at the counter, or over the API.
participant.checked_in
A player who registered has arrived.
competition.started
The first round of the first stage began.
round.started
A round began. Pairings are readable from this moment.
match.completed
A result was reported for the first time.
result.updated
A recorded result was corrected. Anything you published from it has changed.
round.completed
A round closed. Every match in it is final.
competition.completed
The event is over and the standings are final.
standings.updated
The table moved. Sent when a round closes, not on every result.
judge_call.opened
A table asked for a judge. Carries the table and the reason given.
result.disputed
Two reports for one table disagree. A judge decides; nothing is final yet.

These fire from the tournament record itself, so a round paired on a tablet with no signal still announces itself the moment that tablet reconnects.

Verifying a delivery

Every POST carries an op-signature header. Check it before you trust the body — anybody can POST to your URL, and the signature is what tells our delivery from theirs.

op-signature: t=1756500000,v1=<hex>
op-event: round.started
op-delivery: <unique id, safe to deduplicate on>

// v1 = HMAC-SHA256("<t>.<raw body>", "whsec_your_signing_secret")

Reject anything whose t is more than 5 minutes from now: the timestamp is inside the signature, so this is what stops a captured delivery being replayed at you forever. Compare the hex in constant time.

When your endpoint is down

For agents

An MCP server at POST /api/mcp — JSON-RPC 2.0, with initialize, tools/list and tools/call. Send the same API key as the Authorization header. A key is offered only the tools it may actually call, so a read key never sees the one that seats a player.

initialize returns instructions an agent reads before it calls anything — chiefly that it should not compute placings or tiebreakers itself. The engine applies the ruleset the store actually chose, and arithmetic done from raw results will disagree with it. /llms.txt says the same thing for something that arrived by reading rather than by browsing.

Coming from TopDeck

A translation of the API above into the envelope TopDeck publishes, so a bot you already have changes a base URL rather than getting rewritten. Point it at /api/compat/topdeck/v2/…, send your key as Authorization: {key} the way theirs is sent, and {TID} becomes our event id.

This exists to get you off TopDeck, not to keep you in their shape. Everything it returns is a translation of /api/v1, which is where the work goes.

House rules

Every endpoint above is on the free plan too — a plan buys more volume, not more of the API.