Skip to Content

Your first message

With a device paired and connected, sending is one call. What makes it worth a page of its own is the lifecycle that follows — a message is not a fire-and-forget call, and the events it emits are how you build anything reliable on top.

Send

const msg = await luno.sms.send({ deviceId, to: '+9779800000000', body: 'Hello from Luno', ref: 'order-42' }) msg.id // track this through the events below

ref is your own correlation id. It is echoed back on every event the message produces, so you can join Luno’s lifecycle to your domain objects without keeping a side table of message ids.

If the device is offline, this call still succeeds. The command is queued and dispatched when the node reconnects — see Reliability.

Follow the lifecycle

A single send produces up to three events, in order:

sms_accepted

The node has written the command to its durable outbox and checked it for idempotency. Nothing has touched the radio yet.

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

Receiving this means the message will eventually be attempted, even if the process dies one millisecond later.

sms_sent

SmsManager reported the outcome of transmission, per part.

{ "messageId": "msg_1", "parts": [{ "index": 0, "status": "sent" }] }

This means the message left the device. It does not mean it arrived.

delivery_report

The carrier confirmed handset delivery. This may arrive seconds later, minutes later, or never — plenty of carriers and routes simply do not send reports.

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

Subscribe with luno.on. Every step of a message’s life surfaces as one sms.status event carrying the updated record, so you switch on status rather than subscribing to a different name per step:

luno.on('sms.status', ({ message }) => { console.log(message.ref, message.status) // 'pending' → 'dispatched' → 'accepted' → 'sent' → 'delivered' }) luno.on('sms.received', ({ from, body }) => { console.log('inbound from', from, body) })

luno.on returns an unsubscribe function.

Query state directly

Every transition is persisted on both sides, so you can also just ask:

const current = await luno.sms.get(msg.id) current.status // pending | dispatched | accepted | sent // delivered | undelivered | failed | cancelled

Status only ever moves forward. Node events are at-least-once and can arrive out of order, so each status is ranked rather than transitioned — replaying an old event is a no-op instead of a regression.

Receive

Inbound messages need no configuration beyond the full flavor and the RECEIVE_SMS permission. The node captures the broadcast, reassembles multipart messages, persists the result, and emits sms_received:

luno.on('sms.received', async ({ deviceId, from, body, subscriptionId, receivedAt, parts }) => { // … })

Your handler runs after the node has already persisted the message and the server has acked it. If your handler throws, the message is not redelivered by the node — it considers its job done. Treat the handler as the boundary into your own durable system, and make it write before it does anything else.

  • Sending SMS — multipart, multi-SIM, retry classification
  • Delivery reports — why they hang, and how Luno bounds them
  • Reliability — what happens across reboots, process death and offline periods