// delivery & webhooks

Delivery & webhooks

EK SMS pushes a signed webhook the moment a message is delivered - no polling. Most providers in Nepal still make you poll for delivery; we don't.

Message statuses

StatusMeaning
QUEUEDAccepted and charged; waiting for the send worker.
SENTHanded to the upstream network; awaiting a delivery receipt.
DELIVEREDConfirmed delivered to the handset.
UNDELIVEREDThe network reported non-delivery.
FAILEDRejected upstream or failed after retries; credits refunded.

The webhook

When you set callbackUrl on a send, EK SMS POSTs a JSON payload to it on every delivery-status change:

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-...",
  "recipient": "9818000000",
  "status": "DELIVERED",
  "deliveredAt": "2026-06-20T05:51:00.000Z"
}

Verify the signature

Every webhook carries an X-EKSMS-Signature header - an HMAC-SHA256 of the raw request body, keyed with your webhook signing secret. Verify it 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

Compute the HMAC over the exact raw body we sent, before any JSON re-serialization. Re-stringifying the parsed object can change whitespace and break the signature.

Respond fast, then process

Return 2xx quickly to acknowledge receipt. Do slow work asynchronously - we retry a few times on non-2xx with backoff, so make your handler idempotent.

Next: Errors · API reference