Skip to main content

FiveM Bridge Endpoints

All paths are relative to /api/v1/integrations/fivem unless noted. Every request needs Authorization: Bearer <accessToken> (see Getting a Token) and an identifiers object identifying the player, except where noted.

Unit status & "10-codes"

The wire-level status enum has six values, but the FiveM bridge only exposes an on-duty / off-duty toggle directly — "10-8" and "10-7" map cleanly. Anything finer (en route, on scene, busy, out of service) has no direct write endpoint here; those are reserved for a dispatcher action, plus one automatic case: /location auto-flips a unit to ON_SCENE when its reported coordinates land within the configured arrival radius of an active assignment pin.

StatusMeaningSettable via the bridge?
AVAILABLEOn duty, ready for dispatchYes — /duty with onDuty: true
OFF_DUTYNot workingYes — /duty with onDuty: false
EN_ROUTEResponding to an assignmentNo — dispatcher-only
ON_SCENEArrived at an assignmentNo direct write — auto-set by /location proximity
BUSYOccupied, not dispatchableNo — dispatcher-only
OUT_OF_SERVICEUnavailable, still on dutyNo — dispatcher-only

If your integration needs to set the full status range directly (busy, out of service, en route) rather than just on/off duty, use the API credential instead — its PATCH /units/:unitId/status accepts all six values with no proximity workaround needed.

Endpoints

POST /identity/link — Identity link

Requires an fivem-kind credential only (no prior link).

{
"code": "string",
"identifiers": { "license": "string", "discord": "string", "steam": "string", "fivem": "string" }
}
{ "linked": true, "characterPublicId": "uuid", "displayName": "string" }
// or
{ "linked": false, "reason": "invalid_or_expired_code" }

POST /duty — Duty / status sync

No prior link required — auto-provisions a new player on first call. Only sets AVAILABLE or OFF_DUTY — see the status table above.

New-account creation via this auto-provision path is separately capped per credential (default 20/hour, FIVEM_AUTO_PROVISION_RATE_LIMIT_PER_HOUR) — spam protection against a flood of brand-new accounts, distinct from the route's normal per-minute rate limit below. Hitting it returns the same generic RATE_LIMITED error as any other rate limit; an already-provisioned player calling /duty again is never affected by this cap.

{
"identifiers": { "license": "..." },
"departmentSlug": "string",
"onDuty": true,
"jobLabel": "string",
"gradeLabel": "string",
"characterName": "string"
}
{ "synced": true, "unitId": "uuid", "status": "AVAILABLE" }
// or reason: not_linked | unknown_department_slug | no_department_membership |
// callsign_not_assigned | unit_inactive
{ "synced": false, "reason": "..." }

POST /lookup/vehicle — Plate lookup

Full on-duty gate.

{ "identifiers": { "..." }, "plate": "string" }
{ "found": true, "vehicle": { "plate": "...", "make": "..|null", "model": "..|null", "color": "..|null", "registrationStatus": "string", "ownerCivilianId": "uuid|null", "ownerDisplayName": "..|null" } }
// or reason: not_linked | not_on_duty | no_match
{ "found": false, "reason": "..." }

POST /lookup/person — Person lookup

Full on-duty gate.

{ "identifiers": { "..." }, "firstName": "string", "lastName": "string" }
{ "found": true, "matches": [ { "civilianId": "uuid", "firstName": "...", "lastName": "...", "dateOfBirth": "YYYY-MM-DD|null", "licenseStatus": "string" } ] } // capped at 10
{ "found": false, "reason": "not_linked|not_on_duty|no_match" }

POST /warrant-check — Warrant check

Full on-duty gate. Resolves to exactly one civilian by exact name match, narrowed by DOB if ambiguous — never guesses; returns multiple_matches instead of picking one.

{ "identifiers": { "..." }, "firstName": "string", "lastName": "string", "dateOfBirth": "string" }
{ "checked": true, "hasActiveWarrant": true, "civilianId": "uuid" }
// or reason: not_linked | not_on_duty | no_match | multiple_matches
{ "checked": false, "reason": "..." }

POST /bolo — Issue a BOLO

Full on-duty gate. Auto-creates a civilian/vehicle record if no exact match exists yet.

{
"identifiers": { "..." },
"subject": { "civilian": { "firstName": "...", "lastName": "...", "dateOfBirth": "..." }, "plate": "string" },
"severity": "LOW | MEDIUM | HIGH | ARMED_DANGEROUS",
"scope": "DEPARTMENT | COMMUNITY",
"subjectDescription": "string",
"reason": "string",
"narrative": "string"
}

At least one of subject.civilian / subject.plate is required.

{ "issued": true, "boloId": "uuid", "boloNumber": "string" }
// or
{ "issued": false, "reason": "not_linked|not_on_duty" }

POST /bolo-check — Check active BOLOs

Full on-duty gate.

{ "identifiers": { "..." }, "plate": "string", "firstName": "string", "lastName": "string" }

plate or (firstName + lastName) is required.

{ "checked": true, "matches": [ { "boloId": "uuid", "boloNumber": "string", "severity": "LOW|MEDIUM|HIGH|ARMED_DANGEROUS", "subjectDescription": "string", "reason": "string" } ] } // capped at 10, no internal notes
{ "checked": false, "reason": "not_linked|not_on_duty" }

boloId is included specifically so a hit can be followed up with POST /bolo/:boloId/resolve without a separate lookup.

POST /bolo/:boloId/resolve — Resolve a BOLO

Full on-duty gate; boloId is a URL path param. The caller's on-duty department must match the BOLO's own issuing department. Re-resolving an already-RESOLVED BOLO is idempotent and succeeds.

{ "identifiers": { "..." } }
{ "resolved": true, "boloId": "uuid", "status": "RESOLVED" }
// or reason: not_linked | not_on_duty | bolo_not_found | wrong_department
{ "resolved": false, "reason": "..." }

POST /citation — Issue a citation

Full on-duty gate. civilian.dateOfBirth must be strict YYYY-MM-DD — malformed input is a hard 400, not a discriminated response like the others.

{
"identifiers": { "..." },
"civilian": { "firstName": "...", "lastName": "...", "dateOfBirth": "YYYY-MM-DD" },
"plate": "string",
"locationText": "string",
"statuteCode": "string",
"statuteDescription": "string",
"fineAmount": 0
}
{ "issued": true, "citationId": "uuid", "citationNumber": "string" }
// or
{ "issued": false, "reason": "not_linked|not_on_duty" }

POST /arrest — Book an arrest

Full on-duty gate.

{
"identifiers": { "..." },
"civilian": { "firstName": "...", "lastName": "...", "dateOfBirth": "YYYY-MM-DD" },
"locationText": "string",
"narrative": "string",
"charges": [ { "statuteCode": "string", "statuteDescription": "string" } ]
}

charges needs 1–20 entries.

{ "booked": true, "arrestId": "uuid", "arrestNumber": "string" }
// or
{ "booked": false, "reason": "not_linked|not_on_duty" }

POST /civilian — Civilian sync

No gate — valid credential only. Upserts a civilian record for any player regardless of link/duty status; always succeeds.

{ "identifiers": { "..." }, "civilian": { "firstName": "...", "lastName": "...", "dateOfBirth": "..." } }
{ "synced": true, "civilianId": "uuid", "created": true }

POST /civilian/claim — Civilian claim

Requires identity link (not duty). Same request shape as sync.

{ "claimed": true, "civilianId": "uuid" }
// or
{ "claimed": false, "reason": "not_linked | already_claimed" }

POST /vehicle — Vehicle sync

No gate — valid credential only. Upserts a vehicle record regardless of link/duty status; resolves by exact plate match. An existing vehicle's make/model/color are never overwritten by a later sync.

{ "identifiers": { "..." }, "vehicle": { "plate": "string", "make": "string", "model": "string", "color": "string" } }
{ "synced": true, "vehicleId": "uuid", "created": true }

POST /panic — Panic alert

Full on-duty gate. Creates a P1 CAD call at the given coordinates — doesn't auto-assign the triggering unit, dispatch still has to act on it.

{ "identifiers": { "..." }, "mapX": 0, "mapY": 0 }
{ "triggered": true, "callId": "uuid", "callNumber": "string" }
// or
{ "triggered": false, "reason": "not_linked|not_on_duty" }

POST /911 — Emergency call

Linked only — duty not required. The one endpoint any linked player, civilian or officer, on or off duty, can call.

{ "identifiers": { "..." }, "mapX": 0, "mapY": 0, "description": "string" }
{ "reported": true, "callId": "uuid", "callNumber": "string" }
// or
{ "reported": false, "reason": "not_linked|no_department" }

POST /911/self-assign — Self-assign to a 911 call

Full on-duty gate — unlike /911 itself, this needs the caller's own on-duty unit. Lets the accepting officer's own unit self-assign to a /911 call, without waiting on a dispatcher to notice. Only ever touches the calling credential's own unit — never an arbitrary unitId. On success, atomically assigns the unit, transitions it to EN_ROUTE, and (if the call was still PENDING) transitions the call to ACTIVE.

{ "identifiers": { "..." }, "callId": "uuid" }
{ "assigned": true, "unitId": "uuid", "callStatus": "ACTIVE", "unitStatus": "EN_ROUTE" }
// or reason: not_linked | not_on_duty | call_not_found | already_assigned | wrong_department
{ "assigned": false, "reason": "..." }

POST /911/unassign — Unassign from a 911 call

Full on-duty gate. The counterpart to self-assign — releases the caller's own unit from a call it's currently assigned to and returns the unit to AVAILABLE. Only ever touches the calling credential's own unit.

{ "identifiers": { "..." }, "callId": "uuid" }
{ "unassigned": true, "unitId": "uuid", "unitStatus": "AVAILABLE" }
// or reason: not_linked | not_on_duty | call_not_found | not_assigned | wrong_department
{ "unassigned": false, "reason": "..." }

POST /911/narrative — Append a narrative note to a 911 call

Full on-duty gate. Appends a timeline note to a call — unlike self-assign, this doesn't reject a call that's already CLOSED, so it's safe to use for a late follow-up note.

{ "identifiers": { "..." }, "callId": "uuid", "text": "string" }
{ "updated": true, "eventId": "uuid" }
// or reason: not_linked | not_on_duty | call_not_found | wrong_department
{ "updated": false, "reason": "..." }

POST /location — Location report

Full on-duty gate. Designed for a ~5 second heartbeat per officer. Not persisted — drives the realtime map broadcast, and is the only path that auto-transitions a unit to ON_SCENE within the configured arrival radius of an active assignment pin.

{ "identifiers": { "..." }, "mapX": 0, "mapY": 0 }
{ "reported": true }
// or
{ "reported": false, "reason": "not_linked|not_on_duty" }

POST /waypoint — Waypoint poll

Full on-duty gate. Pure read, no side effects — the way to find out where dispatch wants a unit to go without a WebSocket connection.

{ "identifiers": { "..." } }
{ "hasWaypoint": true, "callId": "uuid", "callNumber": "string", "mapX": 0, "mapY": 0 }
// or reason: not_linked | not_on_duty | no_assignment | no_pin
{ "hasWaypoint": false, "reason": "...", "assignedCallId": "uuid|null", "assignedCallNumber": "..|null" }

POST /mdt-handoff — MDT handoff mint

Mints a single-use, 120-second token that hands a player off into an authenticated MDT web session, exchanged separately at POST /auth/mdt-handoff/exchange. Only relevant if you're building your own in-car MDT bridge. Unlike most bridge endpoints, this one does not require a full on-duty gate — it also grants a read-only 'basic' session to a linked-but-not-on-duty (or not department-matched) player, rather than rejecting them outright.

{
"identifiers": { "..." },
"departmentSlug": "string",
"onDuty": true,
"jobLabel": "string",
"gradeLabel": "string",
"characterName": "string"
}

departmentSlug and onDuty are required; the label/name fields are optional context passed through the same as /duty.

{ "issued": true, "token": "eyJ...", "expiresAt": "ISO 8601", "tier": "basic" | "full" }
// or
{ "issued": false, "reason": "not_linked" }

tier reflects what the minted session can actually do: "full" for a linked, on-duty unit in the credential's department; "basic" (read-only) for anyone else linked. Handle both — a "basic" MDT session shouldn't offer write actions in your own UI.

POST /presence — Concurrent-account presence heartbeat

No on-duty/link gate at all — reports the game server's entire currently- connected player population in one batched call (all their raw identifiers, up to 500), roughly once every 30s (kestrel_cad_presence_heartbeat_interval_ms), regardless of whether each player is linked, on duty, or has a Kestrel account at all. Purely a presence signal renewing an already-linked account's "still connected" claim toward the organisation's concurrent_accounts.max billing limit — an unlinked player reported here is silently skipped, never auto-provisioned (unlike /duty). Fire-and-forget: never disconnects or kicks a player, and there is deliberately no matching "player left" endpoint — a claim only ever self-expires if heartbeats stop.

{ "identifiers": [ { "license": "...", "discord": "...", "steam": "...", "fivem": "..." } ] }
{ "acknowledged": true }

GET /resource-release/latest — Latest resource version check

The one bridge route with no credential required at all — no Authorization header, no identifiers. It's a public, IP-throttled version-check endpoint the official FiveM resources call on their own startup to warn a server owner about an out-of-date install. Only useful if you're distributing your own versioned FiveM resource against Kestrel and want the same self-update-check behavior.

GET /integrations/fivem/resource-release/latest?resourceSlug=kestrel-cad&currentVersion=1.4.0
{ "found": true, "resourceSlug": "kestrel-cad", "latestVersion": "1.5.0", "updateAvailable": true, "manifest": { "...": "..." } }
// or
{ "found": false, "resourceSlug": "kestrel-cad" }

Optional: WebSocket

A Socket.IO namespace at /integrations/fivem pushes waypoint changes to connected clients, authenticated with the same bearer JWT via handshake.auth.token. You don't need this if your client only issues commands — it exists so an in-game client can get instant waypoint pins instead of polling /waypoint. Skip it entirely if your integration never needs to know where dispatch is sending someone.

There's exactly one wire event, named waypoint — not three separate event names. Switch on the payload's own type field to tell the three cases apart:

// type: "assignment.created" or "assignment.updated"
{ "type": "assignment.created", "emittedAt": "ISO 8601", "departmentId": "uuid", "callId": "uuid", "callNumber": "string", "unitId": "uuid", "unitLabel": "string", "mapX": 0, "mapY": 0 }

// type: "assignment.removed" — no coordinates, no callNumber
{ "type": "assignment.removed", "emittedAt": "ISO 8601", "departmentId": "uuid", "callId": "uuid", "unitId": "uuid", "unitLabel": "string" }

mapX/mapY can be null. This channel only ever emits for an organisation whose map is configured for real in-game coordinates (gta_v_tiles) — a static_image organisation's connections never receive anything on it.

See Rate Limits & Security for throttling and the security details worth knowing before you ship an integration against this API, or Errors & Status Codes for what a failed request looks like on the wire.