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.
| Status | Meaning | Settable via the bridge? |
|---|---|---|
AVAILABLE | On duty, ready for dispatch | Yes — /duty with onDuty: true |
OFF_DUTY | Not working | Yes — /duty with onDuty: false |
EN_ROUTE | Responding to an assignment | No — dispatcher-only |
ON_SCENE | Arrived at an assignment | No direct write — auto-set by /location proximity |
BUSY | Occupied, not dispatchable | No — dispatcher-only |
OUT_OF_SERVICE | Unavailable, still on duty | No — 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¤tVersion=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.