Why use webhooks
A webhook is an HTTP request Telnxo sends to your server when something happens: a message is delivered, a message fails, a customer replies or a call ends. Instead of asking us repeatedly whether a message arrived, you are told the moment it does. That is faster, cheaper and far more reliable at scale.
A webhook receiver looks simple, but it sits at the boundary of two systems and must cope with network failures, duplicates and hostile traffic. This guide explains how to build one that you can trust in production.
The life of an event
- Something happens. A message changes state or a reply arrives
- We build an event. The event has an ID, a type, a timestamp and the related message data
- We sign it. We compute a signature over the raw body using your secret
- We call your URL. An HTTPS POST with the JSON body and the signature header
- You answer. A 2xx response tells us you have it. Anything else makes us retry
Build a reliable receiver
- Answer quickly. Return a 2xx status within a couple of seconds and do the real work afterwards, for example by putting the event on a queue
- Verify the signature. Compute an HMAC of the raw body with your secret and compare it in constant time to the header
- Be idempotent. The same event can arrive more than once. Store the event ID and ignore repeats
- Handle order. Events can arrive out of order. Use timestamps or a status ranking so that a late
sentnever overwritesdelivered - Log the raw body. For a short period, so that you can replay an event when something goes wrong
- Alert on failure. Watch your error rate and the age of your oldest unprocessed event
Verifying signatures
<?php
$raw = file_get_contents('php://input');
$sent = $_SERVER['HTTP_X_SIGNATURE'] ?? '';
$expected = hash_hmac('sha256', $raw, $secret);
if (!hash_equals($expected, $sent)) {
http_response_code(401);
exit;
}
http_response_code(200); // acknowledge first
// then queue $raw for processingAlways sign and verify the raw body exactly as received. If you decode the JSON and encode it again, whitespace and key order can change, and the signature will no longer match.
Why a constant-time comparison
An ordinary string comparison stops at the first different character. An attacker who can time responses can use that to guess a signature one character at a time. A constant-time function such as hash_equals removes the leak.
Retries and what they mean for you
If your endpoint is unavailable or returns an error, we retry with increasing delays. This protects you from short outages, but it has consequences you should design for.
- Duplicates. If your server processed an event but timed out before answering, we will send it again. Idempotency is mandatory.
- Delay. An event can arrive minutes or hours late after an outage. Do not assume it is fresh.
- Order. Retries mean later events may arrive before earlier ones.
- Giving up. After the retry window we stop. Use the API to reconcile anything you missed.
Choosing what to subscribe to
| Event | Typical use |
|---|---|
| Message delivered | Mark an order notification as received |
| Message failed | Retry on another channel or alert support |
| Inbound reply | Open a conversation or process an opt-out |
| Call completed | Record duration for billing reconciliation |
Subscribe only to what you use. Every extra event type is more traffic to process and more code to maintain.
Security checklist
- Receive on HTTPS only, with a valid certificate
- Verify every signature before touching the payload
- Keep the secret in a secret manager and rotate it periodically
- Reject requests with a timestamp far in the past, to limit replays
- Do not expose the endpoint URL publicly or put it in client-side code
- Limit what the endpoint can do: it should record and queue, not run arbitrary actions
Testing
Test in the sandbox first. Send a message, confirm the receipt arrives, then stop your server and confirm the retry. Finally replay the same event twice and check that your system records it once. Those three tests find most real-world webhook bugs.
A reference architecture
A dependable design has three small parts. The receiver is a thin HTTP endpoint that verifies the signature, writes the raw event to a durable queue or table and returns 200. The worker reads from the queue, checks the event ID against a table of processed events and applies the change to your business data. The reconciler runs on a schedule, asks the API for messages in an unresolved state older than a threshold and repairs any gaps.
Keeping these separate means a slow database never makes the receiver time out, a bug in the worker never loses events, because they sit safely in the queue, and a missed webhook does not leave your data wrong forever.
Table design for idempotency
Create a table with a unique key on the event ID. Before processing, insert the ID. If the insert fails because the key exists, the event was already handled and you simply skip it. This works across many servers without any locking.
Monitoring
| Signal | Alert when |
|---|---|
| Receiver error rate | Above one per cent for ten minutes |
| Queue age | Oldest event older than five minutes |
| Signature failures | Any sudden increase, which may mean a rotated secret or an attack |
| Reconciliation fixes | More than a handful per day, which means webhooks are being lost |
Review these alongside your delivery dashboards. A webhook problem often shows up first as customers reporting missing notifications, so link the alerts to an on-call owner.
Handling failures gracefully
Decide in advance what your system does when something unexpected arrives: an unknown event type, a missing field or a message ID you have never seen. The safe default is to store the raw event, return 200 and raise a low-priority alert. Rejecting unknown events makes us retry them pointlessly, while crashing on them loses the information you need to fix the problem.
Further reading
- RFC 2104, HMAC: Keyed-Hashing for Message Authentication
- RFC 9110, HTTP Semantics, for status codes
- Telnxo guide: Create, scope and rotate API keys
Frequently asked questions
Should I use the parsed JSON or the raw body for the signature?
Use the raw body exactly as received. Re-encoding JSON changes the bytes and breaks the signature.
How fast must my endpoint respond?
Respond within a few seconds. Acknowledge first, process afterwards.
What if I miss events?
Use the API to reconcile messages by ID and time range.
Can I have several webhook URLs?
Use one endpoint that routes internally, unless your dashboard offers per-event URLs.
What status codes cause a retry?
Any non-2xx response, a timeout or a connection failure.