CAD integration guide
This integration lets your CAD or control-room system create an incident in Blue Light Maps and name the callsigns that should attend it. The server resolves each callsign to the devices that carry it, writes an assignment event and sends a push notification to every one of those devices, where the crew accepts the job. Your system reads the resulting acceptance events back out of the API and keeps the incident in step with your own record until it is closed.
Everything below is the HTTP API. Field-by-field detail lives in the API reference.
Lifecycle
Section titled “Lifecycle”-
Your CAD creates the incident.
POST /incidentswith your own incident reference inoriginIdand aresourcesarray of callsigns. -
The server alerts each callsign. For every device that carries one of those callsigns, the server stores an
incidentAssignedevent and sends a push. -
The crew accepts in the app. The app posts an
incidentAcceptedevent, which moves that device’s unit status to En route. -
Your CAD reads acceptances back. Poll the events endpoint for
incidentAcceptedand reconcile against your own incident record. -
Your CAD keeps the incident current.
PUT /incidents/updateto re-prioritise, add or remove callsigns, and finally to close the incident, which stands everyone down. Closed incidents stay on record;DELETE /incidents/{id}removes one entirely.
Prerequisites
Section titled “Prerequisites”- A dedicated integration user. Create one user for your CAD rather than reusing a person’s account. It needs device permissions on every device you intend to dispatch to — the server resolves callsigns only among devices the calling user can reach, and this is not waived for administrators.
- Callsigns must match device names exactly. A resource string is matched by string equality against each device’s
name(shown as Callsign in the web client). Nothing is trimmed or normalised, so a stray space or a different separator means no match. - One callsign, many devices. Several devices can carry the same callsign, for example two tablets in one appliance. A resource name reaches all of them, and each device accepts independently.
- Base URL. Production is
https://app.bluelightmaps.com/api, which all examples below use. For development your Blue Light Maps contact will give you access to a staging environment athttps://app.staging1.infra.bluelightmaps.com/apiwith the same API.
Authentication
Section titled “Authentication”Every request carries a bearer token or, if you prefer, HTTP Basic auth. Both resolve to the same integration user; a token is the better fit for a server-to-server integration because it can be rotated and revoked without touching the account password.
Mint a token once and use it until it expires. Tokens are created against a session cookie, so it is a two-step exchange:
# 1. Log in, keeping the session cookiecurl -c cookies.txt \ -d 'email=cad-integration@example.org' \ -d 'password=CHANGE_ME' \ https://app.bluelightmaps.com/api/session
# 2. Mint a token against that sessioncurl -b cookies.txt \ -d 'expiration=2026-12-31T00:00:00.000Z' \ https://app.bluelightmaps.com/api/session/tokenThe second call returns the token as a plain string in the response body. Use it as:
curl -H 'Authorization: Bearer PASTE_TOKEN_HERE' \ https://app.bluelightmaps.com/api/incidentsexpiration is optional; omit it and the token lasts seven days. If the session you mint against expires sooner than the date you ask for, the earlier of the two wins.
Also accepted on every endpoint. Send the integration user’s login and password on every request:
curl -u 'cad-integration@example.org:CHANGE_ME' \ https://app.bluelightmaps.com/api/incidentsRevoke a token you no longer need — a leaked one, or one belonging to a decommissioned integration:
curl -u 'cad-integration@example.org:CHANGE_ME' \ -d 'token=PASTE_TOKEN_HERE' \ https://app.bluelightmaps.com/api/session/token/revokeRevocation returns 204, and the token is rejected from then on.
Create an incident
Section titled “Create an incident”curl -u 'cad-integration@example.org:CHANGE_ME' \ -H 'Content-Type: application/json' \ -X POST https://app.bluelightmaps.com/api/incidents \ -d '{ "originId": "INC-2026-004411", "type": "FIRE", "timeOfOrigin": "2026-09-11T08:45:00.000Z", "latitude": 51.5074, "longitude": -0.1278, "priority": 1, "description": "Building fire, persons reported", "resources": ["P221", "A14"] }'| Field | Notes |
|---|---|
originId | Required. Your CAD’s incident reference. Must be unique across the whole system; it is the key for every later update. Stored as up to 128 characters. |
type | Incident category, free text up to 128 characters. The Blue Light Maps web client renders FIRE, MEDICAL, POLICE and OTHER with friendly labels and shows any other value verbatim. |
timeOfOrigin | When the incident happened, ISO 8601. If omitted, the server uses its own receipt time. |
latitude, longitude | Decimal degrees. |
priority | Integer. The server does not interpret the value — it stores it, exposes it in list filters and passes it to the app. Agree the scale with your Blue Light Maps contact and use it consistently. |
description | Free text, up to 255 characters. Appended to the push notification body the crew sees. |
resources | Array of callsigns. Each must equal a device name exactly; a callsign shared by several devices reaches all of them. |
status is always OPEN on a new incident, and timeReceived, timeUpdated and timeClosed are always set by the server; don’t send them.
200 versus 202
Section titled “200 versus 202”A 200 means every callsign matched at least one device, all of those devices were online, and the push went out to all of them. The body is the created incident, including the server-assigned id — store that id, because acceptance events reference it.
A 202 means the incident was created but at least one callsign could not be fully reached. The body wraps the incident and lists only the problem callsigns:
{ "incident": { "id": 5127, "originId": "INC-2026-004411", "...": "..." }, "message": "Incident created but some devices were offline or not found", "details": { "deviceStatuses": { "A14": "OFFLINE" } }}| Status | What it means | What to do |
|---|---|---|
OFFLINE | The callsign matched, but at least one of its devices was not online. The push was still sent with a ten-minute time-to-live, and it is queued for retry: the server re-sends it if the device comes online or registers a new push token within ten minutes. | Treat as “sent, not confirmed”. Wait for an acceptance event; if none arrives, fall back to voice. The retry queue is in memory, so a server restart loses it. |
NOT_FOUND | No device with that callsign is visible to your integration user. | A configuration fault: the callsign is misspelt, no device carries it, or your integration user has no permission on it. Alert your dispatcher — nothing was sent to anyone for that callsign. |
Duplicate references
Section titled “Duplicate references”If originId already exists the server returns 409 and creates nothing. Retrying a create after a network timeout is therefore safe: a 409 tells you the first attempt landed.
Update an incident
Section titled “Update an incident”Updates are keyed on your reference, not the database id:
curl -u 'cad-integration@example.org:CHANGE_ME' \ -H 'Content-Type: application/json' \ -X PUT https://app.bluelightmaps.com/api/incidents/update \ -d '{ "originId": "INC-2026-004411", "type": "FIRE", "timeOfOrigin": "2026-09-11T08:45:00.000Z", "latitude": 51.5074, "longitude": -0.1278, "priority": 2, "description": "Building fire, persons reported", "resources": ["P221", "A14"] }'Reassigning is the same call with a different resources array:
curl -u 'cad-integration@example.org:CHANGE_ME' \ -H 'Content-Type: application/json' \ -X PUT https://app.bluelightmaps.com/api/incidents/update \ -d '{ "originId": "INC-2026-004411", "type": "FIRE", "timeOfOrigin": "2026-09-11T08:45:00.000Z", "latitude": 51.5074, "longitude": -0.1278, "priority": 2, "description": "Building fire, persons reported", "resources": ["P221", "P309"] }'The server diffs the old and new callsign lists. Callsigns that appear get an incidentAssigned event and an assignment push on each of their devices; callsigns that disappear get an incidentDeassigned event and a “you have been removed from incident” push. Callsigns present in both are left alone — no repeat push, so re-sending an unchanged list is harmless.
The response is 200 with the updated incident, or 202 in the same shape as a create. The deviceStatuses map on an update covers only the callsigns that changed, not the ones that stayed on the incident.
timeUpdated is refreshed by the server on every successful update.
Receiving acceptance and other events
Section titled “Receiving acceptance and other events”Acceptance is read back by polling. Ask for the app event feed, filtered by type and time window:
curl -u 'cad-integration@example.org:CHANGE_ME' \ -G https://app.bluelightmaps.com/api/events/app \ --data-urlencode 'types=incidentAccepted' \ --data-urlencode 'from=2026-09-11T08:00:00.000Z' \ --data-urlencode 'to=2026-09-11T09:00:00.000Z'[ { "id": 884213, "type": "incidentAccepted", "eventTime": "2026-09-11T08:46:22.000Z", "deviceId": 41, "positionId": 0, "geofenceId": 0, "maintenanceId": 0, "attributes": { "incidentId": 5127 } }]Notes for a poller:
- Always send both
fromandto. The time window is only applied when both are present; with one or neither you get the whole history for that type. incidentIdis the database id, not youroriginId. Keep theidfrom the create response against your own incident record and map back through it.deviceIdis the device, not the callsign. When a callsign has several devices you will see one acceptance per device that accepts. ResolvedeviceIdagainstGET /devicesif you need the callsign.- Results are restricted to devices your integration user can reach, so in normal use you see acceptances only for jobs you could have dispatched.
- You can request several types at once (
types=incidentAccepted,incidentAssigned,incidentDeassigned). Results are concatenated per type rather than merged into one ordered stream, so sort client-side if order matters.
Accepting also moves the device’s unitstatus to 3 (En route) — visible on the device record if you would rather poll device state than events.
Closing an incident
Section titled “Closing an incident”An incident is OPEN from creation until you close it. Closing keeps the record for reporting, stamps timeClosed, and stands down every callsign still assigned: each of their devices gets an incidentDeassigned event carrying incidentStatus: "CLOSED" and a push telling the crew the job is over.
Close by sending the full incident with "status": "CLOSED". Leave resources out or send it empty — a closed incident cannot have callsigns, and sending any is rejected with 409:
curl -u 'cad-integration@example.org:CHANGE_ME' \ -H 'Content-Type: application/json' \ -X PUT https://app.bluelightmaps.com/api/incidents/update \ -d '{ "originId": "INC-2026-004411", "type": "FIRE", "timeOfOrigin": "2026-09-11T08:45:00.000Z", "latitude": 51.5074, "longitude": -0.1278, "priority": 2, "description": "Stop message, incident closed", "status": "CLOSED" }'The response is 200 with the closed incident, or 202 with deviceStatuses if a stood-down device was offline or a callsign could not be resolved, exactly as for a create.
Reopen by sending the full incident with "status": "OPEN" and the callsigns to attend; timeClosed is cleared and the callsigns are assigned as on a normal update.
Stand down without closing by sending an empty resources array with the status left as OPEN, for example while the incident is being re-crewed.
Remove the record with DELETE /incidents/{id}, using the database id from the create response:
curl -u 'cad-integration@example.org:CHANGE_ME' \ -X DELETE https://app.bluelightmaps.com/api/incidents/5127Deleting stands down anyone still assigned in the same way as closing. 204 means the record is gone and every assigned device was online and notified. 202 means the record is gone but at least one device was offline (the push is queued for retry) or a callsign could not be resolved; the body carries the same deviceStatuses map. Once deleted, the id is gone: a later PUT or DELETE on it returns 404. Prefer closing over deleting unless the incident was created in error.
Listing incidents
Section titled “Listing incidents”curl -u 'cad-integration@example.org:CHANGE_ME' \ -G https://app.bluelightmaps.com/api/incidents \ --data-urlencode 'from=2026-09-11T00:00:00.000Z' \ --data-urlencode 'to=2026-09-12T00:00:00.000Z' \ --data-urlencode 'status=OPEN' \ --data-urlencode 'type=FIRE' \ --data-urlencode 'minPriority=1' \ --data-urlencode 'limit=200'fromandtofilter ontimeOfOrigin, and each works on its own.statusmay beOPENorCLOSED, and may be repeated. Without it you get both.typemay be repeated to match several types.minPriorityandmaxPriorityare inclusive bounds.descriptiondoes a partial match.sortByacceptstimeOfOrigin(default),priority,type,status,idanddescription; anything else falls back totimeOfOrigin.sortOrderisDESCby default.limit(default 100, must be at least 1) andoffset(default 0) page through the results in the usual way:limit=100&offset=100is the second page.
The list returns incidents your integration user is linked to, which in practice means the ones it created.
Error handling
Section titled “Error handling”| Status | When | Action |
|---|---|---|
202 | Created, updated, closed or deleted, but a callsign was OFFLINE or NOT_FOUND. | Not an error. Read details.deviceStatuses and act per callsign. |
400 | Missing or empty originId; a body the server cannot store (for example a missing type); a status other than OPEN or CLOSED; a limit below 1 or a negative offset. | Check the response body, which carries the reason. Do not retry unchanged. |
401 | Missing, malformed, expired or revoked credentials. | Re-authenticate, or mint a fresh token. |
403 | The integration user is authenticated but not allowed: no permission on that incident, or an account flagged read-only. | A configuration problem on the Blue Light Maps side. Do not retry; raise it with your contact. |
404 | PUT /incidents/update with an originId that does not exist; any /incidents/{id} call for an unknown or already-deleted id. | Your reference and the server’s have diverged. Create the incident rather than retrying the update. |
409 | POST /incidents with an originId that already exists; an update that sends callsigns on a CLOSED incident. | On create: the record is already there, treat as success for retry purposes and switch to PUT /incidents/update. On update: reopen first, or drop the callsigns. |
503 | The server could not reach its database while checking your credentials. Carries Retry-After. | Transient. Back off and retry. |
Before go-live
Section titled “Before go-live”- A dedicated integration user exists, with permissions on every device you dispatch to.
- Every callsign your CAD can send has been matched, character for character, against the device names — including any callsign carried by more than one device.
-
originIdis unique per incident in your system and stable across retries. - Your create handler branches on
202and readsdeviceStatuses;NOT_FOUNDraises an operator-visible alert. -
409on create is handled as “already exists”, not as a failure. - Every update sends the complete incident, including the full
resourcesarray. - The database
idfrom the create response is stored, so acceptance events can be mapped back to your reference. - Your acceptance poller sends both
fromandto, overlaps its windows, and de-duplicates by eventid. It expects one acceptance per device, not per callsign. - Closure sends
"status": "CLOSED"and your handler readsdeviceStatuseson a202. - Tokens have an explicit expiration and there is a documented way to revoke them.
- The integration has been rehearsed against the staging environment with a device deliberately offline.