Webhooks
Receive real-time delivery and verification events, verify their authenticity, and handle retries safely.
Setup Webhooks
Webhooks are how TextCall notifies your application of what happened after you make an API call. Instead of polling, you register an HTTPS endpoint, and TextCall posts a JSON event to it whenever a message or verification changes state.
Payload schemas live in the referenceThis guide covers setup, signature verification, and reliability. For field-by-field payloads see Message Status Event and Verification Event.
Events available in v1.0
| Event | Trigger |
|---|---|
| message.status.update | The delivery status of an outbound message changes. |
| verification.update | The verification status, or the delivery status, of an OTP message changes. |
A single request produces multiple events. An SMS typically emits SENT, then FAILED or DELIVERED. A cascading verification emits one event per channel attempt plus events for the overall outcome. Your handler must expect several events for the same message_id or verification_id.
Step 1: Build your endpoint
Your endpoint must:
- Be reachable from the public internet over HTTPS with a valid, non-self-signed certificate.
- Accept
POSTwithContent-Type: application/json. - Return any 2xx status code to acknowledge. Anything else or no response within the timeout is treated as a failure and retried.
- Respond within 5 seconds.
- Be able to read the raw, unparsed request body. Signature verification is computed over the exact bytes TextCall sent; most frameworks hand you a deserialised object, and re-serialising it produces a different signature.
Step 2: Register the endpoint
Option A: In the portal (applies to all requests)
- Go to Applications → Manage → Webhooks.
- Click Add endpoint and enter your HTTPS URL.
- Select the events you want:
message.status.update,verification.update, or both. - Copy the application's webhook signing secret and store it in your secret manager. The secret is per application, not per endpoint; every endpoint registered under that application, plus any events sent to a per-request callback_url, is signed with the same secret. It is displayed once; if you lose it, regenerate it (see Rotating the secret below).
- Save.
Option B: Per request (overrides the default)
Pass callback_url in the body of POST /messages or POST /verifications. The override applies to that request only. Events delivered to a per-request callback_url are signed with the same application signing secret, so the same verification applies.
Precedence: request-level callback_url → application-level registered endpoint → no delivery.
Step 3: Verify the signature
Your webhook endpoint is a public URL. Anyone who discovers it can post fake events to it, a forged DELIVERED, or a verification that appears to have succeeded. Every event TextCall sends carries a cryptographic signature computed with a secret known only to your application and TextCall. Your handler recomputes that signature from the request it received and compares. A match proves the event came from TextCall and was not altered in transit; anything else must be discarded.
The signature proves authenticity, not confidentiality. The payload is still readable in transit, which is why HTTPS remains mandatory.
Headers sent on every request
| Header | Example | Description |
|---|---|---|
X-Webhook-Signature | v1=a1b2c3d4… | Comma-separated, versioned signature list. Currently one entry, v1. Parse for the v1 key rather than reading the whole header; future versions may add entries. |
X-Webhook-Timestamp | 773000000 | Unix epoch seconds (not milliseconds, not ISO 8601) at which this delivery attempt was sent. |
How the signature is produced
- TextCall builds the signed payload by joining the timestamp header value, a literal period, and the exact raw JSON body: {timestamp}.{raw_body}
- It computes HMAC-SHA256 over that string, keyed with your application's signing secret.
- The result is encoded as a lowercase hex string and sent as
v1={hex}.
How to verify, in order
- Read the raw request body as UTF-8 bytes, before any JSON parsing.
- Reject if either header is absent, or if X-Webhook-Timestamp is not an integer.
- Reject if the timestamp differs from your current time by more than 300 seconds in either direction. This prevents replay of a captured event.
- Split
X-Webhook-Signatureon commas and take the value prefixedv1=. - Recompute HMAC-SHA256 over {timestamp}.{raw_body} with your stored secret; hex-encode in lowercase.
- Compare using a constant-time comparison. Ordinary string equality returns early on the first mismatched character, and the timing difference lets an attacker recover the signature byte by byte.
- On mismatch, return HTTP 401 Unauthorized and process nothing. On match, parse the JSON and handle the event.
Retries are signed independently. Each delivery attempt carries a fresh X-Webhook-Timestamp and a signature computed over it, so the 5-minute tolerance applies to the attempt, not to the original event. The payload including status_updated_at is identical across retries, which is why deduplication keys are built from payload fields rather than from the timestamp header.
Three things that break verification in production
- A 401 counts as a delivery failure: Rejected events are retried on the schedule in Step 4, and an endpoint failing continuously for 24 hours is disabled automatically. Deploying with the wrong secret means webhooks stop silently and the endpoint is switched off a day later. Alert on your signature-failure rate, not only on 5xx.
- Clock drift rejects everything: With a 5-minute tolerance, a host more than five minutes out of sync fails every event despite holding the correct secret. Run NTP and alert on drift.
- Re-serialised JSON never matches: Key order, whitespace, and number formatting all change the hash. Capture the raw bytes at the earliest point your framework allows.
Rotating the secret
Regenerate the secret under Applications → Manage → Webhooks. Rotation affects every endpoint under that application at once, including per-request callback_url deliveries.
Step 4: Handle retries and duplicates
Retry schedule
If your endpoint returns a non-2xx status or times out, TextCall retries with exponential backoff:
| Attempt | Delay after previous attempt |
|---|---|
| 1 | immediate |
| 2 | 30 seconds |
| 3 | 2 minutes |
| 4 | 10 minutes |
| 5 | 1 hour |
After the final attempt, the event is dropped and recorded as failed. If an endpoint fails continuously for 24 hours, it is automatically disabled, and the account owner is emailed.
Assume at-least-once delivery
Delivery is at-least-once. Retries, network partitions, and slow handlers all produce duplicates.
Do not assume ordering
Events can arrive out of order; a retried SENT may land after a first-attempt
DELIVERED. Never overwrite state based on arrival order. Compare data.status_updated_at (message events) or data.updated_at (verification events) against the value you last stored, and ignore anything older. Do not use X-Webhook-Timestamp for ordering; it reflects the delivery attempt, not the state change. Alternatively, treat terminal statuses as final and never downgrade from them.
Updated 25 days ago
