SOS Actuators
When an SOS incident opens, the platform fires every actuator configured for the app: it calls the user's emergency contacts and posts to any configured HTTP integrations (Slack, webhooks). An actuator is one such notification channel. Their live execution state is exposed to you as a single, uniform object: the actuator snapshot.
This page is the complete reference for that object — what each actuator type is, the status lifecycle every actuator shares, the success outcome verdict, and how to consume the snapshot over both REST and MQTT.
Actuators are read-only for integrators. You configure which actuators an app uses with Eixam; you cannot trigger or mutate them through the SDK API. You observe their state through the incident snapshot described here.
The snapshot model
Every SOS incident carries one actuators object — the same shape everywhere it appears (REST responses and MQTT events):
{
"snapshotVersion": 4,
"items": [
{ "id": "emergency_contacts", "type": "emergency_contacts", "status": "delivered", "outcome": "success", "...": "..." },
{ "id": "slack", "type": "slack", "status": "delivered", "outcome": "success", "...": "..." },
{ "id": "webhook", "type": "webhook", "status": "failed", "outcome": "failure", "...": "..." }
]
}
| Field | Type | Notes |
|---|---|---|
snapshotVersion | integer | Monotonic per incident. Increments on every change. Use it to order and de-duplicate snapshots — see Versioning. |
items | array | One entry per activated actuator. A discriminated union keyed by type. |
Each entry in items shares the same identifying and summary fields, then adds a type-specific detail block:
| Field | Type | Notes |
|---|---|---|
id | string | Unique key for this actuator on the incident. Equals type today (one actuator per type per app). |
type | string | Actuator category you switch on. Extensible — see Actuator types. |
status | enum | Rolled-up lifecycle state. |
outcome | enum | Success verdict derived from status. |
updatedAt | ISO 8601 | Latest transition across this actuator's deliveries. Omitted when nothing has run yet (plan state). |
contacts | array | Present when type = emergency_contacts. See Emergency contacts detail. |
deliveries | array | Present for HTTP actuators (slack, webhook). See HTTP actuator detail. |
Actuator types
type | Detail block | What it does |
|---|---|---|
emergency_contacts | contacts[] | Cascades through the user's priority-ordered contacts over SMS and voice. |
slack | deliveries[] | Posts the alert to a configured Slack integration. |
webhook | deliveries[] | POSTs the alert to a configured HTTP endpoint. |
type is open-ended: new actuator types may appear over time. Treat an unknown type defensively — rely on the shared status/outcome fields, which are identical for every type, and ignore detail blocks you don't recognise.
status: the delivery lifecycle
Every actuator — regardless of type — moves along one shared delivery lifecycle:
scheduled ──▶ sent ──▶ delivered
│
├──────▶ failed (terminal)
└──────▶ skipped (terminal)
status | Meaning |
|---|---|
scheduled | Planned, not yet attempted (or queued at the provider). |
sent | Dispatched / in flight (SMS sent, call ringing or in-progress). |
delivered | Confirmed delivered / completed (incl. an answered voice call). |
failed | Delivery failed — no-answer, busy, or error. |
skipped | Never executed (incident closed before it ran, or no target/link). |
Rollup: most-advanced-wins
An actuator has many underlying attempts (multiple contacts, multiple channels, retried HTTP posts). Its status is the most advanced state across all of them, by this rank:
delivered (4) > sent (3) > scheduled (2) > failed (1) > skipped (0)
So an in-flight cascade is never masked by an early failure — if contact 1's SMS failed but contact 2's is sent, the actuator is sent. One delivered channel makes the whole actuator delivered.
Which states each type reaches
The vocabulary is identical for every actuator, but each only reaches the states its mechanics allow:
- HTTP actuators (
slack,webhook) settle atdelivered(got a 2xx) orfailed. They don't have an intermediate "in flight" signal. emergency_contactscan also sit atsentwhile an SMS is in transit or a call is ringing.
:::info Acknowledgement is not a status
A human responding to the SOS is tracked separately, never as an actuator status:
- Incident-level — the canonical "SOS acknowledged" is the incident's own
status: acknowledgedandacknowledgedAt. - Per-contact — which contact responded is
contacts[].acknowledgedAt.
An answered voice call normalises to delivered on the delivery lifecycle; the acknowledgement signal it carries is surfaced at the incident/contact level above.
:::
outcome: the success check
outcome is a derived verdict so you can answer "did this actuator succeed?" without interpreting the lifecycle. It is a pure function of status:
outcome | When | Derived from status |
|---|---|---|
success | Dispatched or better to at least one target. | sent, delivered |
failure | Every attempt failed; nothing dispatched. | failed |
pending | Not attempted yet. | scheduled |
skipped | Never executed. | skipped |
Because status is most-advanced-wins, one success outweighs earlier failures: a Slack post that failed on attempt 1 (rate-limited) then delivered on attempt 2 rolls up to status: delivered → outcome: success.
For dashboards and "was the user reached?" logic, read outcome. For a detailed timeline, read status plus the per-attempt detail blocks below.
Emergency contacts detail
When type = emergency_contacts, the actuator carries a contacts[] array in priority order:
{
"id": "emergency_contacts",
"type": "emergency_contacts",
"status": "delivered",
"outcome": "success",
"updatedAt": "2026-04-18T12:00:42.000Z",
"contacts": [
{
"contactId": "a1b2c3d4-0000-4000-8000-000000000001",
"name": "Marie Dubois",
"priority": 1,
"status": "delivered",
"acknowledgedAt": "2026-04-18T12:00:42.000Z",
"channels": [
{ "channel": "sms", "status": "delivered" },
{ "channel": "voice", "status": "delivered" }
]
},
{
"contactId": "a1b2c3d4-0000-4000-8000-000000000002",
"name": "Jean Laurent",
"priority": 2,
"status": "sent",
"channels": [
{ "channel": "sms", "status": "sent" }
]
}
]
}
| Contact field | Type | Notes |
|---|---|---|
contactId | UUID | Stable id of the emergency contact. |
name | string | Contact display name. |
priority | integer | Cascade order (1 = first). |
status | enum | This contact's rolled-up delivery status across its channels. Same five values as the actuator. |
acknowledgedAt | ISO 8601 | When this contact responded to the SOS. Omitted if they haven't. Not a delivery status. |
channels | array | Per-channel breakdown. |
Each channels[] entry:
| Channel field | Type | Notes |
|---|---|---|
channel | enum | sms, voice, or http. Ordered sms → voice → http. |
status | enum | That channel's normalized delivery status (same five values). |
HTTP actuator detail
slack and webhook carry a deliveries[] array — one entry per attempt, including retries:
{
"id": "slack",
"type": "slack",
"status": "delivered",
"outcome": "success",
"updatedAt": "2026-04-18T12:00:18.000Z",
"deliveries": [
{ "attempt": 1, "status": "failed", "provider": "slack", "error": "rate_limited", "updatedAt": "2026-04-18T12:00:12.000Z" },
{ "attempt": 2, "status": "delivered", "provider": "slack", "updatedAt": "2026-04-18T12:00:18.000Z" }
]
}
| Delivery field | Type | Notes |
|---|---|---|
attempt | integer | 1-based attempt number. Retries increment it. |
status | enum | This attempt's status (same five values). |
provider | string | Delivering provider/actuator (e.g. slack, webhook). |
error | string | Failure reason when status = failed. Omitted otherwise. |
updatedAt | ISO 8601 | When this attempt last changed. |
Where you get the snapshot
The same actuators object is delivered through two channels.
Over REST
Every SOS incident response embeds the snapshot at incident.actuators:
| Endpoint | Snapshot you get |
|---|---|
POST /v1/sdk/sos | The plan — snapshotVersion: 1, every actuator scheduled / pending. |
GET /v1/sdk/sos | The live snapshot for the open incident. |
GET /v1/sdk/sos/history | Each historical incident's final snapshot. |
POST /v1/sdk/sos/cancel and POST /v1/sdk/sos/resolve return { "incident": null } — there is no open incident afterwards.
Over MQTT
Subscribe to sos/events/<sdk_user_id> (see the MQTT Integration guide). Two event types carry the snapshot:
Event type | Carries | When |
|---|---|---|
processed | The actuator plan (snapshotVersion: 1, all scheduled). | Once, right after the incident is accepted. |
sos.actuator_update | A live snapshot. | Every time an actuator transitions. |
The MQTT event wraps the snapshot in the incident envelope (type, incidentId, status, timestamps, snapshotVersion, actuators). REST and MQTT always agree for the same snapshotVersion.
Versioning & ordering
snapshotVersion is monotonic per incident and is the only safe way to order snapshots — MQTT does not guarantee delivery order, and a REST poll can race a pushed event.
Client rule: keep state keyed by incidentId, and discard any snapshot whose snapshotVersion is not strictly greater than the one you already hold.
hold: { incidentId, snapshotVersion, actuators }
on snapshot S for incidentId:
if S.snapshotVersion > hold.snapshotVersion:
replace hold with S # newer — take it whole
else:
ignore S # stale or duplicate
Each snapshot is complete — it fully replaces the prior one. Do not merge item-by-item; swap the whole actuators object.
Worked example: one incident end-to-end
The same incident (7c9e6679-…) as it progresses. IDs and timestamps are consistent so you can trace it.
1. processed — the plan (just opened, nothing has run):
{
"type": "processed",
"incidentId": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
"status": "active",
"snapshotVersion": 1,
"actuators": {
"snapshotVersion": 1,
"items": [
{
"id": "emergency_contacts", "type": "emergency_contacts",
"status": "scheduled", "outcome": "pending",
"contacts": [
{ "contactId": "a1b2c3d4-0000-4000-8000-000000000001", "name": "Marie Dubois", "priority": 1, "status": "scheduled",
"channels": [ { "channel": "sms", "status": "scheduled" }, { "channel": "voice", "status": "scheduled" } ] },
{ "contactId": "a1b2c3d4-0000-4000-8000-000000000002", "name": "Jean Laurent", "priority": 2, "status": "scheduled",
"channels": [ { "channel": "sms", "status": "scheduled" } ] }
]
},
{ "id": "slack", "type": "slack", "status": "scheduled", "outcome": "pending",
"deliveries": [ { "attempt": 1, "status": "scheduled", "provider": "slack" } ] },
{ "id": "webhook", "type": "webhook", "status": "scheduled", "outcome": "pending",
"deliveries": [ { "attempt": 1, "status": "scheduled", "provider": "webhook" } ] }
]
}
}
2. sos.actuator_update — live (a contact acknowledged → incident acknowledged; Slack succeeded on retry; webhook exhausted its attempts):
{
"type": "sos.actuator_update",
"incidentId": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
"status": "acknowledged",
"acknowledgedAt": "2026-04-18T12:00:42.000Z",
"snapshotVersion": 4,
"actuators": {
"snapshotVersion": 4,
"items": [
{
"id": "emergency_contacts", "type": "emergency_contacts",
"status": "delivered", "outcome": "success", "updatedAt": "2026-04-18T12:00:42.000Z",
"contacts": [
{ "contactId": "a1b2c3d4-0000-4000-8000-000000000001", "name": "Marie Dubois", "priority": 1,
"status": "delivered", "acknowledgedAt": "2026-04-18T12:00:42.000Z",
"channels": [ { "channel": "sms", "status": "delivered" }, { "channel": "voice", "status": "delivered" } ] },
{ "contactId": "a1b2c3d4-0000-4000-8000-000000000002", "name": "Jean Laurent", "priority": 2, "status": "sent",
"channels": [ { "channel": "sms", "status": "sent" } ] }
]
},
{ "id": "slack", "type": "slack", "status": "delivered", "outcome": "success", "updatedAt": "2026-04-18T12:00:18.000Z",
"deliveries": [
{ "attempt": 1, "status": "failed", "provider": "slack", "error": "rate_limited", "updatedAt": "2026-04-18T12:00:12.000Z" },
{ "attempt": 2, "status": "delivered", "provider": "slack", "updatedAt": "2026-04-18T12:00:18.000Z" }
] },
{ "id": "webhook", "type": "webhook", "status": "failed", "outcome": "failure", "updatedAt": "2026-04-18T12:00:30.000Z",
"deliveries": [
{ "attempt": 1, "status": "failed", "provider": "webhook", "error": "http 503", "updatedAt": "2026-04-18T12:00:15.000Z" },
{ "attempt": 2, "status": "failed", "provider": "webhook", "error": "connection timeout", "updatedAt": "2026-04-18T12:00:30.000Z" }
] }
]
}
}
Reading the live snapshot: emergency contacts reached someone who acknowledged (incident-level acknowledged + contacts[0].acknowledgedAt); Slack is a success despite a failed first attempt; webhook is a clean failure.
Integration checklist
- On
POST /v1/sdk/sos(or theprocessedevent), store the plan keyed byincidentId. - Subscribe to
sos/events/<sdk_user_id>and apply eachsos.actuator_updateusing the version rule. - Drive UI from
outcomeper actuator; usecontacts[]/deliveries[]for detail. - Show acknowledgement from the incident
status/acknowledgedAt(andcontacts[].acknowledgedAtfor who), never from an actuatorstatus. - If you reconnect or miss events, re-fetch
GET /v1/sdk/sos— the snapshot is self-contained and the version rule reconciles it with anything buffered.