Webhooks
Receive real-time delivery receipts at your own URL instead of polling.
Setup
Register a URL and choose which events to receive.
Add an endpoint from Settings, then API Keys & Webhooks (or via the webhooks API). Choose which events to receive, or * for all. Each endpoint has its own signing secret.
Delivery payload
What we send when a message reaches a final state.
We POST a JSON body to your URL when a message reaches a final state:
{
"event": "message.delivered",
"mobile_number": "+233241234567",
"provider_message_id": "abc123",
"status": "delivered",
"timestamp": "2026-06-30T10:24:01+00:00"
}
Batched deliveries
One request carrying many events, for high-volume senders.
Enable Batch deliveries on an endpoint to receive status events grouped into batches of up to 100 per request instead of one request per event. The body wraps the same per-event objects shown above:
{
"type": "message.status.batch",
"events": [
{
"event": "message.delivered",
"mobile_number": "+233241234567",
"provider_message_id": "abc123",
"status": "delivered",
"timestamp": "2026-06-30T10:24:01+00:00"
}
]
}
Batched requests carry X-Hellio-Event: message.status.batch and are signed the same way as single deliveries: the signature headers below cover the full request body.
Verifying the signature
Confirm a delivery really came from Hellio.
Every delivery is signed so you can confirm it came from Hellio. Two headers are sent:
X-Hellio-Signature: HMAC-SHA256 of the raw request body, keyed with your endpoint secret.X-Hellio-Timestamp+X-Hellio-Signature-V2: HMAC-SHA256 of"{timestamp}.{body}", for replay protection. Reject deliveries whose timestamp is too old.
Verify in your handler (PHP example):
$payload = file_get_contents('php://input');
$timestamp = $_SERVER['HTTP_X_HELLIO_TIMESTAMP'] ?? '';
$expected = hash_hmac('sha256', $timestamp.'.'.$payload, $secret);
if (! hash_equals($expected, $_SERVER['HTTP_X_HELLIO_SIGNATURE_V2'] ?? '')) {
http_response_code(401);
exit;
}
// ...process the event
Retries & failures
What happens when your endpoint is down.
Respond with a 2xx within 10 seconds to acknowledge. Non-2xx responses are retried with exponential backoff. An endpoint that keeps failing is automatically disabled, and the last error is recorded so you can diagnose and re-enable it.
Best practices
Build a reliable receiver.
- Always verify the signature before trusting a payload.
- Make your handler idempotent, keyed on
provider_message_id. - Return quickly and process heavy work asynchronously.
