Skip to content

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.

  1. Your CAD creates the incident. POST /incidents with your own incident reference in originId and a resources array of callsigns.

  2. The server alerts each callsign. For every device that carries one of those callsigns, the server stores an incidentAssigned event and sends a push.

  3. The crew accepts in the app. The app posts an incidentAccepted event, which moves that device’s unit status to En route.

  4. Your CAD reads acceptances back. Poll the events endpoint for incidentAccepted and reconcile against your own incident record.

  5. Your CAD keeps the incident current. PUT /incidents/update to 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.

  • 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 at https://app.staging1.infra.bluelightmaps.com/api with the same API.

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:

Terminal window
# 1. Log in, keeping the session cookie
curl -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 session
curl -b cookies.txt \
-d 'expiration=2026-12-31T00:00:00.000Z' \
https://app.bluelightmaps.com/api/session/token

The second call returns the token as a plain string in the response body. Use it as:

Terminal window
curl -H 'Authorization: Bearer PASTE_TOKEN_HERE' \
https://app.bluelightmaps.com/api/incidents

expiration 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.

Revoke a token you no longer need — a leaked one, or one belonging to a decommissioned integration:

Terminal window
curl -u 'cad-integration@example.org:CHANGE_ME' \
-d 'token=PASTE_TOKEN_HERE' \
https://app.bluelightmaps.com/api/session/token/revoke

Revocation returns 204, and the token is rejected from then on.

Terminal window
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"]
}'
FieldNotes
originIdRequired. 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.
typeIncident 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.
timeOfOriginWhen the incident happened, ISO 8601. If omitted, the server uses its own receipt time.
latitude, longitudeDecimal degrees.
priorityInteger. 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.
descriptionFree text, up to 255 characters. Appended to the push notification body the crew sees.
resourcesArray 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.

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 idstore 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"
}
}
}
StatusWhat it meansWhat to do
OFFLINEThe 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_FOUNDNo 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.

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.

Updates are keyed on your reference, not the database id:

Terminal window
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:

Terminal window
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.

Acceptance is read back by polling. Ask for the app event feed, filtered by type and time window:

Terminal 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 from and to. The time window is only applied when both are present; with one or neither you get the whole history for that type.
  • incidentId is the database id, not your originId. Keep the id from the create response against your own incident record and map back through it.
  • deviceId is the device, not the callsign. When a callsign has several devices you will see one acceptance per device that accepts. Resolve deviceId against GET /devices if 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.

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:

Terminal window
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:

Terminal window
curl -u 'cad-integration@example.org:CHANGE_ME' \
-X DELETE https://app.bluelightmaps.com/api/incidents/5127

Deleting 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.

Terminal window
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'
  • from and to filter on timeOfOrigin, and each works on its own.
  • status may be OPEN or CLOSED, and may be repeated. Without it you get both.
  • type may be repeated to match several types.
  • minPriority and maxPriority are inclusive bounds.
  • description does a partial match.
  • sortBy accepts timeOfOrigin (default), priority, type, status, id and description; anything else falls back to timeOfOrigin. sortOrder is DESC by default.
  • limit (default 100, must be at least 1) and offset (default 0) page through the results in the usual way: limit=100&offset=100 is the second page.

The list returns incidents your integration user is linked to, which in practice means the ones it created.

StatusWhenAction
202Created, updated, closed or deleted, but a callsign was OFFLINE or NOT_FOUND.Not an error. Read details.deviceStatuses and act per callsign.
400Missing 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.
401Missing, malformed, expired or revoked credentials.Re-authenticate, or mint a fresh token.
403The 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.
404PUT /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.
409POST /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.
503The server could not reach its database while checking your credentials. Carries Retry-After.Transient. Back off and retry.
  • 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.
  • originId is unique per incident in your system and stable across retries.
  • Your create handler branches on 202 and reads deviceStatuses; NOT_FOUND raises an operator-visible alert.
  • 409 on create is handled as “already exists”, not as a failure.
  • Every update sends the complete incident, including the full resources array.
  • The database id from the create response is stored, so acceptance events can be mapped back to your reference.
  • Your acceptance poller sends both from and to, overlaps its windows, and de-duplicates by event id. It expects one acceptance per device, not per callsign.
  • Closure sends "status": "CLOSED" and your handler reads deviceStatuses on a 202.
  • 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.