Skip to main content

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.

  1. Create an integration credential. An org admin with the integrations.credential.manage permission opens the Kestrel web app → Admin → Integrations and creates a credential of kind fivem — not generic_api_key, the bridge endpoints reject those. The clientSecret is shown exactly once and cannot be recovered later, only rotated.

  2. 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 (expiresIn is 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 generic 401 so a bad clientId can't be distinguished from a bad clientSecret by response alone.

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

  4. Call the bridge endpoints. Every request carries Authorization: Bearer <accessToken> and a JSON body containing identifiers (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:

  1. The credential must be fivem-kind (carries a department).
  2. The caller's identifiers must already be linked to a Kestrel character.
  3. That character must hold a department membership in the credential's own department.
  4. 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/link happen before any of this applies. /mdt-handoff is 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.
tip

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.