Skip to content
Preview. LawFive's code and server image aren't public yet, so some steps here won't work until they are.

API reference

The API of a LawFive server, which the web app, the watch apps and the phone apps use. The same reference as OpenAPI 3.1 is openapi.json, for generating code or loading into an API tool.

  • Address: everything is under /v1 on the server: https://api.lawfive.app/v1/... for LawFive’s own service, or your own server’s address.
  • Signing in: apps sign in (or link, for a watch), take the token and send it as Authorization: Bearer <token>. Browsers use the session cookie and must send an Origin header matching the server. Everything needs signing in unless it says otherwise; without it the answer is 401.
  • API keys: for scripts and integrations, made in Settings and sent the same way (Authorization: Bearer lf_...). Each key can use only the collections it was given; each endpoint says what a key needs, and a key without it gets 403. See API keys.
  • Errors: a JSON body with error, a sentence a person can read (ApiError); the account and linking endpoints have their own shapes (AuthError, OAuthError). Anything unexpected is a 500. Every answer, errors included, has a LawFive-Version header with the server’s version; an error without it came from something in front of the server (a proxy while the server restarts, say), so treat it as not reaching the server and try again rather than acting on its status.
  • Data: JSON. Ids are UUIDs, times are ISO 8601 with a UTC offset, durations are whole seconds. Records are described in the data model (docs/MATCH_CONFIG.md and JSON Schema).
  • Older servers: check capabilities in GET /v1/meta before using address search or weather, which a server can switch off; ignore fields and capabilities you don’t know.
Endpoint What it does
GET /health Is the server up
GET /v1/meta What this server offers
GET /v1/openapi.json This server’s API, as OpenAPI
POST /v1/auth/sign-up/email Make an account
POST /v1/auth/sign-in/email Sign in
POST /v1/auth/request-password-reset Email a password reset link
POST /v1/auth/reset-password Choose a new password
POST /v1/auth/change-password Change the password
POST /v1/auth/update-user Change the account’s name
POST /v1/auth/sign-out Sign out
GET /v1/auth/get-session Who is signed in
GET /v1/account The signed-in account
PUT /v1/account/email Change the account’s email
DELETE /v1/account/email Cancel a change of email
POST /v1/account/email/confirm Confirm a new email address
POST /v1/account/delete Delete the account
POST /v1/auth/device/code Ask for a linking code
GET /v1/auth/device Look up a code
POST /v1/auth/device/approve Approve a code
POST /v1/auth/device/deny Refuse a code
POST /v1/auth/device/token Poll for the token
GET /v1/matches The user’s matches
GET /v1/matches/{id} One match
PUT /v1/matches/{id} Save a match
DELETE /v1/matches/{id} Delete a match
The same for match-templates, match-records, venues, competitions, referees, settings See Records
GET /v1/match-records/{id}/track A match report’s track
PUT /v1/match-records/{id}/track Upload a match report’s track
PUT /v1/match-records/{id}/track/pitch Mark where the pitch is
DELETE /v1/match-records/{id}/track/pitch Forget where the pitch is
DELETE /v1/match-records/{id}/track Delete a match report’s track
GET /v1/changes Changes since a cursor
GET /v1/people Everyone on the server
POST /v1/people/{id}/approve Approve an account
POST /v1/people/{id}/decline Decline an account
PUT /v1/people/{id}/admin Make or stop being an admin
PUT /v1/people/{id}/plan Put an account in a plan
GET /v1/people/{id}/plan-history An account’s plans so far
PUT /v1/people/{id}/suspended Suspend an account, or undo it
POST /v1/people/{id}/unlock Unlock signing in
POST /v1/people/{id}/reset-link Make a password reset link
DELETE /v1/people/{id} Delete an account
GET /v1/plans The plans
POST /v1/plans Make a plan
PUT /v1/plans/{code} Change a plan
DELETE /v1/plans/{code} Delete a plan
PUT /v1/plan-settings Choose the default plan and the sign-up trial
GET /v1/server-settings The server’s settings
PUT /v1/server-settings Change the server’s settings
POST /v1/setup/check Check a new server’s setup code
POST /v1/setup/finish Finish setting the server up
GET /v1/server-settings/network How the server sees visitors
POST /v1/server-settings/test-email Send a test email
POST /v1/server-settings/test-push Send a test notification
POST /v1/app-sign-in A code to sign a phone in
GET /v1/app-sign-in/{id} Whether a phone has used a code
POST /v1/app-sign-in/check Whose a code is
POST /v1/app-sign-in/redeem Swap a code for a session
GET /v1/invites The invitations
POST /v1/invites Invite someone
DELETE /v1/invites/{id} Cancel an invitation
GET /v1/invite/{token} Look up an invitation
POST /v1/import/matches Import matches
POST /v1/import/file Import matches from a file
GET /v1/assignor-calendars The account’s assignor calendars
POST /v1/assignor-calendars/preview Preview a calendar link
POST /v1/assignor-calendars Connect an assignor calendar
PATCH /v1/assignor-calendars/{id} Change an assignor calendar
DELETE /v1/assignor-calendars/{id} Remove an assignor calendar
POST /v1/assignor-calendars/{id}/read Read an assignor calendar now
GET /v1/calendar The calendar feed’s address
POST /v1/calendar Make the calendar feed, or a new address for it
DELETE /v1/calendar Turn the calendar feed off
GET /v1/calendar/{file} The calendar feed
GET /v1/stats Stats on the user’s games
GET /v1/stats/games.csv The games, as a spreadsheet
GET /v1/devices Where the account is signed in
DELETE /v1/devices/{id} Sign a device out
GET /v1/devices/matches Which matches are on which watch
POST /v1/devices/sign-out-others Sign out everywhere else
PUT /v1/devices/current/push Say how to reach this phone
GET /v1/notices/{id} What an integration did
GET /v1/account/storage How much of its storage the account is using
GET /v1/account/plan The account’s plan
GET /v1/attachments Attachments of a kind of record
POST /v1/attachments Upload an attachment
PUT /v1/attachments/{id} Change an attachment
PUT /v1/attachments/{id}/file Replace an attachment’s file
DELETE /v1/attachments/{id} Delete an attachment
GET /v1/attachments/{id}/file An attachment’s file
GET /v1/attachments/{id}/thumb An attachment’s thumbnail
GET /v1/export Download everything
POST /v1/restore Restore from an export
GET /v1/matches/{id}/share A match’s share link
POST /v1/matches/{id}/share Share a match
DELETE /v1/matches/{id}/share Stop sharing a match
GET /v1/shared/{token} A shared match
GET /v1/shared/{token}/track A shared match’s track
GET /v1/webhooks The account’s webhooks
POST /v1/webhooks Add a webhook
PUT /v1/webhooks/{id} Change a webhook
DELETE /v1/webhooks/{id} Delete a webhook
GET /v1/webhooks/{id}/deliveries A webhook’s deliveries
POST /v1/webhooks/{id}/test Send a test
GET /v1/api-keys The account’s API keys
POST /v1/api-keys Make an API key
DELETE /v1/api-keys/{id} Delete an API key
PATCH /v1/api-keys/{id} Change an API key
GET /v1/watch/matches The next matches, for the watch
POST /v1/watch/records Upload a finished match
POST /v1/watch/tracks Upload part of a match’s track
POST /v1/watch/pitch Pitch corners from the watch
POST /v1/watch/screen The match screen chosen on the watch
POST /v1/watch/fee Mark a fee paid from the watch
GET /v1/geocode Find an address
GET /v1/quiz The quiz’s questions
GET /v1/weather Weather for matches in a window
GET /v1/weather/{matchId} One match’s weather

What the server is and offers.

Is the server up. Answers 503 if the server can’t reach its database within 2 seconds, for Docker’s healthcheck (ADM-7). Other services, such as weather or email, don’t affect it. No sign-in needed.

Answer Body When
200 ok (text) It is.
413 ApiError The request is too big.
429 ApiError Too many tries from this address; the Retry-After header says how many seconds to wait.
503 database unavailable (text) It can’t reach its database.

What this server offers. The API version and capabilities, the map tiles to use and the data providers to credit. Apps check capabilities before using geocode or weather, which a server can switch off. No sign-in needed.

Answer Body When
200 ServerMeta The server’s description.
413 ApiError The request is too big.
429 ApiError Too many tries from this address; the Retry-After header says how many seconds to wait.

This server’s API, as OpenAPI. The whole API as OpenAPI 3.1, for this server’s version and address, so a client or an AI agent can work from what this server really does. About 340 KB, or much less compressed; https://lawfive.dev/llms.txt lists the same as smaller pages (AGT-3). No sign-in needed.

Answer Body When
200 { openapi: “3.1.0” } The description.
413 ApiError The request is too big.
429 ApiError Too many tries from this address; the Retry-After header says how many seconds to wait.

Signing up, in and out (Better Auth’s endpoints). Browsers keep the session in a cookie and must send an Origin header matching the server’s address; apps send no Origin, take the token from the set-auth-token response header and send it as Authorization: Bearer .

Make an account. Signs the new account in. With SIGN_UP=closed only an invitation can make one; with SIGN_UP=approval the account is pending until an admin approves it (USR-2 to USR-4). An address in ADMIN_EMAILS without an invitation is pending even when sign-up is open (SEC-8). A new server’s first account needs the setupCode from its logs (ADM-5). No sign-in needed.

Body: { name: AccountName, email: string, password: string, invite?: string }

Answer Body When
200 { token: string | null, user: User } Made and signed in. The user’s status is pending when it waits for approval. Header: set-auth-token.
400 AuthError Not allowed: an invalid email, a name that’s empty or over 100 characters (INVALID_NAME, USR-10), a password that’s too short or long, sign-up is closed (SIGN_UP_CLOSED), or the invitation isn’t valid (INVITE_INVALID) or is for another address (INVITE_EMAIL_MISMATCH).
403 AuthError A browser request whose Origin isn’t the server’s.
413 ApiError The request is too big.
422 AuthError An account with this email already exists.
429 ApiError Too many tries from this address; the Retry-After header says how many seconds to wait.

Sign in. No sign-in needed.

Body: { email: string, password: string, rememberMe?: boolean }

Answer Body When
200 { redirect: boolean, token: string, user: User } Signed in. Header: set-auth-token.
401 AuthError Wrong email or password.
403 AuthError A browser request whose Origin isn’t the server’s, the account is suspended (ACCOUNT_SUSPENDED, USR-7), or a phone signing in to an account that already has as many watches and phones as its plan allows (DEVICE_LIMIT, PLAN-7).
413 ApiError The request is too big.
429 AuthError Too many tries from this address (SEC-2), or too many wrong passwords for this email address (ACCOUNT_LOCKED, SEC-12): after 5 in a row it’s locked for a minute, doubling to at most an hour, and even the right password waits. Retry-After says how long; resetting the password ends it.

Email a password reset link. When the server can send email (the email capability), emails a link to the web app’s page for choosing a new password; it works once, for an hour (USR-7). The answer is the same whether or not there’s an account with the address. Without email, an admin makes a link instead (POST /v1/people/{id}/reset-link). No sign-in needed.

Body: { email: string, redirectTo?: string }

Answer Body When
200 { status: true, message: string } Emailed, if there’s an account with the address.
400 AuthError The server can’t send email (RESET_PASSWORD_DISABLED), or the address isn’t valid.
403 AuthError A browser request whose Origin isn’t the server’s.
413 ApiError The request is too big.
429 ApiError Too many tries from this address; the Retry-After header says how many seconds to wait.

Choose a new password. With the token from a reset link, emailed or made by an admin (USR-7). Devices already signed in stay signed in. It also ends a lock after too many wrong passwords (SEC-12). No sign-in needed.

Body: { newPassword: string, token: string }

Answer Body When
200 { status: true } Changed; sign in with it.
400 AuthError The link was used, has expired or is mistyped (INVALID_TOKEN), or the password is too short or long.
403 AuthError A browser request whose Origin isn’t the server’s.
413 ApiError The request is too big.
429 ApiError Too many tries from this address; the Retry-After header says how many seconds to wait.

Change the password. Needs the current password (USR-7).

Body: { currentPassword: string, newPassword: string, revokeOtherSessions?: boolean }

Answer Body When
200 { token: string | null, user: User } Changed.
400 AuthError The current password is wrong (INVALID_PASSWORD), or the new one is too short or long.
401 AuthError Not signed in.
403 AuthError A browser request whose Origin isn’t the server’s.
413 ApiError The request is too big.
429 ApiError Too many tries from this address; the Retry-After header says how many seconds to wait.

Change the account’s name. Better Auth’s own endpoint, open only to the name (USR-10): the same rule as signing up, 1 to 100 characters with spaces at either end trimmed. GET /v1/account and get-session give the new name straight away.

Body: NameChange

Answer Body When
200 { status: true } Changed.
400 AuthError The name is empty or longer than 100 characters, or the body has something besides the name (INVALID_NAME).
401 AuthError Not signed in.
403 AuthError A browser request whose Origin isn’t the server’s, or a watch’s link (WATCH_ONLY).
413 ApiError The request is too big.
429 ApiError Too many tries from this address; the Retry-After header says how many seconds to wait.

Sign out. Ends this session. To sign out another device, use DELETE /v1/devices/{id}. No sign-in needed.

Body: {}

Answer Body When
200 { success: true } Signed out.
413 ApiError The request is too big.
429 ApiError Too many tries from this address; the Retry-After header says how many seconds to wait.

Who is signed in. No sign-in needed.

Answer Body When
200 { session: Session, user: User } | null The session and account, or null when not signed in.
413 ApiError The request is too big.
429 ApiError Too many tries from this address; the Retry-After header says how many seconds to wait.

The signed-in account. Its status and whether it’s an admin. The one endpoint a pending account (USR-4) can use; every other gets 403. Not with API keys.

Answer Body When
200 Account The account.
401 ApiError Not signed in, the session has ended, or the API key is unknown or has expired.
403 ApiError An API key: this needs signing in; or the account is waiting for approval (USR-4).
413 ApiError The request is too big.
429 ApiError Too many tries from this address; the Retry-After header says how many seconds to wait.

Change the account’s email. With the password, to confirm it’s them (USR-7). An address in the server’s ADMIN_EMAILS can’t be changed here, and can’t be changed to. When the server can send email (the email capability), the new address is emailed a link that changes it when followed, once, within 24 hours (USR-9), and the old address keeps working until then; asking again replaces the link. Without email, it changes straight away. Not with API keys.

Body: EmailChange

Answer Body When
200 Account Changed (the server can’t send email), or already the account’s address.
202 Account A link was emailed to the new address: pendingEmail says which, and pendingEmailUntil until when.
400 { error: string, issues?: ValidationIssue[] } The address isn’t valid, or the password is wrong.
401 ApiError Not signed in, the session has ended, or the API key is unknown or has expired.
403 ApiError An API key: this needs signing in; or the account is waiting for approval (USR-4).
409 ApiError Another account has the address, or ADMIN_EMAILS lists the old or new one.
413 ApiError The request is too big.
429 ApiError Too many tries from this address; the Retry-After header says how many seconds to wait.

Cancel a change of email. The link emailed to the new address stops working (USR-9). Nothing to cancel is fine too. Not with API keys.

Answer Body When
200 Account Canceled, or there was nothing waiting.
401 ApiError Not signed in, the session has ended, or the API key is unknown or has expired.
403 ApiError An API key: this needs signing in; or the account is waiting for approval (USR-4).
413 ApiError The request is too big.
429 ApiError Too many tries from this address; the Retry-After header says how many seconds to wait.

Confirm a new email address. What the web app’s /confirm-email/ page sends, signed in or not (USR-9): the account’s address becomes the new one, and the old address is told. It doesn’t sign anyone in. The link works once, for 24 hours. No sign-in needed.

Body: EmailConfirmation

Answer Body When
200 { email: string } Changed.
400 ApiError The link was used, canceled, replaced by a newer one, has expired or is mistyped.
409 ApiError Another account took the address meanwhile, or ADMIN_EMAILS lists the old or new one.
413 ApiError The request is too big.
429 ApiError Too many tries from this address; the Retry-After header says how many seconds to wait.

Delete the account. With the password, to confirm it’s them (USR-7). Deletes the account and everything in it, signs out every device and stops its API keys and calendar feed. Refused for the only admin while there are other accounts. Not with API keys.

Body: { password: string }

Answer Body When
200 { ok: true } Deleted.
400 ApiError The password is wrong.
401 ApiError Not signed in, the session has ended, or the API key is unknown or has expired.
403 ApiError An API key: this needs signing in; or the account is waiting for approval (USR-4).
409 ApiError It’s the server’s only admin; make someone else an admin first.
413 ApiError The request is too big.
429 ApiError Too many tries from this address; the Retry-After header says how many seconds to wait.

A watch links to an account with a code (RFC 8628): it asks for a code and shows it, someone signed in enters it on the web app’s Link page and approves it, and the watch, polling meanwhile, gets its own token. That token only reaches the watch’s endpoints (/v1/watch/), and of the account endpoints only sign-out and get-session (SEC-13); anything else answers 403.

Ask for a linking code. Step 1, from the watch. Codes last 10 minutes. No sign-in needed.

Body: { client_id: “lawfive-garmin”, scope?: string }

Answer Body When
200 DeviceCode A code to show.
400 OAuthError Not a known app (invalid_client).
413 ApiError The request is too big.
429 ApiError Too many tries from this address; the Retry-After header says how many seconds to wait.

Look up a code. Step 2, from the Link page. Signed in, it also claims the code for this session, which approving or refusing it needs. No sign-in needed.

Parameter In Type Description
user_code query string The code the watch shows.
Answer Body When
200 { user_code: string, status: “pending” | “approved” | “denied”, client_id?: string, device_name?: string, scope?: string | null } The code is waiting.
400 OAuthError No such code, or it has expired.
413 ApiError The request is too big.
429 ApiError Too many tries from this address; the Retry-After header says how many seconds to wait.

Approve a code. Step 2, after the lookup: links the watch to the signed-in account.

Body: { userCode: string }

Answer Body When
200 { success: true } Approved; the watch’s next poll gets its token.
400 OAuthError No such code, it has expired, or it wasn’t looked up first.
401 OAuthError Not signed in.
403 AuthError The account is waiting for approval (ACCOUNT_PENDING), or already has as many watches and phones as its plan allows (DEVICE_LIMIT, PLAN-7).
413 ApiError The request is too big.
429 ApiError Too many tries from this address; the Retry-After header says how many seconds to wait.

Refuse a code.

Body: { userCode: string }

Answer Body When
200 { success: true } Refused; the watch’s next poll gets access_denied.
400 OAuthError No such code, it has expired, or it wasn’t looked up first.
401 OAuthError Not signed in.
413 ApiError The request is too big.
429 ApiError Too many tries from this address; the Retry-After header says how many seconds to wait.

Poll for the token. Step 3, from the watch, every interval seconds until it’s approved, refused or expired. No sign-in needed.

Body: { grant_type: “urn:ietf:params:oauth:grant-type:device_code”, device_code: string, client_id: string }

Answer Body When
200 DeviceToken Approved: the watch’s token, which only reaches the watch’s endpoints (SEC-13).
400 OAuthError Not yet (authorization_pending), too often (slow_down), refused (access_denied) or expired (expired_token).
413 ApiError The request is too big.
429 ApiError Too many tries from this address; the Retry-After header says how many seconds to wait.

The user’s records, one collection per kind. Ids are UUIDs made by the client, so a record created offline keeps its id. Every change raises version; an update sends the version it last saw and gets 409 if the record changed in the meantime. Deleted records are kept as markers so other devices learn of the deletion.

Collection Record Saved as Notes
/v1/matches Match MatchInput
/v1/match-templates MatchTemplate MatchTemplateInput
/v1/match-records MatchRecord MatchRecordInput Watches upload theirs through POST /v1/watch/records instead.
/v1/venues Venue VenueInput
/v1/competitions Competition CompetitionInput
/v1/referees Referee RefereeInput
/v1/settings UserSettings UserSettingsInput There’s one per user: create it once, then update it.

Each has the same four endpoints, shown here for matches.

The user’s matches. Every match that isn’t deleted, oldest change first. API keys: need matches:read.

Answer Body When
200 { items: Match[] } The matches.
401 ApiError Not signed in, the session has ended, or the API key is unknown or has expired.
403 ApiError An API key without matches:read; or the account is waiting for approval (USR-4).
413 ApiError The request is too big.
429 ApiError Too many tries from this address; the Retry-After header says how many seconds to wait.

One match. Deleted ones too, with deletedAt set. API keys: need matches:read.

Parameter In Type Description
id path Id UUID made by the device that created the record.
Answer Body When
200 Match The match.
401 ApiError Not signed in, the session has ended, or the API key is unknown or has expired.
403 ApiError An API key without matches:read; or the account is waiting for approval (USR-4).
404 ApiError Not found, or not this user’s.
413 ApiError The request is too big.
429 ApiError Too many tries from this address; the Retry-After header says how many seconds to wait.

Save a match. Creates the match (send no version) or updates it (send the version you last saw). Saving a deleted record brings it back. API keys: need matches:write.

Parameter In Type Description
id path Id UUID made by the device that created the record.

Body: MatchInput

Answer Body When
200 Match Updated; version is raised.
201 Match Created, at version 1.
400 { error: string, issues?: ValidationIssue[] } The id isn’t a UUID, the body’s id differs from the URL’s, or the body doesn’t match the schema (issues lists the problems).
401 ApiError Not signed in, the session has ended, or the API key is unknown or has expired.
403 PlanRefusal An API key without matches:write, or the account waiting for approval; or a new match over the plan’s limit (PLAN-4).
409 { error: string, current?: Match } It changed since the version sent (current is the record as it is now), the id belongs to another kind of record, or it would duplicate another (a match’s external id, a second settings record).
413 ApiError The request is too big.
429 ApiError Too many tries from this address; the Retry-After header says how many seconds to wait.

Delete a match. Marks it deleted; it stays as a marker so other devices hear of it. Deleting it again changes nothing. Its match records go with it, marked deleted the same way, and their tracks are deleted (RES-8); saving the match again doesn’t bring them back. API keys: need matches:write.

Parameter In Type Description
id path Id UUID made by the device that created the record.
version (optional) query integer The version last seen; when given, the delete fails if it has changed since.
Answer Body When
200 Match Deleted: the record with deletedAt.
401 ApiError Not signed in, the session has ended, or the API key is unknown or has expired.
403 ApiError An API key without matches:write; or the account is waiting for approval (USR-4).
404 ApiError Not found, or not this user’s.
409 { error: string, current: Match } It changed since the version sent.
413 ApiError The request is too big.
429 ApiError Too many tries from this address; the Retry-After header says how many seconds to wait.

GPS and heart rate for a match report (TRK-1 to TRK-3), in LawFive’s own format whatever recorded them: convert from the source (RefSix, an activity file, a phone’s health store) before uploading. Not part of sync, as tracks are big; fetch one when it’s shown. The figures (distance, speeds, sprints, heart rate and zones) are worked out by the server each time.

A match report’s track. With its figures, over the periods played when the match report has their start times. API keys: need match-records:read.

Parameter In Type Description
id path Id UUID made by the device that created the record.
Answer Body When
200 { track: MatchTrack, summary: TrackSummary, pitch?: PitchOutline, heat?: { cols: integer, rows: integer, lengthM: number, widthM: number, total: number[], periods: ({ period: integer, cells: number[] })[] }, updatedAt: Timestamp } The track and its figures.
401 ApiError Not signed in, the session has ended, or the API key is unknown or has expired.
403 ApiError An API key without match-records:read; or the account is waiting for approval (USR-4).
404 ApiError No track for this match report, or no such match report.
413 ApiError The request is too big.
429 ApiError Too many tries from this address; the Retry-After header says how many seconds to wait.

Upload a match report’s track. Replaces any track the match report had. Up to 43,200 samples (12 hours at one a second) in each series. API keys: need match-records:write.

Parameter In Type Description
id path Id UUID made by the device that created the record.

Body: MatchTrack

Answer Body When
200 { summary: TrackSummary, updatedAt: Timestamp } Replaced the track it had.
201 { summary: TrackSummary, updatedAt: Timestamp } Stored.
400 ValidationError The body doesn’t match the schema (issues lists the problems).
401 ApiError Not signed in, the session has ended, or the API key is unknown or has expired.
403 ApiError An API key without match-records:write; or the account is waiting for approval (USR-4).
404 ApiError No such match report: save it first.
413 ApiError The request is too big.
429 ApiError Too many tries from this address; the Retry-After header says how many seconds to wait.

Mark where the pitch is. The two corners of one touchline and the center spot, as the referee placed them (TRK-9); kept with the track, marked by: referee. Heatmaps and Stats use it. API keys: need match-records:write.

Parameter In Type Description
id path Id UUID made by the device that created the record.

Body: { corners: [Position, Position], centre: Position }

Answer Body When
200 { pitch: PitchOutline, updatedAt: Timestamp } Kept.
400 ValidationError The body doesn’t match the schema.
401 ApiError Not signed in, the session has ended, or the API key is unknown or has expired.
403 ApiError An API key without match-records:write; or the account is waiting for approval (USR-4).
404 ApiError No track for this match report.
413 ApiError The request is too big.
429 ApiError Too many tries from this address; the Retry-After header says how many seconds to wait.

Forget where the pitch is. Back to the pitch found from the track (TRK-9). API keys: need match-records:write.

Parameter In Type Description
id path Id UUID made by the device that created the record.
Answer Body When
200 { pitch?: PitchOutline, updatedAt: Timestamp } Forgotten; pitch is the one found from the track, when there’s enough GPS.
401 ApiError Not signed in, the session has ended, or the API key is unknown or has expired.
403 ApiError An API key without match-records:write; or the account is waiting for approval (USR-4).
404 ApiError No track for this match report.
413 ApiError The request is too big.
429 ApiError Too many tries from this address; the Retry-After header says how many seconds to wait.

Delete a match report’s track. The match report stays. API keys: need match-records:write.

Parameter In Type Description
id path Id UUID made by the device that created the record.
Answer Body When
200 { ok: true } Deleted.
401 ApiError Not signed in, the session has ended, or the API key is unknown or has expired.
403 ApiError An API key without match-records:write; or the account is waiting for approval (USR-4).
404 ApiError No track for this match report.
413 ApiError The request is too big.
429 ApiError Too many tries from this address; the Retry-After header says how many seconds to wait.

Everything that changed since a device last looked.

Changes since a cursor. Every record written after the cursor, deletions included, oldest first, across all collections (for an API key, the ones it can read). Start with since=0, keep the cursor, and ask again while more is true. Changes made at the same time are never skipped. API keys: any read scope; only those collections are included.

Parameter In Type Description
since (optional) query integer The cursor from the last page; 0 (the default) for everything.
limit (optional) query integer Changes per page: 500 by default, at most 1000.
Answer Body When
200 ChangesPage A page of changes.
400 ApiError since or limit isn’t a whole number.
401 ApiError Not signed in, the session has ended, or the API key is unknown or has expired.
403 ApiError An API key without *:read; or the account is waiting for approval (USR-4).
413 ApiError The request is too big.
429 ApiError Too many tries from this address; the Retry-After header says how many seconds to wait.

Admins (USR-1) see everyone on the server, approve or decline new accounts (USR-4), make other admins, and make password reset links, suspend and delete accounts, and change who can sign up (USR-7); and make plans, named sets of limits, and put each account in one (PLAN-1 to PLAN-5). Everyone else gets 403.

Everyone on the server. Waiting for approval first, then everyone else by name. Not with API keys.

Answer Body When
200 { people: Person[] } The accounts.
401 ApiError Not signed in, the session has ended, or the API key is unknown or has expired.
403 ApiError Not an admin.
413 ApiError The request is too big.
429 ApiError Too many tries from this address; the Retry-After header says how many seconds to wait.

Approve an account. It can use LawFive straight away; they’re emailed when the server can send email. Not with API keys.

Parameter In Type Description
id path string

Body: {}

Answer Body When
200 Person Approved.
401 ApiError Not signed in, the session has ended, or the API key is unknown or has expired.
403 ApiError Not an admin.
404 ApiError No such account.
413 ApiError The request is too big.
429 ApiError Too many tries from this address; the Retry-After header says how many seconds to wait.

Decline an account. Deletes an account that’s waiting for approval. Approved accounts can’t be declined. Not with API keys.

Parameter In Type Description
id path string

Body: {}

Answer Body When
200 { ok: true } Declined and deleted.
401 ApiError Not signed in, the session has ended, or the API key is unknown or has expired.
403 ApiError Not an admin.
404 ApiError No such account.
409 ApiError The account isn’t waiting for approval.
413 ApiError The request is too big.
429 ApiError Too many tries from this address; the Retry-After header says how many seconds to wait.

Make or stop being an admin. Not with API keys.

Parameter In Type Description
id path string

Body: { admin: boolean }

Answer Body When
200 Person Changed.
401 ApiError Not signed in, the session has ended, or the API key is unknown or has expired.
403 ApiError Not an admin.
404 ApiError No such account.
409 ApiError Your own account, an account waiting for approval, or one that ADMIN_EMAILS makes an admin.
413 ApiError The request is too big.
429 ApiError Too many tries from this address; the Retry-After header says how many seconds to wait.

Put an account in a plan. With an optional last day (in the account’s time zone, PLAN-9) and the plan after it, the default when not given (PLAN-3, PLAN-5). Nothing in the account is lost if it’s over the new plan’s limits; only adding more is refused (PLAN-4). Not with API keys.

Parameter In Type Description
id path string

Body: PlanChange

Answer Body When
200 Person Changed.
400 { error: string, issues?: ValidationIssue[] } The body doesn’t match, there’s no such plan, or the last day has passed.
401 ApiError Not signed in, the session has ended, or the API key is unknown or has expired.
403 ApiError Not an admin.
404 ApiError No such account.
413 ApiError The request is too big.
429 ApiError Too many tries from this address; the Retry-After header says how many seconds to wait.

An account’s plans so far. Each change of plan, newest first (PLAN-5): whether someone has had a trial, say. Not with API keys.

Parameter In Type Description
id path string
Answer Body When
200 { changes: PlanHistoryEntry[] } The changes.
401 ApiError Not signed in, the session has ended, or the API key is unknown or has expired.
403 ApiError Not an admin.
404 ApiError No such account.
413 ApiError The request is too big.
429 ApiError Too many tries from this address; the Retry-After header says how many seconds to wait.

Suspend an account, or undo it. A suspended account can’t sign in, and is signed out everywhere (linked watches need linking again afterward); its API keys and calendar feed stop working. Its data is kept (USR-7). Not with API keys.

Parameter In Type Description
id path string

Body: { suspended: boolean }

Answer Body When
200 Person Changed.
400 ApiError The body isn’t { suspended: true } or { suspended: false }.
401 ApiError Not signed in, the session has ended, or the API key is unknown or has expired.
403 ApiError Not an admin.
404 ApiError No such account.
409 ApiError Your own account, an account waiting for approval, or one that ADMIN_EMAILS makes an admin.
413 ApiError The request is too big.
429 ApiError Too many tries from this address; the Retry-After header says how many seconds to wait.

Unlock signing in. After too many wrong passwords an account’s address is locked for a while (SEC-12); this ends it now. A password reset ends it too. Not with API keys.

Parameter In Type Description
id path string

Body: {}

Answer Body When
200 Person Unlocked.
401 ApiError Not signed in, the session has ended, or the API key is unknown or has expired.
403 ApiError Not an admin.
404 ApiError No such account.
413 ApiError The request is too big.
429 ApiError Too many tries from this address; the Retry-After header says how many seconds to wait.

Make a password reset link. For someone who forgot their password, on any server: the admin sends it to them. It works once, for 24 hours (USR-7). Not with API keys.

Parameter In Type Description
id path string

Body: {}

Answer Body When
201 ResetLink Made.
401 ApiError Not signed in, the session has ended, or the API key is unknown or has expired.
403 ApiError Not an admin.
404 ApiError No such account.
409 ApiError Your own account (change it in Settings), or one waiting for approval.
413 ApiError The request is too big.
429 ApiError Too many tries from this address; the Retry-After header says how many seconds to wait.

Delete an account. Deletes the account and everything in it (USR-7). It can’t be undone. Not with API keys.

Parameter In Type Description
id path string
Answer Body When
200 { ok: true } Deleted.
401 ApiError Not signed in, the session has ended, or the API key is unknown or has expired.
403 ApiError Not an admin.
404 ApiError No such account.
409 ApiError Your own account (delete it in Settings), or one that ADMIN_EMAILS makes an admin.
413 ApiError The request is too big.
429 ApiError Too many tries from this address; the Retry-After header says how many seconds to wait.

The plans. Every plan, with how many accounts are in each, and the sign-up trial (PLAN-1, PLAN-5). Not with API keys.

Answer Body When
200 Plans The plans.
401 ApiError Not signed in, the session has ended, or the API key is unknown or has expired.
403 ApiError Not an admin.
413 ApiError The request is too big.
429 ApiError Too many tries from this address; the Retry-After header says how many seconds to wait.

Make a plan. Not with API keys.

Body: PlanInput

Answer Body When
201 Plan Made.
400 { error: string, issues?: ValidationIssue[] } The body doesn’t match.
401 ApiError Not signed in, the session has ended, or the API key is unknown or has expired.
403 ApiError Not an admin.
409 ApiError Another plan has this code.
413 ApiError The request is too big.
429 ApiError Too many tries from this address; the Retry-After header says how many seconds to wait.

Change a plan. Its name, code or limits; everyone in it gets the new limits straight away. A new code breaks anything outside LawFive that refers to the old one. Not with API keys.

Parameter In Type Description
code path string

Body: PlanInput

Answer Body When
200 Plan Changed.
400 { error: string, issues?: ValidationIssue[] } The body doesn’t match.
401 ApiError Not signed in, the session has ended, or the API key is unknown or has expired.
403 ApiError Not an admin.
404 ApiError No such plan.
409 ApiError Another plan has the new code.
413 ApiError The request is too big.
429 ApiError Too many tries from this address; the Retry-After header says how many seconds to wait.

Delete a plan. Not with API keys.

Parameter In Type Description
code path string
Answer Body When
200 { ok: true } Deleted.
401 ApiError Not signed in, the session has ended, or the API key is unknown or has expired.
403 ApiError Not an admin.
404 ApiError No such plan.
409 ApiError It’s in use (Plan.inUse): move its accounts to another plan first.
413 ApiError The request is too big.
429 ApiError Too many tries from this address; the Retry-After header says how many seconds to wait.

Choose the default plan and the sign-up trial. Not with API keys.

Body: PlanSettings

Answer Body When
200 Plans Changed: the plans as they are now.
400 { error: string, issues?: ValidationIssue[] } The body doesn’t match, or there’s no such plan.
401 ApiError Not signed in, the session has ended, or the API key is unknown or has expired.
403 ApiError Not an admin.
413 ApiError The request is too big.
429 ApiError Too many tries from this address; the Retry-After header says how many seconds to wait.

The server’s settings. Each setting an admin can change (ADM-1, ADM-2), where its value comes from, and the value (not for secrets). Not with API keys.

Answer Body When
200 { settings: ServerSettingView[] } The settings.
401 ApiError Not signed in, the session has ended, or the API key is unknown or has expired.
403 ApiError Not an admin.
413 ApiError The request is too big.
429 ApiError Too many tries from this address; the Retry-After header says how many seconds to wait.

Change the server’s settings. Sets or clears the settings sent (ADM-1); one the environment sets can’t be changed, and if any can’t be, none are. Takes effect straight away. GET /v1/meta says what’s offered. Not with API keys.

Body: ServerSettings

Answer Body When
200 { settings: ServerSettingView[] } Changed: the settings as they are now.
400 { error: string, issues?: ValidationIssue[] } The body doesn’t match, a value isn’t one the setting takes, or the environment sets it.
401 ApiError Not signed in, the session has ended, or the API key is unknown or has expired.
403 ApiError Not an admin.
413 ApiError The request is too big.
429 ApiError Too many tries from this address; the Retry-After header says how many seconds to wait.

Check a new server’s setup code. While nobody has an account, whether this is the code in the server’s logs (ADM-5). The first account is made through sign-up, with the code as setupCode. No sign-in needed.

Body: { code: string }

Answer Body When
200 { ok: true } It’s the code.
400 ApiError It isn’t.
404 ApiError The server is already set up, or doesn’t need a code.
413 ApiError The request is too big.
429 ApiError Too many tries from this address; the Retry-After header says how many seconds to wait.

Finish setting the server up. The first account finished or skipped the setup wizard, so it doesn’t open again (ADM-5). Not with API keys.

Answer Body When
200 { ok: true } Done.
401 ApiError Not signed in, the session has ended, or the API key is unknown or has expired.
403 ApiError Not an admin.
413 ApiError The request is too big.
429 ApiError Too many tries from this address; the Retry-After header says how many seconds to wait.

How the server sees visitors. The address this server sees the asking admin at, and the TRUSTED_PROXIES and TRUSTED_IP_HEADER in use (SEC-14), so Settings → Server can show whether a proxy in front of it is believed. Both are environment values, not changed here. Not with API keys.

Answer Body When
200 NetworkView As it is now.
401 ApiError Not signed in, the session has ended, or the API key is unknown or has expired.
403 ApiError Not an admin.
413 ApiError The request is too big.
429 ApiError Too many tries from this address; the Retry-After header says how many seconds to wait.

Send a test email. To the signed-in admin, through the email settings in use (ADM-4). Not with API keys.

Answer Body When
200 { to: string } Sent.
401 ApiError Not signed in, the session has ended, or the API key is unknown or has expired.
403 ApiError Not an admin.
409 ApiError Email isn’t set up.
413 ApiError The request is too big.
429 ApiError Too many tries from this address; the Retry-After header says how many seconds to wait.
502 ApiError The SMTP server didn’t take it; the error says why.

Send a test notification. To the signed-in admin’s own phones that have notifications on, through the push relay (PUSH-6). Not with API keys.

Answer Body When
200 { sent: integer } The relay took it.
401 ApiError Not signed in, the session has ended, or the API key is unknown or has expired.
403 ApiError Not an admin.
409 ApiError Notifications are off on this server, or none of the admin’s phones has them on.
413 ApiError The request is too big.
429 ApiError Too many tries from this address; the Retry-After header says how many seconds to wait.
502 ApiError The relay didn’t take it; the error says why.

Signing in on a phone with a code the web app shows, as a QR code or in letters (APP-13), instead of typing a password.

A code to sign a phone in. It works once, for two minutes. Not with API keys.

Answer Body When
201 AppSignIn Made.
401 ApiError Not signed in, the session has ended, or the API key is unknown or has expired.
403 ApiError An API key: this needs signing in; or the account is waiting for approval (USR-4).
413 ApiError The request is too big.
429 ApiError Too many tries from this address; the Retry-After header says how many seconds to wait.

Whether a phone has used a code. So the web app can say when the phone is signed in. Expired codes go when a new one is made, and then this answers 404. Not with API keys.

Parameter In Type Description
id path Id UUID made by the device that created the record.
Answer Body When
200 AppSignInStatus How it stands.
401 ApiError Not signed in, the session has ended, or the API key is unknown or has expired.
403 ApiError An API key: this needs signing in; or the account is waiting for approval (USR-4).
404 ApiError No such code, or not this account’s.
413 ApiError The request is too big.
429 ApiError Too many tries from this address; the Retry-After header says how many seconds to wait.

Whose a code is. For the phone to ask before signing in. It doesn’t use the code up. No sign-in needed.

Body: AppSignInCode

Answer Body When
200 AppSignInCheck It can be used.
404 ApiError No such code.
410 ApiError It was used, or it has expired.
413 ApiError The request is too big.
429 ApiError Too many tries from this address; the Retry-After header says how many seconds to wait.

Swap a code for a session. The session belongs to the device asking, and shows under Devices. No sign-in needed.

Body: AppSignInCode

Answer Body When
200 AppSignInResult Signed in.
403 PlanRefusal The account is suspended, or already has as many watches and phones as its plan allows (PLAN-7).
404 ApiError No such code.
410 ApiError It was used, or it has expired.
413 ApiError The request is too big.
429 ApiError Too many tries from this address; the Retry-After header says how many seconds to wait.

Links that let someone make an account even when sign-up is closed (USR-3). Admins make and cancel them; the link opens the web app’s invitation page, which looks the invitation up and signs up with it.

The invitations. Newest first, including used and expired ones from the last 90 days. Not with API keys.

Answer Body When
200 { invites: Invite[] } The invitations.
401 ApiError Not signed in, the session has ended, or the API key is unknown or has expired.
403 ApiError Not an admin.
413 ApiError The request is too big.
429 ApiError Too many tries from this address; the Retry-After header says how many seconds to wait.

Invite someone. With send, it’s emailed to them too, when the server can send email. Not with API keys.

Body: NewInvite

Answer Body When
201 { invite: Invite, emailed: boolean } Made.
400 { error: string, issues?: ValidationIssue[] } The body doesn’t match.
401 ApiError Not signed in, the session has ended, or the API key is unknown or has expired.
403 ApiError Not an admin.
409 ApiError There’s already an account with this email.
413 ApiError The request is too big.
429 ApiError Too many tries from this address; the Retry-After header says how many seconds to wait.

Cancel an invitation. Its link stops working straight away. Not with API keys.

Parameter In Type Description
id path Id UUID made by the device that created the record.
Answer Body When
200 { ok: true } Canceled.
401 ApiError Not signed in, the session has ended, or the API key is unknown or has expired.
403 ApiError Not an admin.
404 ApiError No such invitation, or it was used.
413 ApiError The request is too big.
429 ApiError Too many tries from this address; the Retry-After header says how many seconds to wait.

Look up an invitation. For the invitation page, before signing up with it. No sign-in needed.

Parameter In Type Description
token path string
Answer Body When
200 InviteLookup It can be used.
404 ApiError No such invitation: the link is mistyped, or it was canceled.
410 ApiError It was used, or it has expired.
413 ApiError The request is too big.
429 ApiError Too many tries from this address; the Retry-After header says how many seconds to wait.

Adding and updating matches from a spreadsheet, CSV or JSON file (IMP-1 to IMP-8). Apps read the file into rows; the server matches each row to a match (by LawFive id, else match number), finds or adds the venues, competitions and referees it names, and saves only what the row fills in. Check first with dryRun.

Import matches. Each row adds a match or updates one: the row’s ID column (a LawFive match id) wins, else its match number (any source). Only the columns a row fills in change an existing match. Venues, pitches, competitions and referees are found by name, ignoring case (venues, pitches and referees by their other names too), and added when there’s none (an API key then needs write access to those too). Rows with problems are skipped and the rest are saved; nothing is saved on a dry run.

The columns, by their headings (any case or spacing, so home_colour and homeColour work too, as do common other headings like Field for Pitch):

Column JSON What it’s for Example Needed
ID id LawFive’s own ID for a match, as in a LawFive export. Leave it empty for a new match; with it, the row updates that match. 5c1e2a9b-… Optional
Match number matchNumber The league’s or assignor’s number for the match. Importing a file again with the same number updates the match instead of adding a copy. BYL-1142 Optional
Assignor assignor Who assigned you the match: a person, or the assigning group’s name. Dana Whitfield Optional
Status status scheduled, played, abandoned, canceled or postponed. A new match is scheduled when it’s empty. scheduled Optional
Date date The day of the match: 2026-11-07, or the way your computer writes dates (11/7/2026 in the US, 7/11/2026 in most other places). 2026-11-07 New matches*
Time time The kickoff time: 14:30 or 2:30 PM. A new match with a date needs one. 2:30 PM Optional
Kickoff kickoff The date and time together, instead of Date and Time. Add a UTC offset (2026-11-07T14:30-08:00) to fix the moment exactly. 2026-11-07 14:30 New matches*
Time zone timeZone Where the kickoff time is, by its name in the time zone database. Empty: the match’s own time zone, or your computer’s. America/Los_Angeles Optional
Home home The home team’s name. Strikers U12 New matches*
Home short name homeShortName Up to 4 letters for the home team on the watch. Empty: made from both team names (the first three letters or digits of each, skipping the words both names start with), and it follows them when they change. STR Optional
Home color homeColour The home team’s color on the watch: black, white, red, orange, yellow, green, blue or purple, or a hex code like #D71920. red Optional
Away away The away team’s name. Rovers U12 New matches*
Away short name awayShortName Up to 4 letters for the away team on the watch. Empty: made from both team names (the first three letters or digits of each, skipping the words both names start with), and it follows them when they change. ROV Optional
Away color awayColour The away team’s color on the watch, written like Home color. #0057B8 Optional
Competition competition The league, cup or tournament, by name. One you don’t have yet is added, as a league. Bay Youth League U12 Boys Optional
Venue venue Where it’s played, by name. One you don’t have yet is added; give it an address and a place on the map in Venues. Twin Creeks Sports Complex Optional
Venue address venueAddress The venue’s street address, for directions: given to a venue that’s added, or one that has no address yet. 969 Caribbean Dr, Sunnyvale, CA 94089 Optional
Pitch pitch The pitch at the venue. It’s added to the venue if the venue doesn’t have it yet. Field 3 Optional
Match name matchName A name for the match itself, shown with the competition: a round, a final, a friendly’s occasion. Semi-final Optional
Age group ageGroup As the league writes it: U12, U19, Adult, O35. U12 Optional
Gender gender Who plays: male, female or co-ed (boys, girls, men, women and mixed work too). female Optional
Role role Your role: referee, AR1, AR2 or fourth official. Empty: taken from a crew column with your name in it, if there is one. AR1 Optional
Referee referee The referee’s name, or one of their other names. Your own name makes it your role; someone you haven’t saved yet is added to Referees. TBD or empty: nobody yet. Priya Natarajan Optional
AR1 ar1 The first assistant referee’s name, written like Referee. Sam Rivera Optional
AR2 ar2 The second assistant referee’s name, written like Referee. Jordan Lee Optional
Fourth official fourthOfficial The fourth official’s name, written like Referee. Sam Rivera Optional
Template template One of your match templates, by name, for its format (periods, their length, breaks). It must already exist. Empty for a new match: the competition’s template, or 2 × 45 minutes. U12 league: 2×30 Optional
Periods periods How many periods, 1 to 4: 2 for halves, 4 for quarters, 1 for a tournament game of one period. Overrides the template’s. 2 Optional
Period length (min) periodLength Minutes in each period. Overrides the template’s. 30 Optional
Half-time (min) halfTime Minutes of break between periods. Overrides the template’s. 5 Optional
Team size teamSize Players a side on the field, 1 to 11. Overrides the template’s. 9 Optional
Fee fee What you’re paid for the match, as a number. 40.50 Optional
Currency currency The fee’s currency, as a three-letter code (USD, GBP, EUR). Empty: your currency in Settings. USD Optional
Paid paid Whether you’ve been paid the fee: yes or no. no Optional
Tags tags Labels to find and filter matches by, separated by commas. league, needs AR Optional
Notes notes Anything to remember for the match. Bring the spare flags. Optional
Outlook outlook What to expect: how competitive it’ll be, the teams’ strengths, history between them. Top of the table meets second. Optional

* A new match needs a date (Date and Time, or Kickoff) or its teams; everything else is optional, and an empty value never changes a match. API keys: need matches:write.

Body: MatchImport

Answer Body When
200 MatchImportResult What happened to each row (or would, on a dry run).
400 { error: string, issues?: ValidationIssue[] } The body doesn’t match: no rows, more than 1000, or not rows of values.
401 ApiError Not signed in, the session has ended, or the API key is unknown or has expired.
403 PlanRefusal An API key without matches:write; or the matches, venues or referees it would add are over the plan’s limits (PLAN-4), so nothing is saved.
413 ApiError The request is too big.
429 ApiError Too many tries from this address; the Retry-After header says how many seconds to wait.

Import matches from a file. The same import as POST /v1/import/matches, from a file the server reads: CSV (commas, semicolons or tabs), Excel (.xlsx, its Matches sheet or else the first) or JSON (a list of objects, or an object with a matches list). The web app and phone apps use this; send it with dryRun first to check every row, then again without to save. API keys: need matches:write.

Body: multipart/form-data: the fields of MatchImportFileFields, and the files file (required: the file, at most 5 MB and 1000 rows)

Answer Body When
200 MatchImportFileResult What happened to each row (or would, on a dry run), and where each is in the file.
400 { error: string, issues?: ValidationIssue[] } The file is missing, too big (5 MB, or the plan’s fileMb), or can’t be read; error says why, in words to show the person who chose it.
401 ApiError Not signed in, the session has ended, or the API key is unknown or has expired.
403 PlanRefusal An API key without matches:write; or the matches, venues or referees it would add are over the plan’s limits (PLAN-4), so nothing is saved.
413 ApiError The request is too big.
429 ApiError Too many tries from this address; the Retry-After header says how many seconds to wait.

An assignor system’s private calendar link (ArbiterSports, Assignr, HorizonWebRef, RefTown and others give each official one), which the server reads every 30 minutes into the account’s matches through the import (FEED-1 to FEED-10): new games added, moved ones changed, ones gone before kickoff canceled. The link is a password, so it’s kept encrypted and never sent back. Only a signed-in session can manage them.

The account’s assignor calendars. Oldest first, each with how its latest read went. Only when GET /v1/meta lists assignor-calendars. Not with API keys.

Answer Body When
200 { calendars: AssignorCalendar[] } The calendars.
401 ApiError Not signed in, the session has ended, or the API key is unknown or has expired.
403 ApiError An API key: this needs signing in; or the account is waiting for approval (USR-4).
413 ApiError The request is too big.
429 ApiError Too many tries from this address; the Retry-After header says how many seconds to wait.

Preview a calendar link. Reads the link once and says what it holds: the games still to come, how many are already in LawFive, and the first three as they’d be saved. Nothing is saved (FEED-1). Only when GET /v1/meta lists assignor-calendars. Not with API keys.

Body: AssignorCalendarLink

Answer Body When
200 AssignorCalendarPreview What it holds.
400 { error: string, issues?: ValidationIssue[] } The body doesn’t match, or the link can’t be read (not a web address, a private address, no answer, or not a calendar); error says why, in words to show the person.
401 ApiError Not signed in, the session has ended, or the API key is unknown or has expired.
403 ApiError An API key: this needs signing in; or the account is waiting for approval (USR-4).
413 ApiError The request is too big.
429 ApiError Too many tries from this address; the Retry-After header says how many seconds to wait.

Connect an assignor calendar. Reads it straight away, adding its games still to come, then every 30 minutes. As many as the account’s plan allows (FEED-10). Only when GET /v1/meta lists assignor-calendars. Not with API keys.

Body: NewAssignorCalendar

Answer Body When
201 AssignorCalendar Connected and read.
400 { error: string, issues?: ValidationIssue[] } The body doesn’t match, or the link can’t be read; error says why.
401 ApiError Not signed in, the session has ended, or the API key is unknown or has expired.
403 PlanRefusal An API key, or the account already has as many assignor calendars as its plan allows (PLAN-4).
413 ApiError The request is too big.
429 ApiError Too many tries from this address; the Retry-After header says how many seconds to wait.

Change an assignor calendar. Its name, notifications, pausing or resuming it, or a new link. Resuming it or a new link reads it straight away. Only when GET /v1/meta lists assignor-calendars. Not with API keys.

Parameter In Type Description
id path Id UUID made by the device that created the record.

Body: AssignorCalendarChange

Answer Body When
200 AssignorCalendar Changed.
400 { error: string, issues?: ValidationIssue[] } The body doesn’t match, or the new link can’t be read; error says why.
401 ApiError Not signed in, the session has ended, or the API key is unknown or has expired.
403 ApiError An API key: this needs signing in; or the account is waiting for approval (USR-4).
404 ApiError Not found, or not this user’s.
413 ApiError The request is too big.
429 ApiError Too many tries from this address; the Retry-After header says how many seconds to wait.

Remove an assignor calendar. It’s no longer read; the matches it added stay, as ordinary matches. Only when GET /v1/meta lists assignor-calendars. Not with API keys.

Parameter In Type Description
id path Id UUID made by the device that created the record.
Answer Body When
200 { ok: true } Removed.
401 ApiError Not signed in, the session has ended, or the API key is unknown or has expired.
403 ApiError An API key: this needs signing in; or the account is waiting for approval (USR-4).
404 ApiError Not found, or not this user’s.
413 ApiError The request is too big.
429 ApiError Too many tries from this address; the Retry-After header says how many seconds to wait.

Read an assignor calendar now. Instead of waiting for its next read; a paused one stays paused. Only when GET /v1/meta lists assignor-calendars. Not with API keys.

Parameter In Type Description
id path Id UUID made by the device that created the record.
Answer Body When
200 AssignorCalendar Read, with how it went.
401 ApiError Not signed in, the session has ended, or the API key is unknown or has expired.
403 ApiError An API key: this needs signing in; or the account is waiting for approval (USR-4).
404 ApiError Not found, or not this user’s.
413 ApiError The request is too big.
429 ApiError Too many tries from this address; the Retry-After header says how many seconds to wait.

A private calendar feed of the user’s matches (CAL-1 to CAL-5), for calendar apps to subscribe to.

The calendar feed’s address. Not with API keys.

Answer Body When
200 CalendarFeed The feed.
401 ApiError Not signed in, the session has ended, or the API key is unknown or has expired.
403 ApiError An API key: this needs signing in; or the account is waiting for approval (USR-4).
404 ApiError There’s no feed; make one with POST.
413 ApiError The request is too big.
429 ApiError Too many tries from this address; the Retry-After header says how many seconds to wait.

Make the calendar feed, or a new address for it. A new address replaces the old one, which stops working (calendars subscribed to it stop updating). Not with API keys.

Body: {}

Answer Body When
201 CalendarFeed Made.
401 ApiError Not signed in, the session has ended, or the API key is unknown or has expired.
403 ApiError An API key: this needs signing in; or the account is waiting for approval (USR-4).
413 ApiError The request is too big.
429 ApiError Too many tries from this address; the Retry-After header says how many seconds to wait.

Turn the calendar feed off. Not with API keys.

Answer Body When
200 { ok: true } Its address stops working.
401 ApiError Not signed in, the session has ended, or the API key is unknown or has expired.
403 ApiError An API key: this needs signing in; or the account is waiting for approval (USR-4).
404 ApiError There’s no feed.
413 ApiError The request is too big.
429 ApiError Too many tries from this address; the Retry-After header says how many seconds to wait.

The calendar feed. What calendar apps fetch: the user’s matches as events (CAL-2 to CAL-4), past and future, canceled ones marked so. The address is the secret; there’s no other sign-in. No sign-in needed.

Parameter In Type Description
file path string The feed’s token and .ics.
Answer Body When
200 iCalendar The calendar (text/calendar).
403 ApiError The account is suspended (USR-7).
404 ApiError No feed has this address: it was replaced or turned off.
413 ApiError The request is too big.
429 ApiError Too many tries from this address; the Retry-After header says how many seconds to wait.

Reports on the user’s games (STAT-1 to STAT-4), worked out by the server so every app shows the same.

Stats on the user’s games. Games are matches played or abandoned. Every figure follows the filters; options lists what the filters can choose from. An API key gets only what its scopes can read: without match-records:read there are no figures from match reports or tracks (distance, discipline, added time, fitness, heatmaps); without settings:read, no heart-rate zones and the standard sprint speed; without venues:read, referees:read or competitions:read, the lists naming them are empty, while the counts stay. API keys: need matches:read.

Parameter In Type Description
from (optional) query LocalDate The first day, in each match’s own time zone; none for the first game.
to (optional) query LocalDate The last day, inclusive; none for today and on.
gender (optional) query Gender
competitionId (optional) query Id
role (optional) query “referee” | “assistant” | “fourth” Your role: referee, assistant (AR1 or AR2) or fourth.
tag (optional) query string Games with this tag (ignoring case).
venueId (optional) query Id
Answer Body When
200 Stats The stats.
400 ApiError A filter isn’t valid: from or to isn’t a date, an unknown gender or role, or an id isn’t a UUID.
401 ApiError Not signed in, the session has ended, or the API key is unknown or has expired.
403 ApiError An API key without matches:read; or the account is waiting for approval (USR-4).
413 ApiError The request is too big.
429 ApiError Too many tries from this address; the Retry-After header says how many seconds to wait.

The games, as a spreadsheet. The games matching the filters (and, optionally, one slice of a chart), oldest first, as CSV with a header row: date, kickoff, competition, home, away, score, role, venue, pitch, age group, gender, distance (m), average heart rate, kickoff temperature (°C), rain (mm), cautions, send-offs, added time (s), fee, currency and paid (STAT-12). For an API key, the columns its scopes can’t read are left empty, as for /v1/stats. API keys: need matches:read.

Parameter In Type Description
from (optional) query LocalDate The first day, in each match’s own time zone; none for the first game.
to (optional) query LocalDate The last day, inclusive; none for today and on.
gender (optional) query Gender
competitionId (optional) query Id
role (optional) query “referee” | “assistant” | “fourth” Your role: referee, assistant (AR1 or AR2) or fourth.
tag (optional) query string Games with this tag (ignoring case).
venueId (optional) query Id
pitchId (optional) query Id | “none” At this pitch of the venue, or none for games at the venue without a pitch.
refereeId (optional) query Id With this saved referee on the crew.
refereeName (optional) query string With this name typed in on the crew (ignoring case).
ageGroup (optional) query string
month (optional) query string Kicking off in this month, 2026-04, locally.
Answer Body When
200 CSV The games, as a download (Content-Disposition: attachment).
400 ApiError A filter isn’t valid.
401 ApiError Not signed in, the session has ended, or the API key is unknown or has expired.
403 ApiError An API key without matches:read; or the account is waiting for approval (USR-4).
413 ApiError The request is too big.
429 ApiError Too many tries from this address; the Retry-After header says how many seconds to wait.

Where the account is signed in.

Where the account is signed in. Browsers and linked watches, most recently active first. Apps name themselves with an X-LawFive-Device header: garmin, and from the watch app’s later versions what it runs on, garmin; model=006-B3703-00; firmware=12.30, which names it (Garmin Venu 2), and its own version, app=2026.10.30 (VER-3). Not with API keys.

Answer Body When
200 { devices: Device[] } The devices.
401 ApiError Not signed in, the session has ended, or the API key is unknown or has expired.
403 ApiError An API key: this needs signing in; or the account is waiting for approval (USR-4).
413 ApiError The request is too big.
429 ApiError Too many tries from this address; the Retry-After header says how many seconds to wait.

Sign a device out. Unlinks a watch or signs a browser out; its next request gets 401. Not with API keys.

Parameter In Type Description
id path string
Answer Body When
200 { ok: true } Signed out.
401 ApiError Not signed in, the session has ended, or the API key is unknown or has expired.
403 ApiError An API key: this needs signing in; or the account is waiting for approval (USR-4).
404 ApiError Not found, or not this user’s.
413 ApiError The request is too big.
429 ApiError Too many tries from this address; the Retry-After header says how many seconds to wait.

Which matches are on which watch. For a match’s page to say whether it’s on a watch (SYNC-14): each match in each linked watch’s latest download of its next matches (GET /v1/watch/matches, from the watch or through the phone app with the watch’s link), and whether the watch has it as it is now. A watch unlinked drops out; a match deleted isn’t listed. Phones fetch it as they sync and keep the last answer for when there’s no signal. Not with API keys. Not with API keys.

Answer Body When
200 { matches: MatchOnWatch[] } The matches on watches, newest download first.
401 ApiError Not signed in, the session has ended, or the API key is unknown or has expired.
403 ApiError An API key: this needs signing in; or the account is waiting for approval (USR-4).
413 ApiError The request is too big.
429 ApiError Too many tries from this address; the Retry-After header says how many seconds to wait.

Sign out everywhere else. Ends the account’s other sessions, keeping the one asking (SEC-7): other browsers only, or every other device, which unlinks watches and signs phones out. Not with API keys.

Body: SignOutOthers

Answer Body When
200 { signedOut: integer } How many were signed out.
400 ApiError Not browsers or all.
401 ApiError Not signed in, the session has ended, or the API key is unknown or has expired.
403 ApiError An API key: this needs signing in; or the account is waiting for approval (USR-4).
413 ApiError The request is too big.
429 ApiError Too many tries from this address; the Retry-After header says how many seconds to wait.

Say how to reach this phone. Keeps the handle the push relay gave this phone with its session, so signing out removes it (PUSH-4). Needs an X-LawFive-Device header. Not with API keys. Only when GET /v1/meta lists push. Not with API keys.

Body: PushHandle

Answer Body When
200 { ok: true } Kept.
400 ApiError The body doesn’t match, or there’s no device header.
401 ApiError Not signed in, the session has ended, or the API key is unknown or has expired.
403 ApiError An API key: this needs signing in; or the account is waiting for approval (USR-4).
413 ApiError The request is too big.
429 ApiError Too many tries from this address; the Retry-After header says how many seconds to wait.

What an integration did. The matches an API key added or changed, from a match-changed notification’s id (NOTE-3), for the phone to write the notification itself. Kept for 30 days. Not with API keys. Only when GET /v1/meta lists push. Not with API keys.

Parameter In Type Description
id path Id UUID made by the device that created the record.
Answer Body When
200 MatchNotice The notice.
401 ApiError Not signed in, the session has ended, or the API key is unknown or has expired.
403 ApiError An API key: this needs signing in; or the account is waiting for approval (USR-4).
404 ApiError Not found, not this user’s, or more than 30 days old.
413 ApiError The request is too big.
429 ApiError Too many tries from this address; the Retry-After header says how many seconds to wait.

How much of its storage the account is using. Photos and tracks, and the limit if the account’s plan has one (SEC-6, PLAN-2). Not with API keys.

Answer Body When
200 AccountStorage The figures.
401 ApiError Not signed in, the session has ended, or the API key is unknown or has expired.
403 ApiError An API key: this needs signing in; or the account is waiting for approval (USR-4).
413 ApiError The request is too big.
429 ApiError Too many tries from this address; the Retry-After header says how many seconds to wait.

The account’s plan. Its plan and limits, when it ends and what follows, and how much of each limit it’s using (PLAN-6). Only when GET /v1/meta lists plans. Not with API keys.

Answer Body When
200 AccountPlan The plan.
401 ApiError Not signed in, the session has ended, or the API key is unknown or has expired.
403 ApiError An API key: this needs signing in; or the account is waiting for approval (USR-4).
413 ApiError The request is too big.
429 ApiError Too many tries from this address; the Retry-After header says how many seconds to wait.

Photos on matches, venues and referees (FILE-1 to FILE-5), kept by the server on its disk or in S3-compatible storage. Upload the file as multipart/form-data, ideally already made smaller (the web app sends at most 2000 pixels on the longest side, as a JPEG without its location and camera details) with a thumbnail; the server keeps what it’s sent. The files are private: fetch them with the same sign-in. Deleting the record deletes its attachments. Only a signed-in session can use them for now.

Attachments of a kind of record. In order (by record, then position). With recordId, just that record’s; with first, only each record’s first (a referee’s picture). Not with API keys.

Parameter In Type Description
recordType query AttachmentRecordType The kinds of record that can have attachments.
recordId (optional) query Id
first (optional) query “true” Only each record’s first attachment.
Answer Body When
200 { attachments: Attachment[] } The attachments.
400 ApiError recordType or recordId isn’t valid.
401 ApiError Not signed in, the session has ended, or the API key is unknown or has expired.
403 ApiError An API key: this needs signing in; or the account is waiting for approval (USR-4).
413 ApiError The request is too big.
429 ApiError Too many tries from this address; the Retry-After header says how many seconds to wait.

Upload an attachment. Added after the record’s others. At most 30 on a record. Not with API keys.

Body: multipart/form-data: the fields of AttachmentUpload, and the files file (required: the photo, a JPEG, PNG or WebP of at most 10 MB (or the plan’s photoMb)), thumb (optional: a small version, at most 1 MB, for lists and galleries)

Answer Body When
201 Attachment Uploaded.
400 { error: string, issues?: ValidationIssue[] } With storage ({ usedBytes, limitBytes }) when it doesn’t fit the account’s room (FILE-7). A field isn’t valid, the file is missing, too big or not a JPEG, PNG or WebP, or the record has 30 already.
401 ApiError Not signed in, the session has ended, or the API key is unknown or has expired.
403 ApiError An API key: this needs signing in; or the account is waiting for approval (USR-4).
404 ApiError No such record: save it first.
413 ApiError The request is too big.
429 ApiError Too many tries from this address; the Retry-After header says how many seconds to wait.

Change an attachment. Its caption, or its place among the record’s attachments (moving a referee’s photo to 0 makes it their picture). Not with API keys.

Parameter In Type Description
id path Id UUID made by the device that created the record.

Body: AttachmentChange

Answer Body When
200 Attachment Changed.
400 { error: string, issues?: ValidationIssue[] } The body doesn’t match.
401 ApiError Not signed in, the session has ended, or the API key is unknown or has expired.
403 ApiError An API key: this needs signing in; or the account is waiting for approval (USR-4).
404 ApiError Not found, or not this user’s.
413 ApiError The request is too big.
429 ApiError Too many tries from this address; the Retry-After header says how many seconds to wait.

Replace an attachment’s file. A photo cropped or turned by the app, in place of the old one (FILE-8): it keeps its id, place and caption, and the old files are deleted. Its url and thumbUrl change, so a kept copy of the old one isn’t shown. Not with API keys.

Parameter In Type Description
id path Id UUID made by the device that created the record.

Body: multipart/form-data: the fields of { width?: string, height?: string }, and the files file (required: the photo, a JPEG, PNG or WebP of at most 10 MB (or the plan’s photoMb)), thumb (optional: a small version, at most 1 MB)

Answer Body When
200 Attachment Replaced.
400 { error: string, issues?: ValidationIssue[] } With storage ({ usedBytes, limitBytes }) when a bigger file doesn’t fit the account’s room (FILE-7). The file is missing, too big or not a JPEG, PNG or WebP.
401 ApiError Not signed in, the session has ended, or the API key is unknown or has expired.
403 ApiError An API key: this needs signing in; or the account is waiting for approval (USR-4).
404 ApiError Not found, or not this user’s.
413 ApiError The request is too big.
429 ApiError Too many tries from this address; the Retry-After header says how many seconds to wait.

Delete an attachment. With its files, for good. Not with API keys.

Parameter In Type Description
id path Id UUID made by the device that created the record.
Answer Body When
200 { ok: true } Deleted.
401 ApiError Not signed in, the session has ended, or the API key is unknown or has expired.
403 ApiError An API key: this needs signing in; or the account is waiting for approval (USR-4).
404 ApiError Not found, or not this user’s.
413 ApiError The request is too big.
429 ApiError Too many tries from this address; the Retry-After header says how many seconds to wait.

An attachment’s file. As uploaded. A file at an address never changes, so it can be kept (Cache-Control: private, immutable); replacing it (FILE-8) gives the attachment new addresses, with ?v=. Not with API keys.

Parameter In Type Description
id path Id UUID made by the device that created the record.
Answer Body When
200 An image The image.
401 ApiError Not signed in, the session has ended, or the API key is unknown or has expired.
403 ApiError An API key: this needs signing in; or the account is waiting for approval (USR-4).
404 ApiError Not found, or not this user’s.
413 ApiError The request is too big.
429 ApiError Too many tries from this address; the Retry-After header says how many seconds to wait.

An attachment’s thumbnail. The small version, or the file itself when none was uploaded. Not with API keys.

Parameter In Type Description
id path Id UUID made by the device that created the record.
Answer Body When
200 An image The image.
401 ApiError Not signed in, the session has ended, or the API key is unknown or has expired.
403 ApiError An API key: this needs signing in; or the account is waiting for approval (USR-4).
404 ApiError Not found, or not this user’s.
413 ApiError The request is too big.
429 ApiError Too many tries from this address; the Retry-After header says how many seconds to wait.

Everything in the account as one zip (DATA-2), and restoring one into an account (DATA-4), for backups and moving between servers. The zip has lawfive.json (what it is), README.md, records/.json (each an array of records as the API gives them), tracks/.json, attachments.json and the photos in attachments/. Only a signed-in session can use them.

Download everything. Every record, track and photo in the account, as a zip (Content-Disposition: attachment). It’s made as it’s sent, so it starts straight away however big it is. Not with API keys.

Answer Body When
200 A zip file The zip.
401 ApiError Not signed in, the session has ended, or the API key is unknown or has expired.
403 ApiError An API key: this needs signing in; or the account is waiting for approval (USR-4).
413 ApiError The request is too big.
429 ApiError Too many tries from this address; the Retry-After header says how many seconds to wait.

Restore from an export. Merges an export into the account: records keep their ids, anything missing is added, and where the account has a record too, whichever was changed last is kept. Tracks are added to match reports without one; photos the account doesn’t have are added. At most 2 GB. Matches, referees and venues over the plan’s limits (PLAN-4) aren’t added, and problems says so. Not with API keys.

Body: A zip made by GET /v1/export (application/zip)

Answer Body When
200 RestoreResult Restored.
400 ApiError It isn’t a LawFive export, or it’s too big (2 GB, or the plan’s fileMb).
401 ApiError Not signed in, the session has ended, or the API key is unknown or has expired.
403 ApiError An API key: this needs signing in; or the account is waiting for approval (USR-4).
409 ApiError Another restore for the account is under way: one at a time.
413 ApiError The request is too big.
429 ApiError Too many tries from this address; the Retry-After header says how many seconds to wait.

A match’s share link (DATA-7): an address nobody can guess that shows the match, its log and its track to anyone who has it, without signing in. Never the notes, fee, crew, contacts, player names or photos.

A match’s share link. Not with API keys.

Parameter In Type Description
id path Id UUID made by the device that created the record.
Answer Body When
200 MatchShare Its link.
401 ApiError Not signed in, the session has ended, or the API key is unknown or has expired.
403 ApiError An API key: this needs signing in; or the account is waiting for approval (USR-4).
404 ApiError It isn’t shared, or there’s no such match.
413 ApiError The request is too big.
429 ApiError Too many tries from this address; the Retry-After header says how many seconds to wait.

Share a match. Makes its link, or a new one in place of the old, which stops working. Not with API keys.

Parameter In Type Description
id path Id UUID made by the device that created the record.
Answer Body When
201 MatchShare Its link.
401 ApiError Not signed in, the session has ended, or the API key is unknown or has expired.
403 ApiError An API key: this needs signing in; or the account is waiting for approval (USR-4).
404 ApiError No such match.
413 ApiError The request is too big.
429 ApiError Too many tries from this address; the Retry-After header says how many seconds to wait.

Stop sharing a match. Its link stops working. Not with API keys.

Parameter In Type Description
id path Id UUID made by the device that created the record.
Answer Body When
200 { ok: true } Not shared any more.
401 ApiError Not signed in, the session has ended, or the API key is unknown or has expired.
403 ApiError An API key: this needs signing in; or the account is waiting for approval (USR-4).
404 ApiError It isn’t shared.
413 ApiError The request is too big.
429 ApiError Too many tries from this address; the Retry-After header says how many seconds to wait.

A shared match. What the share link’s page shows. The token is the secret; there’s no other sign-in. Answers carry X-Robots-Tag: noindex. No sign-in needed.

Parameter In Type Description
token path string The token in the share link.
Answer Body When
200 SharedMatch The match.
403 ApiError The account is suspended (USR-7).
404 ApiError No match has this link: it was replaced or turned off, or the match was deleted.
413 ApiError The request is too big.
429 ApiError Too many tries from this address; the Retry-After header says how many seconds to wait.

A shared match’s track. With its figures, as the owner sees them. No sign-in needed.

Parameter In Type Description
token path string The token in the share link.
Answer Body When
200 { track: MatchTrack, summary: TrackSummary, pitch?: PitchOutline, heat?: { cols: integer, rows: integer, lengthM: number, widthM: number, total: number[], periods: ({ period: integer, cells: number[] })[] }, updatedAt: Timestamp } The track and its figures.
403 ApiError The account is suspended (USR-7).
404 ApiError No match has this link, or it has no track.
413 ApiError The request is too big.
429 ApiError Too many tries from this address; the Retry-After header says how many seconds to wait.

URLs that hear about changes to the account’s records (HOOK-1 to HOOK-5): matches, match reports, venues, referees, competitions and templates, however they were made (the web app, the API, a watch, an import). Each delivery is a signed JSON POST (WebhookPayload), retried up to five times over about three hours when it doesn’t get a 2xx answer. Only a signed-in session can manage them.

The account’s webhooks. Newest first, each with its latest delivery. Not with API keys.

Answer Body When
200 { webhooks: Webhook[] } The webhooks.
401 ApiError Not signed in, the session has ended, or the API key is unknown or has expired.
403 ApiError An API key: this needs signing in; or the account is waiting for approval (USR-4).
413 ApiError The request is too big.
429 ApiError Too many tries from this address; the Retry-After header says how many seconds to wait.

Add a webhook. As many as the account’s plan allows (PLAN-2). The answer has the secret deliveries are signed with, which isn’t shown again. Not with API keys.

Body: WebhookInput

Answer Body When
201 CreatedWebhook Added.
400 { error: string, issues?: ValidationIssue[] } The body doesn’t match, or the address isn’t allowed (a private network, unless the server allows it).
401 ApiError Not signed in, the session has ended, or the API key is unknown or has expired.
403 PlanRefusal An API key, or the account already has as many webhooks as its plan allows (PLAN-4).
413 ApiError The request is too big.
429 ApiError Too many tries from this address; the Retry-After header says how many seconds to wait.

Change a webhook. Its address, topics, or pausing it (active: false). Not with API keys.

Parameter In Type Description
id path Id UUID made by the device that created the record.

Body: WebhookInput

Answer Body When
200 Webhook Changed.
400 { error: string, issues?: ValidationIssue[] } The body doesn’t match, or the address isn’t allowed.
401 ApiError Not signed in, the session has ended, or the API key is unknown or has expired.
403 ApiError An API key: this needs signing in; or the account is waiting for approval (USR-4).
404 ApiError Not found, or not this user’s.
413 ApiError The request is too big.
429 ApiError Too many tries from this address; the Retry-After header says how many seconds to wait.

Delete a webhook. With its deliveries; nothing more is sent. Not with API keys.

Parameter In Type Description
id path Id UUID made by the device that created the record.
Answer Body When
200 { ok: true } Deleted.
401 ApiError Not signed in, the session has ended, or the API key is unknown or has expired.
403 ApiError An API key: this needs signing in; or the account is waiting for approval (USR-4).
404 ApiError Not found, or not this user’s.
413 ApiError The request is too big.
429 ApiError Too many tries from this address; the Retry-After header says how many seconds to wait.

A webhook’s deliveries. The latest 50, newest first. Not with API keys.

Parameter In Type Description
id path Id UUID made by the device that created the record.
Answer Body When
200 { deliveries: WebhookDelivery[] } The deliveries.
401 ApiError Not signed in, the session has ended, or the API key is unknown or has expired.
403 ApiError An API key: this needs signing in; or the account is waiting for approval (USR-4).
404 ApiError Not found, or not this user’s.
413 ApiError The request is too big.
429 ApiError Too many tries from this address; the Retry-After header says how many seconds to wait.

Send a test. Sends an example delivery now, once, and says how it went. The example is the account’s latest record of the topic, or a made-up one; test is true in it. Not with API keys.

Parameter In Type Description
id path Id UUID made by the device that created the record.

Body: { topic?: WebhookTopic }

Answer Body When
200 { delivery: WebhookDelivery, payload: WebhookPayload, durationMs: integer } Sent (whatever the answer).
401 ApiError Not signed in, the session has ended, or the API key is unknown or has expired.
403 ApiError An API key: this needs signing in; or the account is waiting for approval (USR-4).
404 ApiError Not found, or not this user’s.
413 ApiError The request is too big.
429 ApiError Too many tries from this address; the Retry-After header says how many seconds to wait.

Keys for scripts and integrations (KEY-1 to KEY-4), made in the web app’s Settings. A key is sent like a session token, Authorization: Bearer , and can use only the collections it was given (read, or read and write), plus the weather (with matches:read) and address search (with venues:write). Keys can’t manage devices or other keys, and only a signed-in session can make or delete them.

The account’s API keys. Newest first. The keys themselves aren’t kept, only their first characters. Not with API keys.

Answer Body When
200 { keys: ApiKey[] } The keys.
401 ApiError Not signed in, the session has ended, or the API key is unknown or has expired.
403 ApiError An API key: this needs signing in; or the account is waiting for approval (USR-4).
413 ApiError The request is too big.
429 ApiError Too many tries from this address; the Retry-After header says how many seconds to wait.

Make an API key. As many as the account’s plan allows (PLAN-2). Not with API keys.

Body: NewApiKey

Answer Body When
201 CreatedApiKey Made: the answer has the key, which isn’t shown again.
400 { error: string, issues?: ValidationIssue[] } The body doesn’t match.
401 ApiError Not signed in, the session has ended, or the API key is unknown or has expired.
403 PlanRefusal An API key, or the account already has as many keys as its plan allows (PLAN-4).
413 ApiError The request is too big.
429 ApiError Too many tries from this address; the Retry-After header says how many seconds to wait.

Delete an API key. It stops working straight away. Not with API keys.

Parameter In Type Description
id path Id UUID made by the device that created the record.
Answer Body When
200 { ok: true } Deleted.
401 ApiError Not signed in, the session has ended, or the API key is unknown or has expired.
403 ApiError An API key: this needs signing in; or the account is waiting for approval (USR-4).
404 ApiError Not found, or not this user’s.
413 ApiError The request is too big.
429 ApiError Too many tries from this address; the Retry-After header says how many seconds to wait.

Change an API key. Whether the user’s phones are told when it adds or changes matches (NOTE-1). Its access and expiry can’t change: make a new key instead. Not with API keys.

Parameter In Type Description
id path Id UUID made by the device that created the record.

Body: ApiKeyChange

Answer Body When
200 ApiKey Changed.
400 { error: string, issues?: ValidationIssue[] } The body doesn’t match.
401 ApiError Not signed in, the session has ended, or the API key is unknown or has expired.
403 ApiError An API key: this needs signing in; or the account is waiting for approval (USR-4).
404 ApiError Not found, or not this user’s.
413 ApiError The request is too big.
429 ApiError Too many tries from this address; the Retry-After header says how many seconds to wait.

What a Garmin watch uses, shaped for its little memory: matches in the form it starts one from, and finished matches as it saved them.

The next matches, for the watch. Scheduled matches from three hours ago on, soonest first, so one that’s about to start or running late is still there. A watch’s link asking is noted as the watch now having these matches (SYNC-14, GET /v1/devices/matches), unless it’s the phone app fetching them ahead for the watch (prefetch). Not with API keys.

Parameter In Type Description
limit (optional) query integer At most 10 (the default).
prefetch (optional) query “true” The phone app fetching the watch’s next matches ahead (APP-7), for when the watch asks without signal: not noted as on the watch.
Answer Body When
200 { matches: WatchMatch[], serverVersion?: string, screen?: WatchScreenChange, teamSwipes?: TeamSwipes, misconductScheme?: SchemeId } The matches.
401 ApiError Not signed in, the session has ended, or the API key is unknown or has expired.
403 ApiError An API key: this needs signing in; or the account is waiting for approval (USR-4).
413 ApiError The request is too big.
429 ApiError Too many tries from this address; the Retry-After header says how many seconds to wait.

Upload a finished match. Stored as a match record, and its planned match marked played. A quick match (no matchId) gets a match made for it from what the watch knew. Sending the same match again updates it, unless its result has been corrected since (RES-4) or it has been deleted since (RES-8): then nothing changes. A planned match deleted before its result first arrives comes back, played. Not with API keys.

Body: WatchUpload

Answer Body When
200 WatchUploadResult Updated: this match was sent before (or kept as it is: corrected or deleted since).
201 WatchUploadResult Stored.
400 ValidationError The body doesn’t match.
401 ApiError Not signed in, the session has ended, or the API key is unknown or has expired.
403 ApiError An API key: this needs signing in; or the account is waiting for approval (USR-4).
409 ApiError It would duplicate another record.
413 ApiError The request is too big.
429 ApiError Too many tries from this address; the Retry-After header says how many seconds to wait.

Upload part of a match’s track. The watch sends the track it recorded (TRK-7) a chunk at a time, after the match. Once every chunk and the match are in, the chunks become the match report’s track, unless it has one already (from a file, say), which is kept. Sending a chunk again replaces it. Not with API keys.

Body: WatchTrackChunk

Answer Body When
200 WatchTrackResult Received.
400 ValidationError The body doesn’t match, or the samples can’t be read.
401 ApiError Not signed in, the session has ended, or the API key is unknown or has expired.
403 ApiError An API key: this needs signing in; or the account is waiting for approval (USR-4).
413 ApiError The request is too big.
429 ApiError Too many tries from this address; the Retry-After header says how many seconds to wait.

Pitch corners from the watch. Corners walked on the watch (TRK-13), sent at its next sync (SYNC-12). They’re kept with the match, replacing any it had, and with its venue’s pitch when it has one, for its matches from then on. Not with API keys.

Body: WatchPitchMarking

Answer Body When
200 { ok: true, pitch: boolean } Kept.
400 { error: string, issues?: ValidationIssue[] } The body doesn’t match, or it names neither matchId nor created.
401 ApiError Not signed in, the session has ended, or the API key is unknown or has expired.
403 ApiError An API key: this needs signing in; or the account is waiting for approval (USR-4).
404 ApiError No such match.
413 ApiError The request is too big.
429 ApiError Too many tries from this address; the Retry-After header says how many seconds to wait.

The match screen chosen on the watch. Settings > Match screen on the watch (UI-24), sent at its next sync. It becomes the account’s choice unless the website or a phone has changed it since (the latest change wins). An account with no settings yet keeps nothing. Not with API keys.

Body: WatchScreenChange

Answer Body When
200 { applied: boolean } Whether this change is now the account’s choice.
400 ValidationError The body doesn’t match.
401 ApiError Not signed in, the session has ended, or the API key is unknown or has expired.
403 ApiError An API key: this needs signing in; or the account is waiting for approval (USR-4).
413 ApiError The request is too big.
429 ApiError Too many tries from this address; the Retry-After header says how many seconds to wait.

Mark a fee paid from the watch. The watch’s Paid toggle (SYNC-11), sent at its next sync. It changes only whether the fee is paid, and only if the match hasn’t been changed since (the latest change wins). Not with API keys.

Body: WatchFeeChange

Answer Body When
200 { paid: boolean, applied: boolean } Whether the fee is paid now, and whether this change made it so.
400 ValidationError The body doesn’t match.
401 ApiError Not signed in, the session has ended, or the API key is unknown or has expired.
403 ApiError An API key: this needs signing in; or the account is waiting for approval (USR-4).
404 ApiError No such match, or it has no fee.
413 ApiError The request is too big.
429 ApiError Too many tries from this address; the Retry-After header says how many seconds to wait.

Address search for placing venues on the map.

Find an address. Through Nominatim (OpenStreetMap) by default, at most one lookup a second for the whole server, with answers cached. Every search counts against the account’s plan’s address searches a day (PLAN-11), cached or not. Only when GET /v1/meta lists geocode. API keys: need venues:write.

Parameter In Type Description
q query string The address.
Answer Body When
200 { results: Place[] } Up to five places.
400 ApiError q is shorter than 3 or longer than 200 characters.
401 ApiError Not signed in, the session has ended, or the API key is unknown or has expired.
403 ApiError An API key without venues:write; or the account is waiting for approval (USR-4).
404 ApiError Address search is off on this server (an admin can switch it on, ADM-2).
413 ApiError The request is too big.
429 ApiError The account’s plan allows no more address searches today (PLAN-11), or two of its searches are already waiting; Retry-After says how many seconds to wait.
502 ApiError The address search service didn’t answer.

The Laws of the Game quiz’s questions (QUIZ-1).

The quiz’s questions. Every active question in a language, with the Laws and protocols they refer to, for the app to pick from. The phone apps keep the last copy, for no signal. Only when GET /v1/meta lists quiz. Not with API keys.

Parameter In Type Description
language (optional) query string A two-letter language code; en by default.
Answer Body When
200 QuizBank The questions.
401 ApiError Not signed in, the session has ended, or the API key is unknown or has expired.
403 ApiError An API key: this needs signing in; or the account is waiting for approval (USR-4).
404 ApiError There are no questions in that language.
413 ApiError The request is too big.
429 ApiError Too many tries from this address; the Retry-After header says how many seconds to wait.

Forecasts for the user’s matches, and the weather finished matches were played in (WX-1 to WX-8). The server fetches and keeps forecasts; apps never call the weather service themselves.

Weather for matches in a window. For the user’s matches kicking off between from and to that have a place on the map and a forecast (up to 16 days ahead) or kept weather (once played). Only when GET /v1/meta lists weather. API keys: need matches:read.

Parameter In Type Description
from (optional) query string ISO 8601 time; 30 days ago by default.
to (optional) query string ISO 8601 time; 17 days from now by default. At most 400 days after from.
Answer Body When
200 { items: MatchWeather[] } The weather, one per match that has any.
400 ApiError from or to isn’t a time, or they’re the wrong way round or too far apart.
401 ApiError Not signed in, the session has ended, or the API key is unknown or has expired.
403 ApiError An API key without matches:read; or the account is waiting for approval (USR-4).
413 ApiError The request is too big.
429 ApiError Too many tries from this address; the Retry-After header says how many seconds to wait.

One match’s weather. Only when GET /v1/meta lists weather. API keys: need matches:read.

Parameter In Type Description
matchId path Id UUID made by the device that created the record.
Answer Body When
200 MatchWeather The weather.
401 ApiError Not signed in, the session has ended, or the API key is unknown or has expired.
403 ApiError An API key without matches:read; or the account is waiting for approval (USR-4).
404 ApiError No weather for this match: not found, no place on the map, or too far ahead.
413 ApiError The request is too big.
429 ApiError Too many tries from this address; the Retry-After header says how many seconds to wait.

The API’s own types. Records and weather are in the data model: Match, MatchTemplate, MatchRecord, Venue, Competition, Referee, UserSettings and MatchWeather, each with a JSON Schema file in JSON Schema. The <Record>Input types are those records without updatedAt and deletedAt, and with id and version optional.

The signed-in account, with what it may do.

Field Type Description
id string
name string
email string
status AccountStatus
admin boolean Can invite and approve people (USR-1).
pendingEmail (optional) string A new address waiting for its owner to follow the link emailed to it (USR-9); email stays the old one until then.
pendingEmailUntil (optional) Timestamp When that link stops working.

An account’s name (USR-10): 1 to 100 characters, spaces at either end trimmed.

string

The signed-in account’s plan, when it ends, and what it’s using (PLAN-6).

Field Type Description
plan PlanName
limits PlanLimits
since Timestamp
endsAt (optional) Timestamp When it ends: the end of endsOn in the account’s time zone.
endsOn (optional) string Its last day.
then (optional) PlanName The plan that follows when it ends.
usage PlanUsage

pending: made on a server that approves new accounts, and not approved yet (USR-4). It can sign in, but only GET /v1/account answers. suspended: an admin suspended it (USR-7); it can’t sign in, and its API keys and calendar feed stop working, but its data is kept.

“active” | “pending” | “suspended”

Field Type Description
usedBytes integer Photos and tracks on this account.
limitBytes (optional) integer How much it may keep (SEC-6); absent when this server sets no limit.

Every LawFive error (outside /v1/auth) has this shape; some add fields.

Field Type Description
error string What went wrong, in words a person can read.

A key for scripts and integrations, with only the access it was given (KEY-1).

Field Type Description
id Id
name string What it’s for, “RefSix import”.
prefix string The start of the key, to recognize it: lf_ and 8 characters.
scopes ApiKeyScope[]
createdAt Timestamp
lastUsedAt (optional) Timestamp To within five minutes; none until it’s used.
expiresAt (optional) Timestamp None for a key that doesn’t expire.
notify boolean Whether the user’s phones are told when this key adds a match, or cancels, postpones, moves or removes one that hasn’t kicked off (NOTE-1).

What can change on a key after it’s made.

Field Type Description
notify boolean Tell the user’s phones when this key adds or changes matches (NOTE-1).

:read, or :write (which includes reading it).

“matches:read” | “matches:write” | “match-templates:read” | “match-templates:write” | “match-records:read” | “match-records:write” | “venues:read” | “venues:write” | “competitions:read” | “competitions:write” | “referees:read” | “referees:write” | “settings:read” | “settings:write”

A code that signs a phone in to this account. It works once, for two minutes.

Field Type Description
id string For GET /v1/app-sign-in/{id}.
code string Eight letters and digits.
link string What the QR code holds: the web app’s Get the app page with the code in its fragment, which a phone’s camera can open.
appLink string The same as a lawfive:// link, which opens the app.
server string The address to sign in at, for someone typing the code.
expiresAt Timestamp

Whose the code is, so the phone can ask before signing in.

Field Type Description
name string
email string
expiresAt Timestamp
Field Type Description
code string
Field Type Description
token string The session token, sent from then on as Authorization: Bearer .
user { id: string, name: string, email: string, status: AccountStatus }
Field Type Description
status “waiting” | “used” | “expired”
deviceName (optional) string The phone that used it, when it said which.

An assignor’s calendar link, read into the account’s matches every 30 minutes (FEED-1 to FEED-9).

Field Type Description
id Id
name string
system AssignorSystem
link string The start of the link, to recognize it by: the whole link is a password, so it’s never sent back (FEED-2).
notify boolean Tell the account’s phones when it adds, moves or cancels a match (FEED-8, NOTE-1).
active boolean false: paused, by the person or after the link stopped working (FEED-9); not read.
createdAt Timestamp
lastReadAt (optional) Timestamp When it was last read, whatever happened.
lastChangedAt (optional) Timestamp When a read last added or changed a match.
lastResult (optional) { games: integer, added: integer, updated: integer, canceled: integer } What the latest read that worked found and did.
problem (optional) string Why the latest read didn’t work, or why it’s paused.
stoppedAt (optional) Timestamp When it was paused because the link stopped working (FEED-9).
skipped AssignorCalendarSkipped[] Events the latest read couldn’t save, at most 20.
Field Type Description
name (optional) string
notify (optional) boolean
active (optional) boolean false pauses it; true starts reading it again, straight away.
url (optional) string A new link, such as one the assignor system made again (FEED-9); read straight away.

A game in an assignor’s calendar, as it would be saved.

Field Type Description
title string Home v Away, or the match’s name.
kickoff Timestamp
timeZone (optional) string IANA time zone name, for example America/Los_Angeles.
venue (optional) string
inLawFive boolean A match already in LawFive at this kickoff, which the calendar takes on without changing (FEED-5).
Field Type Description
url string The calendar link: https, http, or webcal (read as https).
system AssignorSystem

What a calendar link holds, before connecting it; nothing is saved (FEED-1).

Field Type Description
calendarName (optional) string What the calendar calls itself (X-WR-CALNAME).
games integer Games still to come.
from (optional) Timestamp The first one’s kickoff.
to (optional) Timestamp The last one’s kickoff.
inLawFive integer Of those, how many are already in LawFive at the same kickoff.
first AssignorCalendarEvent[] The first three, as they’d be saved.
skipped AssignorCalendarSkipped[] Events that couldn’t be read, at most 20.

An event that couldn’t be read or saved, and why (FEED-9).

Field Type Description
title string The event’s summary, or what’s known of it.
kickoff (optional) Timestamp
reason string

Which system a calendar link is from: ArbiterSports, Assignr, HorizonWebRef, RefTown, GotSport, Spordle, or another (FEED-1). Each is read the same way until it has rules of its own (FEED-5).

“arbiter” | “assignr” | “horizon” | “reftown” | “gotsport” | “spordle” | “other”

A photo on a match, venue or referee (FILE-1). The files are private: fetching them needs the same sign-in as the rest of the API.

Field Type Description
id Id
recordType AttachmentRecordType
recordId Id The match, venue or referee it’s on.
contentType “image/jpeg” | “image/png” | “image/webp” What the file is, worked out from its contents.
size integer The file’s size in bytes.
width (optional) integer Its width in pixels, when the upload gave it.
height (optional) integer
caption (optional) string
position integer Its place among the record’s attachments, from 0. A referee’s first photo is their picture.
createdAt Timestamp
url string Where to fetch the file: /v1/attachments/{id}/file, with the same sign-in.
thumbUrl string A small version for lists and galleries: /v1/attachments/{id}/thumb (the file itself when none was uploaded).

What to change about an attachment; leave out what stays the same.

Field Type Description
caption (optional) string | null The new caption; null or an empty string removes it.
position (optional) integer Move it to this place among the record’s attachments (0 is first; a larger number moves it to the end).

The kinds of record that can have attachments.

“match” | “venue” | “referee”

The form fields of an upload (multipart/form-data), beside the files: file (a JPEG, PNG or WebP of at most 10 MB) and, optionally, thumb (a small version, at most 1 MB).

Field Type Description
recordType AttachmentRecordType
recordId Id
caption (optional) string
width (optional) integer The image’s width in pixels.
height (optional) integer

An error from the account endpoints.

Field Type Description
message string
code string For example INVALID_EMAIL_OR_PASSWORD or INVALID_ORIGIN.

It may have more fields.

The account’s private calendar address (CAL-1). Anyone with it can see the matches in it; make a new one to stop the old one working.

Field Type Description
url string The feed (https), for calendar apps that ask for an address.
webcalUrl string The same feed as webcal://, which opens the calendar app to subscribe.
createdAt Timestamp
lastFetchedAt (optional) Timestamp When a calendar app last fetched it, to within an hour.

A record as it is now, after a change: created, updated or deleted (deletedAt set).

  • { collection: “matches”, record: Match }
  • { collection: “match-templates”, record: MatchTemplate }
  • { collection: “match-records”, record: MatchRecord }
  • { collection: “venues”, record: Venue }
  • { collection: “competitions”, record: Competition }
  • { collection: “referees”, record: Referee }
  • { collection: “settings”, record: UserSettings }

One page of the changes after a cursor, for sync.

Field Type Description
changes Change[] Oldest first. A record changed several times appears once, as it is now.
cursor integer Send as since next time. Unchanged when there’s nothing new.
more boolean true when there are more changes after this page; ask again straight away.

A new key, with the key itself. The server keeps only a hash, so it can’t show it again.

Field Type Description
id Id
name string What it’s for, “RefSix import”.
prefix string The start of the key, to recognize it: lf_ and 8 characters.
scopes ApiKeyScope[]
createdAt Timestamp
lastUsedAt (optional) Timestamp To within five minutes; none until it’s used.
expiresAt (optional) Timestamp None for a key that doesn’t expire.
notify boolean Whether the user’s phones are told when this key adds a match, or cancels, postpones, moves or removes one that hasn’t kicked off (NOTE-1).
key string The key itself, shown only this once: send it as Authorization: Bearer .

A new webhook, with the secret its deliveries are signed with.

Field Type Description
id Id
url string Where deliveries are POSTed.
topics WebhookTopic[] Which records’ changes it hears about.
active boolean Paused webhooks get no deliveries.
createdAt Timestamp
lastDelivery (optional) { at: Timestamp, status: “pending” | “delivered” | “failed”, responseCode?: integer } The latest delivery and how it went.
secret string For checking a delivery’s X-LawFive-Signature, shown only this once.

Somewhere the account is signed in: a browser or a linked watch.

Field Type Description
id string The session’s id, for DELETE /v1/devices/{id}.
kind string browser, or the app’s name from its X-LawFive-Device header (garmin).
name string For people: “Garmin Venu 2” (“Garmin watch” when it didn’t say which), “Chrome on macOS”.
model (optional) string A watch’s Garmin part number, “006-B3703-00”, when it gives one.
firmware (optional) string A watch’s firmware version, “12.30”, when it gives one.
appVersion (optional) string The version of LawFive on it (VER-3), “2026.10.30”, from the app= in its X-LawFive-Device header; missing for browsers and apps from before it.
linkedAt Timestamp When it signed in or was linked.
lastActiveAt Timestamp
current boolean true for the device making this request.
watchOnly (optional) boolean true for a watch’s link, which only syncs matches (SEC-13); absent for everything else.

A code for linking a watch (RFC 8628).

Field Type Description
device_code string Secret; the watch polls with it.
user_code string Eight letters and digits without look-alikes, shown on the watch.
verification_uri string The web app’s Link page.
verification_uri_complete string The Link page with the code filled in, for a QR code.
expires_in integer Seconds (600).
interval integer Seconds to wait between polls (5).

The watch’s token once someone has approved its code.

Field Type Description
access_token string Send as Authorization: Bearer .
token_type “Bearer”
expires_in integer
scope string

It may have more fields.

Likely duplicates across all of the user’s venues or referees (DUP-1, DUP-7).

Field Type Description
groups ({ reasons: string[], members: DuplicateMember[] })[] Groups that are probably one venue or person, surest first.
of integer How many venues or referees there are in all.

A venue or referee as the duplicate finder and merge preview show it (DUP-2, DUP-7).

Field Type Description
id Id
name string
aliases string[] Its other names (DUP-6, DUP-9).
address (optional) string A venue’s.
pitches (optional) integer A venue’s number of pitches.
email (optional) string A referee’s.
phone (optional) string A referee’s.
matches MatchCount
reasons (optional) string[] Why it’s likely the same as the one asked about (“Same address”, “Same name, in a different order”), surest first; for the likeliest only.
Field Type Description
email string
password string The account’s password, to confirm it’s them.

Following the link that confirms a new email address (USR-9).

Field Type Description
token string From the link emailed to the new address: /confirm-email/.

A row as a spreadsheet has it: column names (Home, home_colour and homeColour are the same column) and text, number or true/false values. Empty values change nothing.

{}

It may have more fields.

Field Type Description
row integer The row’s place in rows, from 1.
action “add” | “update” | “skip” skip: it has problems, so it wasn’t imported.
matchId (optional) Id The match it added or updated (or would).
title (optional) string The match in a few words, for showing the row: home v away.
kickoff (optional) Timestamp Its kickoff, once imported.
problems string[] Why it’s skipped, naming the column; empty when it isn’t.

An invitation to make an account, even when sign-up is closed (USR-3). It works once, for 14 days.

Field Type Description
id Id
email (optional) string Who it’s for; only this address can use it. None for a link anyone can use once.
name (optional) string Their name, to fill in the sign-up form.
link string The link to send: the web app’s invitation page.
createdBy string The name of the admin who made it.
createdAt Timestamp
expiresAt Timestamp
usedAt (optional) Timestamp
usedBy (optional) string The name of the account made with it.
emailedAt (optional) Timestamp When it was emailed, if it was.
plan (optional) PlanName The plan the account joins (PLAN-5); the default (or the sign-up trial) when there’s none.
planDays (optional) integer For how many days, then the default plan; none: it doesn’t end.

What the invitation page shows before someone signs up with it.

Field Type Description
email (optional) string The address it’s for; the sign-up form uses it and doesn’t let it be changed.
name (optional) string
invitedBy string
expiresAt Timestamp

The venues or referees most likely the same as one (Merge with…, DUP-1, DUP-7).

Field Type Description
likely DuplicateMember[] At most six, likeliest first, each with its reasons.

How many matches a venue or referee has, or several as if merged.

Field Type Description
played integer
upcoming integer Scheduled matches.
last (optional) Timestamp The latest played match’s kickoff.

Matches to add or update, from a spreadsheet, CSV or JSON file (IMP-1 to IMP-8).

Field Type Description
rows ImportRow[]
timeZone (optional) string For kickoffs written without a UTC offset, when the row has no Time zone column; the web app sends the browser’s. UTC when not given.
dateOrder (optional) “dmy” | “mdy” How to read dates written with slashes or dots (7/11/2026); ISO dates (2026-11-07) need none. mdy when not given.
dryRun (optional) boolean Check every row and say what would happen, without saving anything (IMP-5).

The form fields of a file import (multipart/form-data), beside the file.

Field Type Description
timeZone (optional) string For kickoffs written without a UTC offset, when the row has no Time zone column.
dateOrder (optional) “dmy” | “mdy” How to read dates written with slashes or dots (7/11/2026); ISO dates (2026-11-07) are always understood.
dryRun (optional) “true” | “false” true: check every row and say what would happen, without saving anything (IMP-5).

MatchImportResult, for a file (IMP-2).

Field Type Description
rows ImportedRow[] Every row that had anything in it, in order.
added integer
updated integer
skipped integer
created { venues: string[], competitions: string[], referees: string[] } Names the user had no record for, added (or, on a dry run, to be added) (IMP-4).
ignoredColumns string[] Columns the import doesn’t know, as written in the file.
file { kind: “table” | “json”, lines: integer[] } How to point at a row in the file.
Field Type Description
rows ImportedRow[] Every row that had anything in it, in order.
added integer
updated integer
skipped integer
created { venues: string[], competitions: string[], referees: string[] } Names the user had no record for, added (or, on a dry run, to be added) (IMP-4).
ignoredColumns string[] Columns the import doesn’t know, as written in the file.

What an integration (an API key, or an assignor calendar) did to the user’s matches, collected over about a minute, for a phone to write as a notification (NOTE-3).

Field Type Description
id Id
source string Who made the changes: the API key’s name, which is usually the integration’s, or the assignor calendar’s (FEED-8).
createdAt Timestamp
matches ({ matchId: Id, changes: MatchNoticeChange[], title: string, kickoff?: Timestamp, previousKickoff?: Timestamp, timeZone?: string, venue?: string, previousVenue?: string })[] The matches, soonest first.

added: a new match; canceled, postponed; backOn: scheduled again after being canceled or postponed; kickoff: moved to another time; venue: moved to another venue; removed: deleted.

“added” | “canceled” | “postponed” | “backOn” | “kickoff” | “venue” | “removed”

A match on one of the account’s linked watches (SYNC-14): in the watch’s latest download of its next matches.

Field Type Description
matchId Id
deviceId string The watch’s link, as GET /v1/devices gives it (the session’s id).
kind string garmin, wearos or applewatch, from the watch’s X-LawFive-Device header; watch when it didn’t say.
name string For people: “Garmin Venu 2”, “Galaxy Watch4”, “Apple Watch Series 9” (“Garmin watch” when it didn’t say which).
since Timestamp When the watch first got the match as it has it now.
syncedAt Timestamp The watch’s latest download, which still had the match.
upToDate boolean false when what the watch shows of the match has changed since (teams, kickoff, format, venue, officials, notes, fee), or the match is no longer scheduled: the watch’s next sync brings the change, or takes the match off.

A match’s share link (DATA-7).

Field Type Description
url string The public page: PUBLIC_URL/s/. Anyone with it can see the match.
createdAt Timestamp

A new name for the signed-in account (USR-10); nothing else can be changed this way.

Field Type Description
name AccountName

How the server tells visitors apart, for its limits on tries (SEC-2, SEC-14). Both settings are environment values only.

Field Type Description
yourAddress string The address this server sees the request coming from, after believing the trusted proxies.
trustedProxies string[] TRUSTED_PROXIES: the proxies believed about the visitor’s address; empty, none.
trustedIpHeader (optional) string TRUSTED_IP_HEADER, when set.
Field Type Description
name string
scopes ApiKeyScope[] At least one.
expiresInDays (optional) integer Leave out for a key that doesn’t expire.
notify (optional) boolean Tell the user’s phones when this key adds or changes matches (NOTE-1); off when left out.
Field Type Description
url string The calendar link: https, http, or webcal (read as https).
system AssignorSystem
name string
notify (optional) boolean true when left out (FEED-8).
Field Type Description
email (optional) string Who it’s for; only this address can use it.
name (optional) string Their name, to fill in the sign-up form.
send (optional) boolean Email it to them (needs email set up on the server, and an email).
plan (optional) string The plan the account joins (PLAN-5).
planDays (optional) integer For how many days, then the default plan.

Venues or referees the user says are different, so they aren’t suggested together again (DUP-5).

Field Type Description
others Id[] Venues or referees that aren’t the same as this one, nor as each other.

An error from the linking endpoints, as RFC 8628 has them.

Field Type Description
error string authorization_pending, slow_down, expired_token, access_denied, invalid_request, invalid_client, invalid_grant or unauthorized.
error_description (optional) string

It may have more fields.

An account on this server, as an admin sees it (USR-1).

Field Type Description
id string
name string
email string
status AccountStatus
admin boolean
adminFromSetting boolean An admin because the server’s ADMIN_EMAILS lists them, which the web app can’t change.
joinedAt Timestamp
lastActiveAt (optional) Timestamp When one of their devices last used the account; none if never signed in since joining.
invitedBy (optional) string The name of the admin whose invitation they joined with.
plan PersonPlan Their plan (PLAN-3).
lockedUntil (optional) Timestamp Signing in is refused until then, after too many wrong passwords (SEC-12); Unlock ends it now.
Field Type Description
code string A plan’s short handle, unique on the server, to look it up by (PLAN-1): free, pro, friends-family.
name string
endsAt (optional) Timestamp
endsOn (optional) string
then (optional) PlanName

A place found by address search.

Field Type Description
name string The full address.
lat number
lon number

A named set of limits (PLAN-1).

Field Type Description
code string A plan’s short handle, unique on the server, to look it up by (PLAN-1): free, pro, friends-family.
name string
limits PlanLimits
default boolean New accounts join it, and accounts given no plan are in it.
people integer Accounts in it now.
inUse boolean Something refers to it (an account, an account’s next plan, an unused invitation, the sign-up trial, or it’s the default), so it can’t be deleted.

Putting an account in a plan (PLAN-3).

Field Type Description
plan string A plan’s short handle, unique on the server, to look it up by (PLAN-1): free, pro, friends-family.
endsOn (optional) string Its last day, in the account’s time zone; none: it doesn’t end.
then (optional) string The plan after it ends; the default when left out.
Field Type Description
from (optional) PlanName None for the first entry.
to PlanName
at Timestamp
reason “sign-up” | “invite” | “admin” | “ended” sign-up: joined (a trial, or the default then); invite: joined with an invitation’s plan; admin: an admin changed it; ended: the plan before it ended.
by (optional) string The admin’s name, for reason admin.
endsAt (optional) Timestamp When this one was set to end.
Field Type Description
code string A plan’s short handle, unique on the server, to look it up by (PLAN-1): free, pro, friends-family.
name string
limits PlanLimits

What a plan allows (PLAN-2).

Field Type Description
matches (optional) integer Matches kept, not counting deleted ones; matches recorded on a watch are never refused. Left out: no limit; 0: none at all.
referees (optional) integer Referees kept. Left out: no limit; 0: none at all.
venues (optional) integer Venues kept. Left out: no limit; 0: none at all.
storageMb (optional) integer Megabytes of photos and tracks (SEC-6); a track recorded on a watch is never refused. Left out: no limit; 0: none at all.
photoMb (optional) integer The largest photo, in megabytes; the server’s own most, 10 MB, still applies. Left out: no limit; 0: none at all.
fileMb (optional) integer The largest file brought in (an import or a restore), in megabytes; the server’s own most for each (5 MB and 2 GB) still applies. Left out: no limit; 0: none at all.
devices (optional) integer Watches and phones signed in at once (PLAN-7); browsers don’t count. Left out: no limit; 0: none at all.
pushPerDay (optional) integer Phone notifications a day (from midnight UTC), across all the account’s phones (PLAN-8). Left out: no limit; 0: none at all.
apiKeys (optional) integer API keys (KEY-1). Left out: no limit; 0: none at all.
apiCallsPerMinute (optional) integer Requests made with the account’s API keys a minute, all its keys together (PLAN-8). Left out: no limit; 0: none at all.
apiCallsPerDay (optional) integer Requests made with the account’s API keys a day (from midnight UTC), all its keys together (PLAN-8). Left out: no limit; 0: none at all.
webhooks (optional) integer Webhooks (HOOK-1). Left out: no limit; 0: none at all.
assignorCalendars (optional) integer Assignor calendars (FEED-10). Left out: no limit; 0: none at all.
addressSearchesPerDay (optional) integer Address searches a day (from midnight UTC), for placing venues on the map (PLAN-11). Left out: no limit; 0: none at all.
Field Type Description
code string A plan’s short handle, unique on the server, to look it up by (PLAN-1): free, pro, friends-family.
name string

An ApiError; over a plan’s limit, it says which (PLAN-4).

Field Type Description
error string
limit (optional) “matches” | “referees” | “venues” | “storageMb” | “photoMb” | “fileMb” | “devices” | “pushPerDay” | “apiKeys” | “apiCallsPerMinute” | “apiCallsPerDay” | “webhooks” | “assignorCalendars” | “addressSearchesPerDay” The plan’s limit that was reached (PLAN-4).
allowed (optional) integer What the plan allows of it.
Field Type Description
default (optional) string The plan new accounts join.
signUpTrial (optional) SignUpTrial | null null: no trial.

How much of each limit the account is using; per-minute and per-file limits aren’t counted.

Field Type Description
matches integer
referees integer
venues integer
storageMb number Photos and tracks, in megabytes.
devices integer Watches and phones signed in.
pushPerDay integer Notifications sent today (UTC).
apiKeys integer
apiCallsPerDay integer Requests with the account’s API keys today (UTC).
webhooks integer
assignorCalendars (optional) integer Absent from older servers.
addressSearchesPerDay (optional) integer Address searches today (UTC); absent from older servers.
Field Type Description
plans Plan[] By name.
signUpTrial (optional) SignUpTrial
Field Type Description
handle string | null What the push relay gave this phone (PUSH-2), which only it can read; null to stop notifications.

Referees to merge into one (DUP-3, DUP-4).

Field Type Description
from Id[] The referees to merge into this one; they’re deleted.
name string The name to keep: one of the referees’ names or other names. The rest become other names.
email (optional) string When they have different emails, the one to keep; otherwise the one there is.
phone (optional) string When they have different phone numbers, the one to keep; otherwise the one there is.

A referee merge as chosen so far, to preview (DUP-4).

Field Type Description
from Id[] The referees to merge into this one; they’re deleted.
name (optional) string The name chosen so far; left out, or not one of theirs, the kept referee’s.
email (optional) string The email chosen so far, where they differ.
phone (optional) string The phone number chosen so far, where they differ.

What merging these referees would make (DUP-4, DUP-11).

Field Type Description
referees DuplicateMember[] The referee kept, then the others, in the order sent; each with its reasons when it’s among the likeliest.
names string[] Every name to choose from: theirs and their other names, once each. The rest become other names.
name string The name that will be kept.
emails string[] Their emails, once each: a choice when there’s more than one.
email (optional) string
phones string[] Their phone numbers, once each: a choice when there’s more than one.
phone (optional) string
together MatchCount Their matches as one referee, crew typed in under one of their names included; a match counts once.
merge RefereeMerge What to send to POST /v1/referees/{id}/merge to merge exactly this.
Field Type Description
referee Referee The referee they were merged into, as saved.
merged integer How many referees were merged into it (and deleted).
matchesUpdated integer Matches whose crew now names it, including crew typed as one of its names.

A link an admin sends someone who forgot their password, for servers without email (USR-7).

Field Type Description
link string The web app’s page for choosing a new password. It works once.
expiresAt Timestamp

What restoring an export did (DATA-4). Records the account already has a newer copy of are unchanged; running it again adds nothing.

Field Type Description
records {} By collection: matches, venues, referees…
tracks { added: integer, updated: integer, unchanged: integer }
photos { added: integer, updated: integer, unchanged: integer }
problems string[] What couldn’t be restored and why, at most 50.

What this server is and offers, so apps can adapt to older or self-hosted servers.

Field Type Description
product “LawFive”
apiUrl (optional) string The address apps should use for the API, which may differ from the one they came through (APP-13).
apiVersion integer Raised when the API changes in a way older apps can’t follow.
openApiUrl (optional) string Where this server describes its own API as OpenAPI (AGT-3); missing on servers from before it.
version (optional) string The server’s version (VER-1): the day of its last change, “2026.10.30” (“2026.10.30.2” for a second change that day), or “dev” when run from a checkout without git. Missing on servers from before it.
commit (optional) string The commit it was built from, “f5de635”.
capabilities string[] What this server offers: accounts, records, changes, tracks and stats (missing on servers from before them), devices, link, watch, people, invites, calendar and import, then email, geocode, push, notices (with push, from servers that tell phones what integrations did, NOTE-1) and weather when they’re switched on; merging, from servers that find duplicates and preview merges for the apps (DUP-11); plans, from servers with plans and the account’s time zone (PLAN-6, PLAN-9); assignor-calendars, from servers that read assignors’ calendar links (FEED-1). Ignore names you don’t know.
maps { style?: { url: string, darkUrl?: string, attribution: string }, tileUrl: string, attribution: string, satellite?: { tileUrl: string, attribution: string } }
geocoder (optional) { url: string } The address search service behind GET /v1/geocode, to credit it.
weather (optional) { source: string } Whose weather data GET /v1/weather uses, to credit it.
firstAccount (optional) true Nobody has an account on this server yet (USR-8): the first one can be made whatever signUp says, with the setup code when setupCodeNeeded, and it’s the server’s admin.
setupCodeNeeded (optional) true The first account needs the code in the server’s logs (ADM-5).
setupPending (optional) true The first account hasn’t finished setting the server up (ADM-5).
publicUrl (optional) string The server’s own address (PUBLIC_URL), for the website to say if it isn’t HTTPS (ADM-5, ADM-6).
signUp (optional) SignUpMode Who can make an account (USR-2); missing on servers from before it, which were open or closed.

A server setting as the admin page shows it (ADM-1).

Field Type Description
key “signUp” | “smtpUrl” | “mailFrom” | “mapStyleUrl” | “mapStyleDarkUrl” | “mapStyleAttribution” | “mapTileUrl” | “mapAttribution” | “mapSatelliteUrl” | “mapSatelliteAttribution” | “geocoderUrl” | “weatherUrl” | “weatherHistoryUrl” | “weatherApiKey” | “weatherDailyLimit” | “webhookAllowPrivate”
env string Its environment variable.
source “environment” | “admin” | “default” environment: set there, so fixed; admin: set in the web app; default: neither.
value (optional) string The value in use, when it’s set and not a secret.
secret (optional) true Never sent back: set says whether it has a value.
set (optional) boolean

Settings for the whole server that admins change in the web app (ADM-1, ADM-2), sending only those that change. One the environment sets is fixed there and can’t be changed.

Field Type Description
signUp (optional) SignUpMode | null Who can make an account (USR-2, SIGN_UP).
smtpUrl (optional) string | null Email through an SMTP server: smtp://user:password@host:587 or smtps://…:465. A secret: never sent back (SMTP_URL). null or “” clears it, back to the default.
mailFrom (optional) string | null The sender, “LawFive lawfive@example.com” (MAIL_FROM). null or “” clears it, back to the default.
mapStyleUrl (optional) string | null A MapLibre style for the map, or off for raster tiles (MAP_STYLE_URL). null or “” clears it, back to the default.
mapStyleDarkUrl (optional) string | null The style for dark mode (MAP_STYLE_DARK_URL). null or “” clears it, back to the default.
mapStyleAttribution (optional) string | null The style’s credit, as HTML (MAP_STYLE_ATTRIBUTION). null or “” clears it, back to the default.
mapTileUrl (optional) string | null Raster tiles, with {z}, {x} and {y} (MAP_TILE_URL). null or “” clears it, back to the default.
mapAttribution (optional) string | null The tiles’ credit, as HTML (MAP_ATTRIBUTION). null or “” clears it, back to the default.
mapSatelliteUrl (optional) string | null Satellite tiles, with the provider’s key (MAP_SATELLITE_URL). null or “” clears it, back to the default.
mapSatelliteAttribution (optional) string | null Their credit, as HTML (MAP_SATELLITE_ATTRIBUTION). null or “” clears it, back to the default.
geocoderUrl (optional) string | null Address search (Nominatim-compatible), or off (GEOCODER_URL). null or “” clears it, back to the default.
weatherUrl (optional) string | null Forecasts (Open-Meteo-compatible), or off (WEATHER_URL). null or “” clears it, back to the default.
weatherHistoryUrl (optional) string | null Past weather for played matches, or off (WEATHER_HISTORY_URL). null or “” clears it, back to the default.
weatherApiKey (optional) string | null For a paid plan. A secret: never sent back (WEATHER_API_KEY). null or “” clears it, back to the default.
weatherDailyLimit (optional) string | null Weather calls a day at most (WEATHER_DAILY_LIMIT). null or “” clears it, back to the default.
webhookAllowPrivate (optional) string | null true lets webhooks reach private and local addresses (WEBHOOK_ALLOW_PRIVATE). null or “” clears it, back to the default.

A signed-in device.

Field Type Description
id string
userId string
token string The bearer token.
expiresAt string 90 days after it was last renewed; a session is renewed once a day while it’s used.
createdAt string
updatedAt string

It may have more fields.

What a match’s share link shows (DATA-7): never its notes, fee, crew, contacts, player names or photos.

Field Type Description
match Match Only what a public page shows: without its notes, outlook, fee, crew, tags, match number, assignor or the ids of its venue, pitch, competition and template.
record (optional) MatchRecord Its match report, when it’s been played, without notes on events or team officials’ names.
competition (optional) string The competition’s name.
venue (optional) string The venue’s name.
pitch (optional) string The pitch’s name.
distanceUnit DistanceUnit The sharer’s, for the figures.
track boolean Whether GET /v1/shared/{token}/track has a track.
Field Type Description
scope “browsers” | “all” browsers: other browsers only, so watches and phones stay linked. all: every other device too.

Who can make an account (USR-2): open, anyone; approval, anyone, but an admin approves the account before it can be used; closed, only people with an invitation (USR-3).

“open” | “approval” | “closed”

New accounts start in this plan for these days, then go to the default (PLAN-5).

Field Type Description
plan string A plan’s short handle, unique on the server, to look it up by (PLAN-1): free, pro, friends-family.
days integer

A LawFive account.

Field Type Description
id string
name string
email string
emailVerified boolean
image (optional) string | null
createdAt string
updatedAt string

It may have more fields.

The body doesn’t match the schema; issues lists each problem.

Field Type Description
error string
issues ValidationIssue[]

One problem with a request body, as Zod reports it.

Field Type Description
code string
path (string | number)[] Where in the body, for example [“home”, “colour”].
message string

It may have more fields.

Venues to merge into one (DUP-7): the venue’s details, and what happens to each of the others’ pitches.

Field Type Description
from Id[] The venues to merge into this one; they’re deleted.
name string The name to keep: one of the venues’ names or other names. The rest become other names.
addressFrom (optional) Id When they have different addresses or parking spots, the venue (this one or one of from) whose address and parking to keep; otherwise the ones there are.
pitches ({ id: Id, into?: Id, name?: string })[] What happens to each pitch of the venues in from: every one, once.

A venue merge as chosen so far, to preview (DUP-7, DUP-8).

Field Type Description
from Id[] The venues to merge into this one; they’re deleted.
name (optional) string The name chosen so far; left out, or not one of theirs, the kept venue’s.
addressFrom (optional) Id The venue whose address and parking were chosen so far, where they differ.
pitches (optional) ({ id: Id, into?: Id, name?: string })[] The pitches the user has chosen for so far; the rest get the suggestion. A name can be empty while it’s being typed.

What merging these venues would make (DUP-7, DUP-8, DUP-11).

Field Type Description
venues DuplicateMember[] The venue kept, then the others, in the order sent; each with its reasons when it’s among the likeliest.
names string[] Every name to choose from: theirs and their other names, once each. The rest become other names.
name string The name that will be kept.
places ({ venueId: Id, address?: string, onMap: boolean })[] Their different addresses and parking spots, once each: a choice when there’s more than one.
addressFrom (optional) Id The venue whose address and parking will be kept.
pitches ({ id: Id, name: string, venue?: string, matches: integer, corners: integer, onMap: boolean, check: boolean, why: string, choice: { into?: Id, name?: string }, targets: ({ id: Id, name: string })[], problem?: string })[] Every pitch of the venues merged away, riskiest first, then more matches and corners walked first (DUP-8). The order depends only on the venues, so rows stay put while the choices change.
notes string[] Things to know, such as corners walked over the 20 a pitch keeps.
problems integer Pitches that need a look before merging.
together MatchCount Their matches, at one venue.
merge (optional) VenueMerge What to send to POST /v1/venues/{id}/merge to merge exactly this; none while there are problems.
Field Type Description
venue Venue The venue they were merged into, as saved.
merged integer How many venues were merged into it (and deleted).
matchesUpdated integer Matches that now point at it, and at its pitches.

[code, team, period, seconds, player, extra]. code: G goal, GO own goal, GP penalty, GF direct free kick, Y caution, Y2 second caution, R send-off, SB a sin bin, with its length in seconds in extra, SB2 a second sin bin for the same number, which counts as a send-off (EVT-12), TY and TR a team official’s caution and send-off, with the role in extra (0 head coach, 1 assistant coach, 2 other), I injury, S stoppage, CK and CA the match clock and the added time corrected during play (TMR-14), with the change in seconds in extra (negative for back), E end of period, A abandoned, with the reason in extra (0 weather, 1 lightning, 2 crowd trouble, 3 not enough players, 4 other), SU play suspended, with the reason in extra as for A (TMR-13), RS play resumed, with the whole seconds suspended in extra, both at the period time where play stopped, PG and PM shoot-out kick scored and missed. team: 0 home, 1 away, -1 neither. period counts from 0. seconds: into the period, or -1 before kickoff, -2 at a break, -3 after full time, -4 in the shoot-out. player: shirt number or null. extra: a stoppage’s length in seconds.

[string, integer, integer, integer, integer | null, integer | null]

A match’s fee marked paid or not paid on the watch (SYNC-11).

Field Type Description
matchId Id
paid boolean
at integer When it was changed on the watch, in seconds since 1970.

A match in the form the Garmin watch starts one from (its setup keys), plus what its list shows. Watches have little memory, so the server does the converting.

Field Type Description
id Id
kickoff integer | null Seconds since 1970 (UTC).
title string “STR v ROV”, for the watch’s list.
name string | null The match’s name, or its competition’s.
where string | null Venue and pitch, “Twin Creeks, Field 3”.
role string | null Referee, AR1, AR2 or 4th official.
weather string | null A short forecast in the user’s temperature unit, “54° rain” (WX-7).
fee (optional) { text: string, paid: boolean } | null The fee, for the match’s screen and its Paid toggle (UI-19); absent from servers before it.
crew (optional) ([string, string])[] The other officials, [role, name]: [“AR1”, “Sam Rivera”].
notes (optional) string | null The match’s notes, shortened to 300 characters for the watch’s memory.
home string Short name shown on the watch.
away string
homeName string
awayName string
homeColor integer 0xRRGGBB, or -1 for none.
awayColor integer
numPeriods integer
periodSec integer
halftimeSec integer The break between periods.
teamSize integer
midBreak boolean A break halfway through each period (for example a water break).
breakLenSec integer
extraTime boolean
etPeriods integer
etPeriodSec integer
goldenGoal boolean A goal in extra time ends the match (ET-6).
penalties boolean
clockStyle 0 | 1 | 2 For older watch builds: the account’s match clock (TMR-11), 0 Match time, 1 each period from 0, 2 time left. Newer ones take it from the match screen.
trackStoppage boolean
stoppingClock (optional) boolean The match clock stops while play is stopped, with no added time (TMR-15); trackStoppage is on with it. Absent from servers before it.
stopForGoalsCards (optional) boolean With a stopping clock, a goal or card recorded stops it (TMR-15).
breakStoppage boolean The halfway break’s time is added on as stoppage (TMR-12).
goalScorers boolean Ask the scorer’s number for each goal (EVT-10).
goalTypes boolean
sinBins boolean Sin bin in the team menu (EVT-12).
sinBinSec integer A sin bin’s length, 10 minutes unless the format says.
misconductScheme (optional) SchemeId The codes for a card’s reason on the watch (EVT-14): the competition’s, else the account’s.

A pitch’s corners walked on the watch (TRK-13, SYNC-12).

Field Type Description
matchId (optional) Id A planned match’s id.
created (optional) integer Or, for a quick match, when it was started on the watch (it has no id until it’s uploaded).
corners ([integer, integer])[] [latitude, longitude] in millionths of a degree, as the watch’s tracks are: its JSON keeps only six significant figures of a decimal.
at integer When they were marked, in seconds since 1970.

The match screen as chosen on the watch, or as it’s sent to one (UI-21, UI-24); the latest change wins.

Field Type Description
layout WatchLayout The nearest old layout. A watch that sends only this and secondClock is a build from before the four choices (UI-21).
secondClock ClockStyle
clocks (optional) WatchClocks
mainClock (optional) ClockStyle
breakClock (optional) BreakClockStyle
edge (optional) WatchEdge
names (optional) boolean
at integer When it was changed on the watch, in seconds since 1970.

Part of a match’s track as the watch recorded it (TRK-7): GPS and heart rate about once a second during the periods.

Field Type Description
created integer The match’s created time, as in its upload: which match the track is for.
start integer When the track starts (the first kickoff), in seconds since 1970.
index integer Which chunk this is, from 0.
count integer How many chunks the track has.
device (optional) string The watch model, “venu2”.
data string The chunk’s samples as the watch packs them, “dt,dlat,dlon,speed,hr;” each: tenths of a second since the sample before (empty for 10; the chunk’s first counts from start), millionths of a degree moved since the chunk’s position before (the first is whole; both empty without a new fix), the speed the watch measured in cm/s, and beats a minute. Empty fields mean unknown; trailing ones are left off.
Field Type Description
received integer How many of the track’s chunks the server has.
count integer
track “waiting” | “saved” | “kept” | “dropped” waiting: for more chunks or the match; saved: the track is on the match report; kept: the match report already had a track (from a file, say), which stays; dropped: nothing in it could be used (no positions or heart rate).

A finished match as the watch saved it. The server stores it as a match record.

Field Type Description
created integer When the match was started on the watch, in seconds since 1970.
matchId (optional) Id | null The planned match it was started from; none for a quick match.
device (optional) string The watch model, “venu2”.
home string
away string
homeName (optional) string
awayName (optional) string
homeColor (optional) integer
awayColor (optional) integer
numPeriods integer
periodSec integer
halftimeSec integer
teamSize (optional) integer
breakAtSec (optional) integer When the halfway break comes, or -1 for none.
breakLenSec (optional) integer
extraTime (optional) boolean
etPeriods (optional) integer
etPeriodSec (optional) integer
goldenGoal (optional) boolean The format’s golden goal (ET-6).
penalties (optional) boolean
clockStyle (optional) integer
trackStoppage (optional) boolean
stoppingClock (optional) boolean The format’s stopping clock (TMR-15): every event’s seconds are the clock’s, which stood still during stoppages (S).
stopForGoalsCards (optional) boolean
breakStoppage (optional) boolean
goalScorers (optional) boolean
goalTypes (optional) boolean
sinBins (optional) boolean The format’s sin bins (EVT-12).
sinBinSec (optional) integer
score [integer, integer] [home, away].
pens (optional) [integer, integer] | null Shoot-out score, [home, away].
golden (optional) boolean It ended on a golden goal in extra time (ET-6).
distanceM (optional) number | null How far the referee ran, in meters.
startedAt (optional) integer[] When each period kicked off, in seconds since 1970, for the track’s figures (TRK-3).
kickoff (optional) integer The team that kicked off the first period: 0 home, 1 away, -1 not chosen.
homeHeading (optional) number | null The compass heading of the goal the home team attacked in the first period, in radians from true north, when the referee recorded it (PRE-8).
events WatchEvent[]
misconduct (optional) { scheme: SchemeId, codes: ([integer, string])[] } The cards’ reasons chosen on the watch (EVT-14); a code that isn’t the scheme’s for that card is left out.
diagnostics (optional) { battery?: ([integer, integer, number])[], app?: string, firmware?: string, gpsFirstFixSec?: integer, gpsSamples?: integer, gpsMissed?: integer, restarts?: integer } How the recording went, for tracing problems (DIAG-1).
Field Type Description
recordId Id The match record. The same every time this match is sent, so sending it again updates it.
matchId Id The planned match, or the one made for a quick match.

A URL that hears about changes to the account’s records (HOOK-1).

Field Type Description
id Id
url string Where deliveries are POSTed.
topics WebhookTopic[] Which records’ changes it hears about.
active boolean Paused webhooks get no deliveries.
createdAt Timestamp
lastDelivery (optional) { at: Timestamp, status: “pending” | “delivered” | “failed”, responseCode?: integer } The latest delivery and how it went.

One change sent to a webhook, with how it went.

Field Type Description
id Id Also sent as X-LawFive-Delivery.
event string match.created, matchRecord.updated, venue.deleted…
status “pending” | “delivered” | “failed” pending: waiting for its next try; failed: gave up after five tries.
attempts integer
responseCode (optional) integer
error (optional) string Why the last try failed, when it didn’t get an answer.
test boolean Sent with Send test.
createdAt Timestamp
deliveredAt (optional) Timestamp
Field Type Description
url string http or https.
topics WebhookTopic[]
active (optional) boolean true when left out.

What each delivery POSTs, as JSON. It’s signed: X-LawFive-Signature is t=,v1=<hex HMAC-SHA256 of “.” with the webhook’s secret>.

Field Type Description
id Id The delivery’s id; the same on every retry, so a receiver can ignore one it has had.
event string .<created|updated|deleted>
createdAt Timestamp When the change was made.
test (optional) boolean true for Send test; data is then an example.
data {} The record as stored (as GET returns it); for deleted, its id, version and deletedAt.

The kinds of record a webhook can hear about: matches, match reports (results), venues, referees, competitions and match templates.

“match” | “matchRecord” | “venue” | “referee” | “competition” | “matchTemplate”