Getting a Token
There's no self-serve or CLI signup — the first step happens once, in the browser, by an org admin. Everything after that is scriptable.
-
Create an integration credential. An org admin with the
integrations.credential.managepermission opens the Kestrel web app → Admin → Integrations and creates a credential of kindfivem— notgeneric_api_key, the bridge endpoints reject those. TheclientSecretis shown exactly once and cannot be recovered later, only rotated. -
Exchange it for an access token.
POST /integrations/fivem/token{ "clientId": "uuid", "clientSecret": "string" }{ "accessToken": "eyJ...", "tokenType": "Bearer", "expiresIn": 720 }You get back a Bearer JWT good for 12 minutes (
expiresInis in seconds). There's no refresh token — just re-call this endpoint before it expires. Every failure mode (unknown id, wrong secret, revoked credential) returns the same generic401so a badclientIdcan't be distinguished from a badclientSecretby response alone. -
Link the officer's identity, once per player. Every action below that touches a specific officer (a lookup, a BOLO, a citation…) requires that player already be linked via
/integrations/fivem/identity/link, using a one-time code the player generates from their own Kestrel account page. -
Call the bridge endpoints. Every request carries
Authorization: Bearer <accessToken>and a JSON body containingidentifiers(the player's license/discord/steam/fivem id) plus whatever the action needs — see FiveM Bridge Endpoints.
How authorization works
Most bridge endpoints share one gate, re-checked fresh on every single call — nothing is cached, so a player going off duty mid-session locks out their next action immediately:
- The credential must be
fivem-kind (carries a department). - The caller's identifiers must already be linked to a Kestrel character.
- That character must hold a department membership in the credential's own department.
- That membership must have an active unit whose status isn't
OFF_DUTY.
Every failure in that chain — not linked, wrong department, off duty,
whatever — collapses to the same generic reason string (not_linked or
not_on_duty). That's deliberate: your client can't distinguish "never
linked" from "off duty right now" from the response alone, which closes
off using this endpoint to enumerate who's linked or on duty.
A few endpoints deviate from the full gate: /911 only requires linking
(any duty state — a civilian can call 911); /civilian (sync) and
/vehicle (sync) require no gate at all, just a valid credential — either
can be called for any player regardless of link/duty status; and /token
/identity/linkhappen before any of this applies./mdt-handoffis its own special case — see FiveM Bridge Endpoints, it grants a reduced read-only session instead of failing outright when the full gate isn't met.
Treat your clientId/clientSecret and any short-lived access token like
a password — never commit them to source control, and rotate the
credential if you suspect exposure. Kestrel's own design assumes a
credential will leak eventually and limits blast radius accordingly
(short-lived tokens, independent per-credential revocation) rather than
assuming it can't happen.