Skip to content

Delivery & webhooks

Message statuses

StatusMeaning
queuedAccepted and debited, waiting for the carrier.
sentThe carrier accepted it. Messages with no delivery receipt stay here.
deliveredThe carrier confirmed delivery to the handset.
undeliveredThe carrier reported it could not deliver (phone off too long, number inactive). Not refunded.
failedThe carrier rejected it, or every retry failed. Refunded automatically.

Delivery receipts depend on the destination network. Where the carrier sends none, sent is the final status.

Refunds

A failed message refunds its exact cost to your wallet on its own, stamped on the message and visible in your Transactions ledger. sent and delivered messages are never refunded. undelivered is not refunded either: the carrier did the work.

Webhooks

Set a webhook URL on the API keys page and we POST these events to it:

EventWhen
message.deliveredA delivery receipt confirmed the handset got it.
message.failedA message failed terminally (and was refunded).
batch.completedEvery message in a batch reached an outcome.
balance.lowA send left your balance under the low-balance threshold.

Example delivery:

json
{
  "event": "message.delivered",
  "created_at": "2026-08-30T17:02:11+00:00",
  "message_id": "b3e6a9d0-...",
  "batch_id": "9d2f7c1e-...",
  "to": "+263771234567"
}

Failed deliveries retry five times with growing gaps (30s up to 8 minutes). Answer with any 2xx status to acknowledge.

Verifying signatures

Every webhook is signed so you can prove it came from us. Two headers arrive with each POST:

X-XashSms-Timestamp: 1793552531
X-XashSms-Signature: 3f1d...

The signature is HMAC-SHA256 over "{timestamp}.{raw body}" using your whsec_ secret from the API keys page. Verify before trusting:

php
$expected = hash_hmac(
    'sha256',
    $request->header('X-XashSms-Timestamp').'.'.$request->getContent(),
    $webhookSecret,
);

abort_unless(hash_equals($expected, $request->header('X-XashSms-Signature')), 401);

Reject anything with a bad signature or a timestamp older than a few minutes. Rotating the secret on the dashboard invalidates the old one immediately.