
The short answer: it is nearly always one of five things: the address is not HTTPS on port 443, it is a private address, your server takes more than 5 seconds to answer, the signature is checked against the wrong body, or the message was created before the webhook was saved. Always start with POST /v1/sms/webhook/test: it shows what your server answered.
The checklist
| Symptom | Likely cause | What to do |
| The webhook will not save | Address without HTTPS, on another port, or internal | Use https:// on port 443, at a public address. |
| The test returns an error or times out | Your server did not answer with a 2xx code within 5 seconds | Answer 200 at once and do the work afterwards, in a queue of your own. |
| Your code rejects every notice | The signature is computed over an already parsed body, or with the old secret | Use the raw body exactly as it arrived, and the current secret. |
| It rejects only some notices | Server clock running late, so t looks old | Fix the server time and allow a sensible tolerance. |
| One message never triggered a notice | It was created before the webhook was saved | Look it up with GET /v1/sms/messages/{id}. |
| The same notice arrived twice | Your server did not answer in time and we retried | Handle notices by message id and event; ignore the repeat. |
Checking the signature
The X-Webhook-Signature header looks like t=<unix time>,v1=<hex>. v1 is the HMAC-SHA256 of t + "." + body using your secret. In PHP:
$body = file_get_contents('php://input'); // the raw body, as it arrived
parse_str(str_replace(',', '&', $_SERVER['HTTP_X_WEBHOOK_SIGNATURE'] ?? ''), $a);
$expected = hash_hmac('sha256', ($a['t'] ?? '') . '.' . $body, $secret);
$fresh = abs(time() - (int) ($a['t'] ?? 0)) < 300; // tolerance of your choosing
if (!$fresh || !hash_equals($expected, $a['v1'] ?? '')) { http_response_code(401); exit; }
http_response_code(200); // answer now; process the notice afterwards
More detail, and the same idea in other languages, in receiving a webhook in PHP and checking its signature.
The retries
If your server does not answer with a 2xx within 5 seconds, we try again after 1 minute, 5 minutes, 30 minutes, 2 hours and 6 hours. So a server that is down for a few hours does not lose notices, but it may get them late and, if it answered too slowly, more than once.
| Do not do the heavy lifting before you answer. Sending an e-mail, building a PDF or calling another service inside the request is the most common cause of the 5-second timeout. Save the notice, answer 200 and handle it next. |
| Rotated the secret? Asking for a new one with rotate_secret replaces the old one for the notices that follow. Update your server at the same moment, or it starts rejecting everything. |
|
Still nothing reaching your address? See the API page, or contact us with the request's correlation_id. See the API page |
RECOMMENDED PRODUCT Web hosting with cPanel Domain and SSL included, daily backups and the panel you already know. from ₦9.900,00/mo (3-year plan, with coupon) See plans |











