Hellio Messaging
Docs /Webhooks

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.
Was this page helpful? Thanks for the feedback! Still stuck? Talk to our team