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/linkstep 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 noidentifiersfield — 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.
| Group | Scope key | Grants |
|---|---|---|
| Units | cad.unit.view | List units, read status |
| Units | cad.unit.assign | Change a unit's status (full enum) |
| CAD Calls | cad.call.view | List calls, read call details |
| CAD Calls | cad.call.update | Change a call's status or priority |
| Records · BOLOs | records.bolo.view | List/search active BOLOs |
| Records · BOLOs | records.bolo.create | Issue a new BOLO |
| Records · BOLOs | records.bolo.manage | Resolve/update an existing BOLO, or report a camera hit |
| Records · Vehicles | records.vehicle.view | Search/view vehicle registration ("run plates") |
| Records · Persons | records.civilian.view | Search/view civilian records ("run names") |
| Records · Warrants | records.warrant.view | List/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.
| Method | Path | Scope required | Notes |
|---|---|---|---|
| GET | /units | cad.unit.view | ?departmentId=, ?isActive= (boolean), ?limit= (max 500) — all optional, omit departmentId to list across every department |
| GET | /units/:unitId | cad.unit.view | |
| PATCH | /units/:unitId/status | cad.unit.assign | Body: { "status": "AVAILABLE"|"EN_ROUTE"|"ON_SCENE"|"BUSY"|"OUT_OF_SERVICE"|"OFF_DUTY", "reason"?: string } — any status may follow any status |
| GET | /cad/calls | cad.call.view | ?departmentId=, ?status=, ?limit=, ?offset= (limit max 500) — all optional |
| GET | /cad/calls/:callId | cad.call.view | |
| PATCH | /cad/calls/:callId/status | cad.call.update | Body: { "status": "PENDING"|"ACTIVE" } |
| GET | /records/bolos | records.bolo.view | ?status=, ?issuingDepartmentId= — plain array response; see the plate gotcha and the summary-shape note below |
| GET | /records/bolos/:boloId | records.bolo.view | |
| POST | /records/bolos | records.bolo.create | Body: { subjectCivilianId? | subjectVehicleId?, issuingDepartmentId, severity, scope, subjectDescription, reason, narrative?, internalNotes?, expiresAt? } |
| PATCH | /records/bolos/:boloId/status | records.bolo.manage | Body: { "status": "ISSUED"|"ACKNOWLEDGED"|"HIT_REPORTED"|"RESOLVED"|"ARCHIVED"|"EXPIRED" } |
| POST | /records/bolos/:boloId/hits | records.bolo.manage | Camera-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/hits | records.bolo.view | Full camera-sighting timeline for one BOLO, newest first |
| GET | /records/vehicles | records.vehicle.view | ?plate=, ?registeredOwnerCivilianId=, ?cursor=, ?limit= (default 100, max 100) — cursor-paginated, see below |
| GET | /records/civilians | records.civilian.view | ?q= (name search), ?licenseStatus=, ?cursor=, ?limit= (default 100, max 100) — cursor-paginated |
| GET | /records/warrants | records.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.