SMS webhook not arriving or rejected: HTTPS, signature, the 5-second answer and retries

WhatsApp & APIs·· 98 views· 3 min read

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

SEE ALSO

The SMS webhook: how your system learns that each notice was sent or failed

Receiving a webhook in PHP and checking its signature

Webhook or polling: how one system learns that something changed in another

When an SMS notice fails: retries, returned credits and falling back to e-mail from your system

SMS API errors and message statuses: what each one means and what to do

An API key or token has leaked: what to do, in the right order

RECOMMENDED PRODUCT

Web hosting with cPanel

Domain and SSL included, daily backups and the panel you already know. from $6.60/mo (3-year plan, with coupon)

See plans
Was this answer helpful? Your answer helps us improve this article.