Webhook Guides
Twilio Webhooks
Twilio webhooks notify your application about messaging, voice, verification, conversations, and status changes. The production challenge is that Twilio signature validation depends on the exact public URL Twilio called and the complete set of parameters it sent.
FastHook fits best as a durable event gateway for Twilio status callbacks: receive the callback quickly, preserve headers and body, route to the right receiver, record every destination attempt, and retry or replay safely after downstream failures.
Twilio callback model through FastHook
Treat Twilio as the provider, FastHook as the evidence and routing layer, and your receiver as the trust and business-logic layer. Twilio sees the FastHook source response; destination failures are recovered with FastHook attempts, retry rules, event retry, and request retry.
Choose the right Twilio webhook path
Twilio has both event-style callbacks and interactive webhooks. FastHook is strongest for event-style status callbacks. If Twilio expects your application to return dynamic TwiML in real time, use FastHook only when a static custom response is enough.
| Twilio flow | What arrives | FastHook fit |
|---|---|---|
| Messaging status callback | POST form parameters such as MessageSid, MessageStatus, SmsStatus, ErrorCode | Excellent fit: quick FastHook 200, durable request record, destination retry, replay |
| Voice status callback | POST callback event for call lifecycle or recording state | Good fit when the callback is event-style and does not need dynamic TwiML in the source response |
| Incoming SMS or Voice webhook | May require an immediate TwiML response | Use FastHook only if a static custom_response is acceptable; otherwise route directly to the app |
| Twilio retry controls | URL fragment options such as rc and rp | Provider-side retry before or around FastHook; fragments are not part of signature validation |
| FastHook retry and replay | Connection retry rule, event retry, request retry, bulk operations | Destination-side recovery after FastHook has accepted or stored the request |
Twilio signature boundary
Twilio signs requests with X-Twilio-Signature. Twilio's security documentation says the signature uses HMAC-SHA1 with the Twilio account auth token, and validation needs the signature header, the exact URL Twilio used, and all parameters sent by Twilio. For JSON callbacks, use the SDK helper that validates the raw body and keep the bodySHA256 query parameter in the URL when Twilio includes it.
This is not the same as FastHook source HMAC auth. FastHook source HMAC auth verifies HMAC-SHA256 over the raw body; Twilio's signature model is provider-specific. Keep Twilio validation in your receiver, and use FastHook destination signatures separately to trust the FastHook-to-receiver hop.
| Boundary | Header or config | Validator | Use this input |
|---|---|---|---|
| Twilio -> FastHook | X-Twilio-Signature | Twilio SDK validation in receiver | Exact FastHook source URL plus all Twilio parameters |
| FastHook source auth | BASIC_AUTH, API_KEY, or HMAC | FastHook ingress auth | Not a drop-in replacement for Twilio's signature algorithm |
| FastHook -> receiver | x-fasthook-signature | FastHook destination signature | timestamp.rawBody with v1= HMAC-SHA256 |
| Tracing and idempotency | MessageSid, CallSid, I-Twilio-Idempotency-Token, x-fasthook-request-id | Application deduplication | Useful evidence, not a substitute for signature validation |
POST /twilio/status HTTP/1.1
Host: hook-xxxxxx.fasthook.io
Content-Type: application/x-www-form-urlencoded
X-Twilio-Signature: Np1nax6uFoY6qpfT5l9jWwJeit0=
I-Twilio-Idempotency-Token: 20d7190c-25a7-4f2a-a137-1f7d68f7c1d4
MessageSid=SMaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa&MessageStatus=delivered&SmsStatus=delivered&To=%2B18005551212&From=%2B18005550199&AccountSid=ACaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa&ErrorCode=FastHook source for Twilio status callbacks
Create a source that accepts the Twilio methods you configure and responds quickly. For status callbacks, a small JSON custom response is usually enough. For interactive Twilio webhooks, only use a static XML response when static TwiML is acceptable for the whole endpoint.
curl -X POST "https://api.fasthook.io/v1/sources" \
-H "Authorization: Bearer fhp_xxx" \
-H "x-team-id: tm_3b5335b627084a838b" \
-H "Content-Type: application/json" \
-d '{
"name": "Twilio status callbacks",
"description": "Twilio messaging and voice status callback source",
"type": "WEBHOOK",
"status": "enabled",
"config": {
"auth_type": null,
"auth": null,
"allowed_http_methods": ["POST", "GET"],
"custom_response": {
"content_type": "json",
"body": "{\"queued\":true}"
}
}
}'curl -X POST "https://api.fasthook.io/v1/sources" \
-H "Authorization: Bearer fhp_xxx" \
-H "x-team-id: tm_3b5335b627084a838b" \
-H "Content-Type: application/json" \
-d '{
"name": "Twilio static TwiML",
"description": "Only for callbacks where one static TwiML response is enough",
"type": "WEBHOOK",
"status": "enabled",
"config": {
"auth_type": null,
"auth": null,
"allowed_http_methods": ["POST", "GET"],
"custom_response": {
"content_type": "xml",
"body": "<Response><Message>Thanks, we received your message.</Message></Response>"
}
}
}'Route Twilio callbacks
Twilio status callbacks are often form encoded. FastHook preserves the original body for delivery and replay, but connection filters inspect JSON bodies as objects and non-JSON bodies as strings. Start with path, query, and header filters; filter body fields only after you normalize the form body in a receiver or transformation.
// FastHook preserves the original Twilio body for delivery and replay.
// Form-encoded callbacks arrive as a string until your receiver or a transformation parses them.
// Use path/query/header filters first; only filter body fields after you normalize the form body.
new URLSearchParams(rawBody).get("MessageStatus");curl -X POST "https://api.fasthook.io/v1/destinations" \
-H "Authorization: Bearer fhp_xxx" \
-H "x-team-id: tm_3b5335b627084a838b" \
-H "Content-Type: application/json" \
-d '{
"name": "Twilio callback receiver",
"type": "HTTP",
"config": {
"url": "https://api.example.com/webhooks/twilio/status",
"http_method": "POST",
"auth_type": "FASTHOOK_SIGNATURE",
"auth": {}
}
}'curl -X POST "https://api.fasthook.io/v1/connections" \
-H "Authorization: Bearer fhp_xxx" \
-H "x-team-id: tm_3b5335b627084a838b" \
-H "Content-Type: application/json" \
-d '{
"name": "twilio-status-to-receiver",
"source_id": "src_twilio_status",
"destination_id": "des_twilio_receiver",
"rules": [
{
"type": "filter",
"headers": {
"x-twilio-signature": { "$exists": true }
},
"path": { "$in": ["/twilio/status", "/messages/status"] }
},
{
"type": "retry",
"strategy": "exponential",
"count": 5,
"interval": 60000,
"response_status_codes": ["429", "500-599"]
}
]
}'Validate in the receiver
Your receiver should validate Twilio before side effects. The important detail is the URL: validate with the public FastHook source URL Twilio called, not the destination URL that FastHook used to call your receiver.
import express from "express";
import twilio from "twilio";
const app = express();
// Twilio status callbacks are commonly application/x-www-form-urlencoded.
app.post("/webhooks/twilio/status", express.urlencoded({ extended: false }), async (req, res) => {
const signature = req.get("x-twilio-signature") || "";
// This must be the exact public FastHook source URL configured in Twilio,
// not this destination URL. Include the original query string if Twilio used one.
const publicTwilioUrl = process.env.TWILIO_FASTHOOK_SOURCE_URL;
const authToken = process.env.TWILIO_AUTH_TOKEN;
const valid = twilio.validateRequest(authToken, signature, publicTwilioUrl, req.body);
if (!valid) {
res.status(403).send("Invalid Twilio signature");
return;
}
const messageSid = req.body.MessageSid;
const status = req.body.MessageStatus || req.body.SmsStatus;
await upsertMessageStatusOnce({
idempotencyKey: messageSid,
status,
errorCode: req.body.ErrorCode || null,
fasthookRequestId: req.get("x-fasthook-request-id")
});
res.status(204).end();
});import express from "express";
import twilio from "twilio";
const app = express();
app.post("/webhooks/twilio/json", express.raw({ type: "application/json" }), async (req, res) => {
const signature = req.get("x-twilio-signature") || "";
const publicTwilioUrl = process.env.TWILIO_FASTHOOK_SOURCE_URL;
const authToken = process.env.TWILIO_AUTH_TOKEN;
const rawBody = req.body.toString("utf8");
const valid = twilio.validateRequestWithBody(authToken, signature, publicTwilioUrl, rawBody);
if (!valid) {
res.status(403).send("Invalid Twilio signature");
return;
}
const payload = JSON.parse(rawBody);
await processTwilioJsonCallbackOnce(payload);
res.json({ received: true });
});Status callback payload
Messaging status callbacks include message identifiers, the current status, and error details for failed or undelivered messages. Twilio can add parameters without notice, so parsers and signature validation must accept the complete evolving parameter set.
MessageSid=SMaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
MessageStatus=delivered
SmsStatus=delivered
To=+18005551212
From=+18005550199
AccountSid=ACaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
ErrorCode={
"MessageSid": "SMaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
"MessageStatus": "delivered",
"SmsStatus": "delivered",
"To": "+18005551212",
"From": "+18005550199",
"AccountSid": "ACaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
"ErrorCode": ""
}| Field | Meaning | Operational use |
|---|---|---|
MessageSid | Stable Twilio message identifier | Trace and idempotency key for message status work |
MessageStatus / SmsStatus | Current message status | Route delivered, failed, undelivered, sent, queued |
ErrorCode | Twilio error code when status is failed or undelivered | Alert and retry analysis |
AccountSid | Twilio account identifier | Tenant/account scoping |
To / From | Recipient and sender numbers or channels | Operational context, not a primary idempotency key |
Inspect, retry, and replay
Use request records to prove what Twilio sent. Use events and attempts to prove what happened to each destination delivery. Retry one failed event when routing was correct. Retry the request when source handling, routing, filters, or transformations changed.
curl "https://api.fasthook.io/v1/requests?source_id=src_twilio_status&status=accepted&include_data=true&q=SMaaaaaaaa" \
-H "Authorization: Bearer fhp_xxx" \
-H "x-team-id: tm_3b5335b627084a838b"curl "https://api.fasthook.io/v1/events?source_id=src_twilio_status&status=FAILED&include_data=true&q=MessageStatus" \
-H "Authorization: Bearer fhp_xxx" \
-H "x-team-id: tm_3b5335b627084a838b"curl "https://api.fasthook.io/v1/attempts?event_id=evt_01JYTWILIO" \
-H "Authorization: Bearer fhp_xxx" \
-H "x-team-id: tm_3b5335b627084a838b"curl -X POST "https://api.fasthook.io/v1/events/evt_01JYTWILIO/retry" \
-H "Authorization: Bearer fhp_xxx" \
-H "x-team-id: tm_3b5335b627084a838b"curl -X POST "https://api.fasthook.io/v1/requests/req_twilio_status/retry" \
-H "Authorization: Bearer fhp_xxx" \
-H "x-team-id: tm_3b5335b627084a838b"Twilio retry vs FastHook retry
Twilio connection overrides are provider-side controls on the callback URL. FastHook retry rules are destination-side controls after FastHook accepts the request. If you use Twilio URL fragments such asrc and rp, remember that URL fragments are not sent in HTTP requests and should not be included in signature validation.
https://hook-xxxxxx.fasthook.io/twilio/status#rc=2&rp=ct,rt&ct=1000&rt=5000Production checklist
- Use one stable FastHook source URL per Twilio callback shape when signature validation must be simple.
- Validate
X-Twilio-Signaturewith Twilio's SDK before processing side effects. - Validate against the exact public FastHook source URL Twilio called, including encoded query parameters.
- Do not treat FastHook source HMAC as a replacement for Twilio signature validation.
- Use
MessageSid,CallSid, or a domain key for receiver idempotency. - Preserve unknown Twilio parameters; do not build allow-list parsers that break when Twilio adds fields.
- Use path, query, and header filters for form-encoded callbacks unless you normalize the body first.
- Use FastHook event retry after receiver bugs are fixed; use request retry after routing or parsing changes.
Twilio documentation notes
Twilio documents request signing in Webhooks security, status callback fields in the Message resource status callback reference, and provider-side retry fragments in Webhooks Connection Overrides.
Twilio webhook FAQ
Can FastHook source HMAC auth verify X-Twilio-Signature directly?
No. FastHook source HMAC auth verifies configurable HMAC-SHA256 over the raw body, optionally with a timestamp header and prefix. Twilio X-Twilio-Signature uses Twilio's URL-and-parameter validation model, so validate it with a Twilio SDK in the receiver.
Which URL should a receiver use when validating a Twilio signature behind FastHook?
Use the exact public FastHook source URL that Twilio called, including any query string and original encoding. Do not validate against the destination URL, because Twilio did not sign that URL.
Are Twilio status callbacks a good fit for FastHook?
Yes. Status callbacks are event-style notifications. FastHook can acknowledge Twilio quickly, preserve request evidence, route to destinations, record attempts, and retry or replay after receiver failures.
Should interactive Twilio Voice or Messaging webhooks go through FastHook?
Only when a static custom response is enough. Interactive Voice or Messaging flows that must return dynamic TwiML from business logic should usually call the application directly, because FastHook source acknowledgements are independent from downstream destination processing.