Skip to main content

API Credential (kind: 'generic_api_key')

The credential kind labeled "API" in Admin → Integrations. Built for "an external system talks to Kestrel directly, no FiveM in the loop" — this is what kestrel-anpr-bridge uses, for example, since a camera network isn't a player. Two structural differences from the FiveM Bridge:

  • Org-wide, not department-scoped. A FiveM credential is tied to one department; an API credential isn't tied to any department — it can read/write across every department in the org, gated only by whichever scopes it was granted.
  • No player identity linking, no on-duty gate. There's no /identity/link step and no requirement that some player be linked and on duty. Every action is attributed directly to the credential itself — shown in the UI as the credential's label (e.g. "ANPR Cameras") rather than an officer's name. Request bodies have no identifiers field — there's no player to identify, the credential itself is the actor.

Creating one

Same one-time-secret flow as a FiveM credential, except: Admin → Integrations → New credential → kind "API". No department picker — instead, a scope checklist. Tick exactly what this credential should be allowed to do (see below), or "Select all" for full read/write across everything currently wired. Keep it to the minimum your integration actually needs — this credential's secret is only as safe as wherever you're running it.

Getting a token

POST /integrations/token
{ "clientId": "uuid", "clientSecret": "string" }
{ "accessToken": "eyJ...", "tokenType": "Bearer", "expiresIn": 720 }

Identical shape to the FiveM Bridge's token exchange — it's the same underlying mechanism, just under a credential-kind-neutral path. The two token routes share one rate-limit budget per caller.

Scope catalog

Every route below requires the matching scope on the credential — nothing is reachable by default, and there's no wildcard/"admin" scope; "Select all" at creation just ticks every box below individually.

GroupScope keyGrants
Unitscad.unit.viewList units, read status
Unitscad.unit.assignChange a unit's status (full enum)
CAD Callscad.call.viewList calls, read call details
CAD Callscad.call.updateChange a call's status or priority
Records · BOLOsrecords.bolo.viewList/search active BOLOs
Records · BOLOsrecords.bolo.createIssue a new BOLO
Records · BOLOsrecords.bolo.manageResolve/update an existing BOLO, or report a camera hit
Records · Vehiclesrecords.vehicle.viewSearch/view vehicle registration ("run plates")
Records · Personsrecords.civilian.viewSearch/view civilian records ("run names")
Records · Warrantsrecords.warrant.viewList/view warrant status

This is a deliberate, growing slice of the general CAD API, not the whole thing — organisation administration and platform-level operations are permanently out of scope for this credential kind. Citations, arrests, properties, and RMS reports aren't wired to this credential kind yet.

Endpoints

All under /api/v1, no identifiers body field. Most responses are the same full object shape a dispatcher would see in the web app, not a narrow bridge-specific summary — BOLOs are the one exception, see below.

MethodPathScope requiredNotes
GET/unitscad.unit.view?departmentId=, ?isActive= (boolean), ?limit= (max 500) — all optional, omit departmentId to list across every department
GET/units/:unitIdcad.unit.view
PATCH/units/:unitId/statuscad.unit.assignBody: { "status": "AVAILABLE"|"EN_ROUTE"|"ON_SCENE"|"BUSY"|"OUT_OF_SERVICE"|"OFF_DUTY", "reason"?: string } — any status may follow any status
GET/cad/callscad.call.view?departmentId=, ?status=, ?limit=, ?offset= (limit max 500) — all optional
GET/cad/calls/:callIdcad.call.view
PATCH/cad/calls/:callId/statuscad.call.updateBody: { "status": "PENDING"|"ACTIVE" }
GET/records/bolosrecords.bolo.view?status=, ?issuingDepartmentId= — plain array response; see the plate gotcha and the summary-shape note below
GET/records/bolos/:boloIdrecords.bolo.view
POST/records/bolosrecords.bolo.createBody: { subjectCivilianId? | subjectVehicleId?, issuingDepartmentId, severity, scope, subjectDescription, reason, narrative?, internalNotes?, expiresAt? }
PATCH/records/bolos/:boloId/statusrecords.bolo.manageBody: { "status": "ISSUED"|"ACKNOWLEDGED"|"HIT_REPORTED"|"RESOLVED"|"ARCHIVED"|"EXPIRED" }
POST/records/bolos/:boloId/hitsrecords.bolo.manageCamera-network hit report. Body: { plate, cameraId, cameraLabel, mapX, mapY, seenAt }boloId is the URL path param. Rejects a civilian-only BOLO, an inactive BOLO, or a plate that doesn't match the BOLO's linked vehicle. On success, also transitions the BOLO to HIT_REPORTED. Response: { hit, bolo }
GET/records/bolos/:boloId/hitsrecords.bolo.viewFull camera-sighting timeline for one BOLO, newest first
GET/records/vehiclesrecords.vehicle.view?plate=, ?registeredOwnerCivilianId=, ?cursor=, ?limit= (default 100, max 100) — cursor-paginated, see below
GET/records/civiliansrecords.civilian.view?q= (name search), ?licenseStatus=, ?cursor=, ?limit= (default 100, max 100) — cursor-paginated
GET/records/warrantsrecords.warrant.view?civilianId=, ?status=, ?cursor=, ?limit= (default 50, max 200) — cursor-paginated

A request to any route without the matching scope gets a clean 403, never a silent partial success.

Cursor pagination

/records/vehicles, /records/civilians, and /records/warrants don't return a bare array — they return:

{ "items": [ /* ... */ ], "nextCursor": "string|null", "hasMore": true }

Pass the previous response's nextCursor back as ?cursor= to get the next page; nextCursor: null (and hasMore: false) means you're on the last page. Treat cursor as an opaque token — don't try to construct or parse one yourself. /units and /cad/calls are not cursor-paginated — they return a plain array, optionally trimmed by ?limit=/?offset=.

BOLOs never carry the full narrative for this credential kind

Unlike every other resource on this page, a BOLO returned to an API credential is always the narrow summary shape — no narrative, internalNotes, or issuedByCharacterId field, on any of the four BOLO routes above, regardless of which scopes the credential holds. This isn't a scope gap you can fix by granting more access: the web app only returns the full BOLO object to a request made as a specific character, and an API credential (unlike a FiveM credential) never has one attached. If your integration needs the human-readable narrative, that has to come from somewhere other than this credential kind today.

The BOLO plate gotcha

A vehicle-linked BOLO returned by GET /records/bolos / GET /records/bolos/:boloId carries only subjectVehicleId — a bare UUID. There's no plate field anywhere on that response, and no nested vehicle object either. To get the actual plate string, make a second call: GET /records/vehicles/:vehicleId using subjectVehicleId — that response's plate field is the one and only place it lives.

POST /records/bolos/:boloId/hits does this resolution server-side already — your hit-report request's own plate field is independently verified against the BOLO's linked vehicle before being accepted, so reporting a hit doesn't strictly require you to pre-fetch the plate yourself. Anything else you build that needs to display or compare a plate (a dashboard, a report) does need the second call.

What's deliberately not here yet

  • Citations, arrests, properties, tips, attachments/notes, RMS reports/cases, radio talkgroups, and reference-data admin are still web-app-only — not reachable through this credential kind yet.
  • No fine-grained per-status transition rules — any status may follow any status, same as a human dispatcher can do in the web app.
  • A fivem-kind credential can't use these routes, even with matching scopes manually set on it — the two credential kinds are deliberately kept separate. If you need both in-game bridge behavior and direct API access, mint two separate credentials.