Skip to Content

Events

Events travel node → backend. They are the node’s entire reporting surface, and they are at-least-once: reliable events persist under their stable id, resend on every reconnect, and clear only on ack.

Summary

TypePayload
sms_accepted{commandId, messageId}
sms_sent{messageId, parts:[{index, status, errorCode?}]}
delivery_report{messageId, part, status, at}
sms_received{from, body, subscriptionId, receivedAt, parts}
device_status{battery?, network?, sims[]}
heartbeat{queueDepth, battery?, signals[], transports[]}
log{level, tag, msg, at} — redacted
error{ref?, code, message}

Message lifecycle events

sms_accepted

{ "commandId": "cmd_1", "messageId": "msg_1" }

The command is durably queued and idempotency-checked. Nothing has touched the radio. Receiving this guarantees the message will be attempted even if the process dies immediately after.

sms_sent

{ "messageId": "msg_1", "parts": [ { "index": 0, "status": "sent" }, { "index": 1, "status": "failed", "errorCode": "no_service" } ] }

The radio reported transmission, per part. Per-part detail is preserved so partial failure is visible rather than collapsed into a summary.

delivery_report

{ "messageId": "msg_1", "part": 0, "status": "delivered", "at": "…" }

The carrier confirmed handset delivery. May arrive minutes later or never — see Delivery reports.

sms_received

{ "from": "+9779800000000", "body": "…", "subscriptionId": 1, "receivedAt": "2026-07-19T09:00:00Z", "parts": 1 }

An inbound message, persisted and reassembled before it was reported.

Telemetry events

device_status

A full DeviceState snapshot — battery, network, and every SIM with its per-subscription signal. Emitted on change, and on demand in response to get_status.

heartbeat

{ "queueDepth": 0, "battery": { "level": 98, "charging": true }, "signals": [{ "subscriptionId": 1, "dbm": -87 }], "transports": [{ "id": "sms", "state": "ready" }] }

Emitted every 30–60 seconds, configurable with config_update. Its purpose is twofold: prove the peer is processing, not merely socket-alive, and carry cheap telemetry so a dashboard stays live without full status events flooding the radio.

queueDepth is the single most useful number for monitoring. A rising depth on a node with healthy heartbeats means sends are failing — the link is fine and the radio is not.

Diagnostics

log

{ "level": "warn", "tag": "SmsTransport", "msg": "…", "at": "…" }

Throttled, and redacted at the source. Phone numbers and message bodies are masked by a single central redactor before the line ever exists, so no call site can leak PII into your log pipeline. Full bodies live only in the node’s encrypted tables.

error

{ "ref": "order-42", "code": "policy_rejected", "message": "…" }

Normalised onto the error taxonomy, so your handling never has to special-case a subsystem. ref correlates the failure back to whatever you sent.

Acknowledgement and replay

The backend acks each event it has durably recorded:

{ "kind": "ack", "type": "ack", "payload": { "ackedId": "evt_7" } }

Until that ack arrives, the event stays in the node’s durable event_outbox and is resent on every transition to READY.

Your event handlers must be idempotent. Dedupe on the envelope id — a network drop between your server persisting the event and the node receiving the ack causes a redelivery of the identical frame.