Webhooks

We POST a signed notification to your server whenever a message reaches its final state. This is how you learn what was actually delivered.

Setting it up

Set your notification URL in Settings. It must be a public HTTPS address. URLs pointing at private networks, internal hostnames or our own servers are rejected when you save them and again before every delivery.

Payload

JSON
{
  "event": "message.delivered",
  "data": {
    "message_id": "cmu9x2k4a0001abcd",
    "recipient": "+233241234567",
    "sender": "JKASMS",
    "status": "DELIVERED",
    "segments": 1,
    "cost": "0.06",
    "currency": "GHS",
    "campaign_id": null,
    "failure_reason": null,
    "submitted_at": "2026-09-21T10:30:02.000Z",
    "delivered_at": "2026-09-21T10:30:04.000Z",
    "failed_at": null
  },
  "sent_at": "2026-09-21T10:30:04.120Z"
}

Events

EventWhen
message.submittedA network accepted the message. This is when you are charged.
message.deliveredThe handset confirmed receipt.
message.failedThe message failed, was rejected, or expired.
voice.call.answeredA call was picked up and the recording played. This is when a call is charged.
voice.call.no_answerA call rang out. Its call time is released.
voice.call.busyDeclined, engaged, unreachable or switched off. Released.
voice.call.failedThe call could not be placed. Released.
voice.call.cancelledYou cancelled the campaign before this call dialled.

Voice events carry a call_id and credits_used in seconds where a message event carries a message_id and a cost in cedis — the two channels bill in different units. The envelope, the signature and the retry schedule are identical, so one handler can switch on event and treat both. See Send a voice message for a full voice payload.

Responding

Return any 2xx as soon as you have stored the event. Do the real work afterwards — we time out after 15 seconds and treat that as a failure.

Retries

Any non-2xx or a timeout is retried five times with exponential backoff: roughly 10s, 40s, 2.5min, 10min, 43min. That covers a typical deploy window. After that the delivery is marked failed and we stop.

Make your handler idempotent. A retry after your server processed the event but failed to respond will deliver the same event again — key on message_id plus status.

Redirects are not followed

A redirect could point us at an address the pre-flight safety check never saw, so we do not follow them. Give us the final URL.

Next: verify the signature.