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.
  • 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
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/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
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}/suspended Suspend an account, or undo it
POST /v1/people/{id}/reset-link Make a password reset link
DELETE /v1/people/{id} Delete an account
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
POST /v1/server-settings/test-email Send a test email
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/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
POST /v1/devices/sign-out-others Sign out everywhere else
GET /v1/account/storage How much of its storage the account is using
GET /v1/attachments Attachments of a kind of record
POST /v1/attachments Upload an attachment
PUT /v1/attachments/{id} Change an attachment
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
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. 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.

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.

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 (or an address in ADMIN_EMAILS) can make one; with SIGN_UP=approval the account is pending until an admin approves it (USR-2 to USR-4). No sign-in needed.

Body: { name: string, 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 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, or the account is suspended (ACCOUNT_SUSPENDED, USR-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.

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. 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.

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. Not with API keys.

Body: EmailChange

Answer Body When
200 Account Changed.
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.

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.

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).
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.
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 ApiError An API key without matches:write; or the account is waiting for approval (USR-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. 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 centre 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). 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.

Suspend an account, or undo it. A suspended account can’t sign in, and is signed out everywhere (linked watches need linking again afterwards); 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.

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 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.

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.

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 ApiError The account is suspended.
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 } Cancelled.
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 cancelled.
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, competitions and referees are found by name, ignoring case, 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
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: the watch uses the first three letters of the name. 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: the watch uses the first three letters of the name. 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
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: 2 for halves, 4 for quarters. 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 ApiError An API key without matches:write; 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.

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, 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 ApiError An API key without matches:write; 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.

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, cancelled 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. Not open to API keys yet: the answer includes referee and venue names. Not with API keys.

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: 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 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). Not with API keys.

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: 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.

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.

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.

How much of its storage the account is using. Photos and tracks, and the limit if this server has one (SEC-6). 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.

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), 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.

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. Files never change, so they can be kept (Cache-Control: private, immutable). 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. 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.
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.

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. At most 10 per account. 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, the address isn’t allowed (a private network, unless the server allows it), or the account already has 10.
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 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. At most 20 per account. 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, or the account already has 20 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.

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.

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. Not with API keys.

Parameter In Type Description
limit (optional) query integer At most 10 (the default).
Answer Body When
200 { matches: WatchMatch[], serverVersion?: string, screen?: WatchScreenChange } 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. Not with API keys.

Body: WatchUpload

Answer Body When
200 WatchUploadResult Updated: this match was sent before.
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. 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 Too many tries from this address; the Retry-After header 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).

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 recognise 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.

: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.

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 recognise 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.
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.

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.

Field Type Description
email string
password string The account’s password, to confirm it’s them.

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.

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

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.

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
Field Type Description
name string
scopes ApiKeyScope[] At least one.
expiresInDays (optional) integer Leave out for a key that doesn’t expire.
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).

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.

A place found by address search.

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

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.
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.
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 and weather when they’re switched on. 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, and unless ADMIN_EMAILS is set 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” | “accountStorageLimitMb”
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.
accountStorageLimitMb (optional) string | null How many megabytes of photos and tracks each account may keep; none means no limit (ACCOUNT_STORAGE_LIMIT_MB). 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 Without its notes, outlook, fee, crew, tags, match number or the ids of its venue and competition.
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”

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.

[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, S stoppage, E end of period, A abandoned, with the reason in extra (0 weather, 1 lightning, 2 crowd trouble, 3 not enough players, 4 other), 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
penalties boolean
clockStyle 0 | 1 | 2 0 counts up, 1 counts up from zero each period, 2 counts down.
trackStoppage boolean
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

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-24); the latest change wins.

Field Type Description
layout WatchLayout
secondClock ClockStyle
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
penalties (optional) boolean
clockStyle (optional) integer
trackStoppage (optional) boolean
breakStoppage (optional) boolean
goalScorers (optional) boolean
goalTypes (optional) boolean
score [integer, integer] [home, away].
pens (optional) [integer, integer] | null Shoot-out score, [home, away].
distanceM (optional) number | null How far the referee ran, in metres.
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[]
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

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”