NG-SOS API
The NG-SOS API is the inbound half of an integration: your system calls NG-SOS. It reports calls, creates incidents, keeps dispatcher-entered data current, reaches the caller through chat, live video or a location SMS, and closes the incident.
The outbound half, where NG-SOS calls your system, is NG-SOS Webhooks v3.
| Environment | Base URL |
|---|---|
| Production | https://api.ng-sos.com |
| Azeroth | https://ems.azeroth.ng-sos.com |
Before you start
Obtain a client_id, a client_secret, the scopes your application may request and the agency IDs
it may act for from NG-SOS. The Identity Server guide covers the token request
in full; this page covers only what the API expects.
The typical flow
- Get a token from NG-SOS Identity with the scopes the operations need
and
agency_contextset to the agency you act for. Every incident operation, creation included, also needsusername.requiredinscopeand theusernameof the acting dispatcher. Cache it until shortly before it expires. - Report the call with
POST /v1/calls, andPOST /v1/calls/{callId}/endwhen it ends, if your system handles the telephony leg. - Create the incident with
POST /v1/incidents.caller.phoneNumberis required. The response carries the incident ID. - Keep the incident current. Each dispatcher-entered field has its own
PATCHoperation: call sign, caller full name, incident label, incident summary and patient description. A caller location is added withPOST /v1/incidents/{incidentId}/caller/track. - Reach the caller with
caller/locate, which sends a location-link SMS, or withcaller/chat/activateandcaller/live-video/activate, which return the Portal URL to open for the dispatcher. - Close the incident with
POST /v1/incidents/{incidentId}/close. A closed incident rejects every further change with409 IncidentClosed.
Every operation and its exact request and response shape is in the API reference.
PATCH or POST
| Verb | Meaning |
|---|---|
PATCH on a field | Sets a value you can set again. The five text fields also clear with an empty string. |
POST on a collection | Appends an entry that stays. caller/track is the only one. |
POST on a sub-resource | Performs an action, or a transition that happens once. |
A transition that already happened answers 409: IncidentClosed, CallAlreadyEnded,
ChatAlreadyActive, LiveVideoAlreadyActive, CollaborationInvitationAlreadyResolved.
Routing an incident
POST /v1/incidents creates the incident for the agency in the access token. Set routingMode to
TokenAgency.
When supplied, caller.location becomes the caller's first known position - the same kind of position
POST /v1/incidents/{incidentId}/caller/track adds. Both use an entered location with WGS84
coordinates and the required capturedAtUtc timestamp. The only supported source is Entered;
the server records when it received the position separately. Measured positions with an accuracy and
another source reach NG-SOS from devices, not over this API, and NG-SOS Webhooks v3 reports them as
Location.
Adding to the caller track
POST /v1/incidents/{incidentId}/caller/track appends a position a dispatcher established - the
caller described a landmark, a colleague called back with an address. Every location you add is
kept: a later one is preferred for display and supersedes the earlier one without replacing it, so
the incident keeps the whole sequence of what was known and when. There is no operation to correct
or remove an entry; add the better position and it takes over.
The track is the same one NG-SOS Webhooks v3 delivers back as caller.tracks, where each entry
carries the source it came from. Positions you add over this API form the track for the dispatcher
source; positions measured by a device form their own.
This is why it is a POST on a collection rather than a PATCH on a field. A PATCH in this API
sets a value you can set again, and a caller location is not that.
The caller phone number
POST /v1/incidents requires caller.phoneNumber. Your system takes the call, so it knows the number,
and NG-SOS needs it for everything it can do for that caller afterwards:
caller/locate sends the location-link SMS to it, and caller/chat/activate and
caller/live-video/activate reach the caller the same way. An incident created without one could
never use any of them.
The number cannot be changed over this API: there is no PATCH for it. NG-SOS may still resolve a
number of its own for incidents raised elsewhere, and
NG-SOS Webhooks v3 reports that as
PATCH /v3/incident/{incidentId}/caller/phone-number.
Repeating a request that timed out
POST /v1/calls recognises a repeat on a key, answers 201 when it created the resource and
200 when the key was already taken by a matching resource, and answers 409 when the key is
taken by something that does not match. A 409 changes nothing.
| Operation | Key | 201 | 200 | 409 |
|---|---|---|---|---|
POST /v1/calls | the agency in the token, callerPhoneNumber and startedAtUtc | the call was created | the call already existed | CallAlreadyExists — the call already ended at a time other than the endedAtUtc you sent |
Both 200 and 201 carry the same call body, so a retry needs no special handling: read the ID
and carry on.
POST /v1/incidents creates a new incident with a server-generated identifier for every accepted
request. Do not automatically retry it after a timeout: a retry can create another incident.
Recovering from a 409:
CallAlreadyExists— repeat withoutendedAtUtcto obtain the call ID, then end the call withPOST /v1/calls/{callId}/end. There is no read operation for a call, so repeating the request is the only way to obtain its ID.
Ending a call is a transition, not a creation, and is not repeatable: a second
POST /v1/calls/{callId}/end answers 409 CallAlreadyEnded.
Clearing a text field
Every PATCH operation that carries dispatcher-entered text — call sign, caller full name, incident
label, incident summary, patient description — clears the value when you send null. The property
itself stays required, so send it as null rather than leaving it out. An empty string and a value
of only whitespace are both rejected with 400 ValidationFailed.
curl --request PATCH "https://api.ng-sos.com/v1/incidents/$INCIDENT_ID/label" \
--header "Authorization: Bearer $ACCESS_TOKEN" \
--header "Content-Type: application/json" \
--data '{"label":null}'
This is the same value NG-SOS Webhooks v3 delivers for a cleared field, so a
system that implements both halves can carry a value between them unchanged: the null you receive
in IncidentLabelChanged is the null you send back here.
Scopes and token context
Each operation lists the scope it requires. Beyond the scope, operations differ in what the token must identify:
| Context | What the token request needs | Operations |
|---|---|---|
| Agency | agency_context | GET /v1/agency/info, POST /v1/calls, POST /v1/calls/{callId}/end |
| Agency and user | username.required in scope, plus agency_context and username | POST /v1/incidents and every operation on an existing incident |
An operation that needs the user context but receives an application-only token fails with
403 UserContextRequired; a missing agency context fails with 403 AgencyContextRequired.
Errors
Every error is application/problem+json with a stable code and a type of
https://ng-sos.dev/errors/{code}. Log code and traceId; branch on code, never on detail.
{
"type": "https://ng-sos.dev/errors/IncidentClosed",
"title": "Conflict",
"status": 409,
"detail": "The incident is already closed.",
"instance": "/v1/incidents/497f6eca-6276-4993-bfeb-53cbbbba6f08",
"code": "IncidentClosed",
"traceId": "00-example-trace-id"
}
| Status | code | Meaning |
|---|---|---|
| 400 | ValidationFailed | A field did not satisfy the documented constraints. errors names the offending fields. |
| 400 | CallerPhoneNumberRequired | The incident has no caller phone number. |
| 400 | AgencyNotAllowed | The submitted agency may not be invited to this incident. |
| 401 | AuthenticationRequired | A valid access token is required. |
| 403 | AgencyContextRequired | The token does not identify an agency. |
| 403 | UserContextRequired | The token does not identify a user. |
| 403 | AuthorizationDenied | The access token does not grant this operation. |
| 404 | IncidentNotFound | No incident with that ID is accessible to the token agency. |
| 404 | CallNotFound | No call with that ID is owned by the token agency. |
| 409 | CallAlreadyExists | The call already ended at a time other than the endedAtUtc you sent. |
| 409 | IncidentClosed | The incident is closed and accepts no further change. |
| 409 | UpdateConflict | The resource changed concurrently and could not be updated. |
| 409 | ChatAlreadyActive | Caller chat is already active. |
| 409 | LiveVideoAlreadyActive | Live video is already active on the incident. Activation never fails because a device cannot do video. |
| 409 | CallAlreadyEnded | The call already has an end time. |
| 409 | CollaborationInvitationPending | An invitation for that agency is still open. |
| 409 | CollaborationAlreadyActive | That agency already collaborates on the incident. |
| 409 | NoPendingCollaborationInvitation | No open invitation exists for the authenticated agency. |
| 409 | CollaborationInvitationAlreadyResolved | The invitation was already accepted or declined. |
| 415 | UnsupportedMediaType | Send the body as application/json. |
| 500 | InternalServerError | The request could not be completed. |
An incident that belongs to another agency answers 404 IncidentNotFound rather than 403, so the
API never reveals that an incident exists.
Collaboration
The agency that owns an incident obtains its currently eligible collaboration agencies from
GET /v1/incidents/{incidentId}/collaboration/available-agencies. This incident-specific
snapshot must not be cached. The invited agency accepts or declines with collaboration/accept
or collaboration/decline.