// delivery & webhooks
Delivery & webhooks
Pass a callbackUrl with a send and EK Message posts a signed webhook to it when the message is delivered or fails, so you do not have to poll.
Message statuses
| Status | Meaning |
|---|---|
| QUEUED | Accepted and charged; waiting for the send worker. Device-route messages stay QUEUED until the phone reports it sent them. |
| SENT | Handed to the upstream network (or sent by the device); awaiting a delivery receipt. |
| DELIVERED | Confirmed delivered to the handset. |
| UNDELIVERED | The network reported non-delivery. |
| FAILED | Rejected upstream or failed after retries; credits refunded. |
The webhook
When you set callbackUrl on a send, EK Message POSTs a JSON payload to it when the message reaches a final state. The X-EKSMS-Event header and the event field name it:
| Event | Sent when |
|---|---|
| message.delivered | The network confirms delivery. deliveredAt is set. |
| message.failed | The message failed and its credits were refunded. failureReason says why. |
Both events carry the same keys:
POST https://your-app.com/webhooks/sms
X-EKSMS-Signature: 9f86d081884c7d659a2feaa0c55ad015...
X-EKSMS-Event: message.delivered
Content-Type: application/json
{
"event": "message.delivered",
"messageId": "a1b2c3d4-...",
"batchId": "e5f6a7b8-...",
"recipient": "9800000000",
"status": "DELIVERED",
"deliveredAt": "2026-06-20T05:51:00.000Z",
"failureReason": null
}
Verify the signature
Every webhook carries an X-EKSMS-Signature header - an HMAC-SHA256 of the raw request body, keyed with your account's own signing secret. An organization owner can reveal or rotate it under Settings in the dashboard. Verify the header before trusting the payload, using a constant-time compare:
import { createHmac, timingSafeEqual } from "node:crypto";
function verify(rawBody, signature, secret) {
const expected = createHmac("sha256", secret)
.update(rawBody) // the RAW request body, not re-serialized
.digest("hex");
return timingSafeEqual(
Buffer.from(expected),
Buffer.from(signature),
);
}
// Express: use express.raw() so you get the exact bytes we signed.
app.post("/webhooks/sms", express.raw({ type: "*/*" }), (req, res) => {
const ok = verify(req.body, req.get("X-EKSMS-Signature"), process.env.EKSMS_WEBHOOK_SECRET);
if (!ok) return res.status(401).end();
const event = JSON.parse(req.body.toString());
// ... handle event.status
res.status(200).end();
});
Sign over the raw bytes
Rotating the secret
X-EKSMS-Signature-Previous, signed with the old secret. Accept either header until you have deployed the new secret.Respond fast, then process
2xx quickly to acknowledge receipt. Do slow work asynchronously - we make up to 3 attempts on non-2xx with a short backoff, so make your handler idempotent.Next: Errors · API reference