Skip to main content

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.

note

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", "...": "..." }
]
}
FieldTypeNotes
snapshotVersionintegerMonotonic per incident. Increments on every change. Use it to order and de-duplicate snapshots — see Versioning.
itemsarrayOne 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:

FieldTypeNotes
idstringUnique key for this actuator on the incident. Equals type today (one actuator per type per app).
typestringActuator category you switch on. Extensible — see Actuator types.
statusenumRolled-up lifecycle state.
outcomeenumSuccess verdict derived from status.
updatedAtISO 8601Latest transition across this actuator's deliveries. Omitted when nothing has run yet (plan state).
contactsarrayPresent when type = emergency_contacts. See Emergency contacts detail.
deliveriesarrayPresent for HTTP actuators (slack, webhook). See HTTP actuator detail.

Actuator types

typeDetail blockWhat it does
emergency_contactscontacts[]Cascades through the user's priority-ordered contacts over SMS and voice.
slackdeliveries[]Posts the alert to a configured Slack integration.
webhookdeliveries[]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)
statusMeaning
scheduledPlanned, not yet attempted (or queued at the provider).
sentDispatched / in flight (SMS sent, call ringing or in-progress).
deliveredConfirmed delivered / completed (incl. an answered voice call).
failedDelivery failed — no-answer, busy, or error.
skippedNever 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 at delivered (got a 2xx) or failed. They don't have an intermediate "in flight" signal.
  • emergency_contacts can also sit at sent while 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: acknowledged and acknowledgedAt.
  • Per-contactwhich 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:

outcomeWhenDerived from status
successDispatched or better to at least one target.sent, delivered
failureEvery attempt failed; nothing dispatched.failed
pendingNot attempted yet.scheduled
skippedNever 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: deliveredoutcome: success.

tip

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 fieldTypeNotes
contactIdUUIDStable id of the emergency contact.
namestringContact display name.
priorityintegerCascade order (1 = first).
statusenumThis contact's rolled-up delivery status across its channels. Same five values as the actuator.
acknowledgedAtISO 8601When this contact responded to the SOS. Omitted if they haven't. Not a delivery status.
channelsarrayPer-channel breakdown.

Each channels[] entry:

Channel fieldTypeNotes
channelenumsms, voice, or http. Ordered sms → voice → http.
statusenumThat 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 fieldTypeNotes
attemptinteger1-based attempt number. Retries increment it.
statusenumThis attempt's status (same five values).
providerstringDelivering provider/actuator (e.g. slack, webhook).
errorstringFailure reason when status = failed. Omitted otherwise.
updatedAtISO 8601When 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:

EndpointSnapshot you get
POST /v1/sdk/sosThe plansnapshotVersion: 1, every actuator scheduled / pending.
GET /v1/sdk/sosThe live snapshot for the open incident.
GET /v1/sdk/sos/historyEach 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 typeCarriesWhen
processedThe actuator plan (snapshotVersion: 1, all scheduled).Once, right after the incident is accepted.
sos.actuator_updateA 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

  1. On POST /v1/sdk/sos (or the processed event), store the plan keyed by incidentId.
  2. Subscribe to sos/events/<sdk_user_id> and apply each sos.actuator_update using the version rule.
  3. Drive UI from outcome per actuator; use contacts[] / deliveries[] for detail.
  4. Show acknowledgement from the incident status/acknowledgedAt (and contacts[].acknowledgedAt for who), never from an actuator status.
  5. 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.