MakeReady

API

Base: https://<portal>/. JSON in and out. Errors: { "error": "…" } with 401/403/404/409/422; zod validation errors return 422 with issues.

Authentication

FormFor whomHow
Session cookie mkr_sessionweb portalPOST /api/v1/auth/login
Bearer device tokentabletsPOST /api/v1/auth/device/login
Bearer API key mkr_…club portals, integrationscreated by a club admin, with scopes

Only the SHA-256 hash of tokens and keys is stored.

Accounts

MethodPathDescription
POST/api/v1/auth/registercreate account (email, password ≥ 10, firstName, lastName)
POST/api/v1/auth/login / /logoutweb session
POST/api/v1/auth/device/logintablet: { email, password, device: { deviceId, name, platform, appVersion } } → { token, expiresAt, user, memberships }
GET/api/v1/auth/meprofile + club roles
PATCH/api/v1/auth/meedit own name (firstName, lastName)
POST/api/v1/auth/me/password{ currentPassword, newPassword }; revokes all other sessions and device tokens
GET/api/v1/auth/me/resultsmatches in which this account is linked as a competitor

Clubs (any signed-in user may create a club and becomes its admin)

MethodPath
GET/POST/api/v1/clubs
GET/api/v1/clubs/:id (id or slug) — includes the caller's role
PATCH/api/v1/clubs/:id — club admin: name, slug, country, website
GET/PUT/api/v1/clubs/:id/members — { email, role: admin|staff|member }
DELETE/api/v1/clubs/:id/members/:userId
GET/POST/DELETE/api/v1/clubs/:id/api-keys — scopes registrations:write, registrations:read, results:read, matches:read, matches:write, *
GET/POST/api/v1/clubs/:id/integrations — webhooks (url, events[]) → secret shown once

Matches

MethodPathPermissions
GET/api/v1/matches?clubId=&kind=match|trainingvisibility — events of both kinds unless kind filters
POST/api/v1/matchesany signed-in user — clubId? (omit for a club-less match), name, discipline (ipsc_handgun|fds|lvr|multigun), kind (match|training, default match), startDate, endDate, visibility, externalId?, scoring?, divisions?, categories?; a club match requires club admin
GET/PATCH/api/v1/matches/:idPATCH (match admin or owner): name, kind, visibility, status (draft|registration|live|finished|archived), startDate, endDate, divisions[], categories[] (the scoring profile itself comes from the central rules)
POST/api/v1/matches/:id/claimstaff/admin of clubId — a club claims a club-less match
GET/api/v1/matches/:id/stages · PUT /stages/:stageIdstage upsert (number, name, targets[], parTimeSeconds?, dnfTimeSeconds?, briefing?, active); the stage type always follows the match scoring method (Comstock or time plus)
PUT/api/v1/matches/:id/squads/:squadId409 if the club portal manages the registrations
GET/PUT/api/v1/matches/:id/competitors[/:competitorId]same
GET/api/v1/matches/:id/resultsstandings per division + overall
DELETE/api/v1/matches/:iddelete a match or training with its stages, squads, competitors, scores and bracket (match admin; the web portal asks to type the name, the tablet confirms)
PATCH/api/v1/matches/:idname, kind, status, visibility, dates, divisions, categories; clubId (null releases the event, a club id claims or transfers it to a club you are staff of; visibility "club" falls back to "private" without a club); discipline (only while no approved score or decided bout exists: the scoring profile, divisions and categories are re-applied from the new discipline's central rules, stage types follow, a shootoff bracket is removed)
GET/api/v1/matches/:id/competitors/suggest?q=autocomplete for the competitor form: names this club (or, for a club-less event, this organiser) registered before, with club, division, power factor, categories and linked account; never another club's entries (staff)
GET/api/v1/clubs/:id/roster?q=the club's competitor directory (staff)
GET/api/v1/matches/:id/bracketshootoff: the bracket with all results plus the resolved view (named sides, status, placements)
POST/api/v1/matches/:id/bracket/startshootoff: generate the bracket once (staff); body `{ lives: 123, bestOf: 135, finalBestOf: 135 }`, defaults 2/1/1
POST/api/v1/matches/:id/bracket/resultsshootoff: bout decisions `{ results: [{ boutId, winnerIdnull, heats?: competitorId[], decidedAt, deviceId }] }`; best-of bouts send the heat winners in order; latest per bout wins, each validated (staff; tablets and the portal)
POST/api/v1/matches/:id/bracket/restartshootoff: regenerate the bracket from the current competitors with a new format (same body as start; defaults to the current format), discarding all decisions (admin; the UIs confirm first)
DELETE/api/v1/matches/:id/bracketshootoff: remove the bracket while no bout is decided (admin)
GET/api/v1/matches/:id/results/detail?division=standings with the result per stage (time, hits, penalties with the procedural reasons, hit factor, stage points, rank, revision of the counted score); feeds the detail view and the side-by-side comparison
GET/api/v1/matches/:id/bundleeverything for tablets/dashboards
POST/api/v1/matches/:id/live/refreshstaff: recalculate live standings

Discipline rules (central, versioned)

MethodPathRights
GET/api/v1/rulesany signed-in user — all disciplines + versions map (tablets compare this with their local copy)
GET/api/v1/rules/:disciplineany signed-in user
PUT/api/v1/rules/:disciplineMakeReady admin — name?, scoring?, divisions?, categories?, notes?; increments version
POST/api/v1/matches/:id/rules/reapplymatch admin — copies the current rules into a match without approved scores

Sync (tablets, staff)

MethodPath
GET/api/v1/sync/matches — events this user may score or manage (all statuses except archived, drafts included)
GET/api/v1/sync/matches/:id?since= — bundle/incremental
POST/api/v1/sync/push — { deviceId, matchId, scores[] } → { accepted[], ignored[], serverTime }

Integrations (API key)

MethodPathScope
GET/api/v1/integrations/matchesmatches:read — all events of the club
POST/api/v1/integrations/matchesmatches:write — create an event for the club: name, discipline, kind (default training), startDate, endDate?, visibility (default club), externalId; idempotent on externalId (returns the existing event with created: false)
GET/api/v1/integrations/matches/:refmatches:read — one event
PATCH/api/v1/integrations/matches/:refmatches:write — name, startDate, endDate, visibility, status, externalId
PUT/api/v1/integrations/matches/:ref/registrationsregistrations:write — :ref = MakeReady id or externalId
GET/api/v1/integrations/matches/:ref/registrationsregistrations:read
GET/api/v1/integrations/matches/:ref/resultsresults:read — standings + raw scores

Details and the webhook format: docs/club-portal-integration.md.

Web portal

The web portal is the site's home page: a single-page app served at / (client script at /app.js, no build step) that uses the JSON API below with the session cookie. /matches/:id redirects into it and /app redirects to /. It offers: sign in and registration, the match list and "my results", a match wizard with stage editor (targets, par/DNF time, briefing), squads and competitors, results per division with a detail per stage for every competitor and a side-by-side comparison of two competitors, score corrections (new revisions via the sync API; a corrected score carries a coloured "Corrected · rev n" pill in the results and scores tabs), club management (members and roles, API keys, webhooks) and, for the MakeReady admin, clubs and the discipline rules editor.

MethodPathRights
GET/api/v1/matches/:id/rolevisibility — the caller's role on the match (platform, owner, admin, staff, member, null)
GET/api/v1/matches/:id/squadsvisibility
DELETE/api/v1/matches/:id/stages/:stageIdmatch admin — only without scores
DELETE/api/v1/matches/:id/squads/:squadIdmatch admin — not when the club portal manages registrations
DELETE/api/v1/matches/:id/competitors/:competitorIdmatch admin — only without scores

Website and versions (no authentication)

MethodPath
GET/ — the web portal (SPA)
GET/faq — FAQ with public matches and version numbers
GET/docs, /docs/:slug — documentation, generated from docs/*.md on every deploy
GET/api/v1/version — { portal, android: { version, versionCode }, ios: { version, buildNumber }, build }
GET/health

Public (no authentication, only visibility = public)

MethodPath
GET/api/public/matches?kind=match|training
GET/api/public/matches/:id/results (cache 5 s while live, 5 min when finished)
GET/scoreboard/:id?division=OPEN — HTML scoreboard for TV/kiosk
WS/live/matches/:id?role=board — live protocol (packages/shared/src/live.ts)
GET/live/matches/:id/state — latest snapshot (debug)

Matches with visibility club/private are reachable through the same paths for anyone who is logged in and has the permission.

Result format

{
  "match": { "id": "…", "name": "…", "discipline": "ipsc_handgun", "status": "live" },
  "results": {
    "ALL":  [{ "rank": 1, "competitorId": "…", "name": "Stijn Maes", "division": "OPEN", "club": "Example Club", "total": 25, "percent": 100, "stagesScored": 1, "disqualified": false }],
    "OPEN": [ … ]
  },
  "generatedAt": "2026-10-04T13:54:34.208Z"
}

total is match points (IPSC/LVR), percentage points (Multigun time_percentage) or total time in seconds (total_time).

An OpenAPI document (/api/v1/openapi) is on the roadmap; until then the zod schemas in packages/shared are the contract.