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 takes one of two shapes, selected by routingMode:
routingMode | Meaning |
|---|---|
TokenAgency | The incident belongs to the agency in the access token. |
Jurisdiction | NG-SOS resolves the responsible agencies from caller.location, optionally narrowed by preferredAgencyTypes. |
caller.location is required for Jurisdiction routing, and NG-SOS stores it as the caller's
first known position - the same kind of position
POST /v1/incidents/{incidentId}/caller/track adds. Both are GeoPoint: a position a dispatcher
or partner picks has no capture time beyond the moment it was picked, and no accuracy. Measured
positions with a capture time, an accuracy and a 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 in both routing modes. 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, and a repeat of
POST /v1/incidents under the same requestedIncidentId does not rewrite 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 and POST /v1/incidents follow the same rule, so one retry branch covers both:
each recognises a repeat on a key, answers 201 when it created the resource and 200 when the key
was already taken by a resource matching the request, 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 |
POST /v1/incidents | requestedIncidentId | the incident was created | the incident already existed and matches the request | IncidentAlreadyExists — the ID is taken by an incident that does not match the request |
Both 200 and 201 carry the same body, so a retry needs no special handling: read the ID and
carry on.
The call key is derived from the request, so POST /v1/calls is repeatable without any extra field.
requestedIncidentId — a UUID you generate — is optional, and it is the only thing that makes
POST /v1/incidents repeatable: without it NG-SOS cannot tell a repeat from a new incident and every
request creates one.
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.IncidentAlreadyExists— the incident exists under the ID you sent. Continue with the operations on/v1/incidents/{incidentId}instead of creating another incident. Note that the comparison is against the incident as it stands now, so a repeat sent long after the original request can differ from it because the incident has since been edited.
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 | IncidentAlreadyExists | requestedIncidentId is taken by an incident that does not match the request. |
| 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
An agency involved in an incident can invite another agency from
GET /v1/agency/info → relatedAgencies. The invited agency accepts or declines with
collaboration/accept or collaboration/decline.