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.

Twilio webhook delivery through FastHook showing provider callback, FastHook source record, routing, receiver validation, attempts, retry, and replay.
FastHook stores the Twilio request and delivers it to your receiver, but the receiver should validate the Twilio signature with the exact public source URL Twilio called.

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 flowWhat arrivesFastHook fit
Messaging status callbackPOST form parameters such as MessageSid, MessageStatus, SmsStatus, ErrorCodeExcellent fit: quick FastHook 200, durable request record, destination retry, replay
Voice status callbackPOST callback event for call lifecycle or recording stateGood fit when the callback is event-style and does not need dynamic TwiML in the source response
Incoming SMS or Voice webhookMay require an immediate TwiML responseUse FastHook only if a static custom_response is acceptable; otherwise route directly to the app
Twilio retry controlsURL fragment options such as rc and rpProvider-side retry before or around FastHook; fragments are not part of signature validation
FastHook retry and replayConnection retry rule, event retry, request retry, bulk operationsDestination-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.

Twilio signature validation boundary showing Twilio X-Twilio-Signature, FastHook source auth, FastHook destination signature, and idempotency identifiers.
Provider signatures, FastHook ingress auth, and FastHook destination signatures answer different trust questions. Do not swap one for another.
BoundaryHeader or configValidatorUse this input
Twilio -> FastHookX-Twilio-SignatureTwilio SDK validation in receiverExact FastHook source URL plus all Twilio parameters
FastHook source authBASIC_AUTH, API_KEY, or HMACFastHook ingress authNot a drop-in replacement for Twilio's signature algorithm
FastHook -> receiverx-fasthook-signatureFastHook destination signaturetimestamp.rawBody with v1= HMAC-SHA256
Tracing and idempotencyMessageSid, CallSid, I-Twilio-Idempotency-Token, x-fasthook-request-idApplication deduplicationUseful evidence, not a substitute for signature validation
Twilio signed callback
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.

Create Twilio status source
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}"
      }
    }
  }'
Static TwiML source
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.

Form body normalization note
// 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");
Create Twilio destination
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": {}
    }
  }'
Create Twilio connection
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.

Validate form callback
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();
});
Validate JSON callback
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.

Status callback form fields
MessageSid=SMaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
MessageStatus=delivered
SmsStatus=delivered
To=+18005551212
From=+18005550199
AccountSid=ACaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
ErrorCode=
Status callback as parsed JSON
{
  "MessageSid": "SMaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
  "MessageStatus": "delivered",
  "SmsStatus": "delivered",
  "To": "+18005551212",
  "From": "+18005550199",
  "AccountSid": "ACaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
  "ErrorCode": ""
}
FieldMeaningOperational use
MessageSidStable Twilio message identifierTrace and idempotency key for message status work
MessageStatus / SmsStatusCurrent message statusRoute delivered, failed, undelivered, sent, queued
ErrorCodeTwilio error code when status is failed or undeliveredAlert and retry analysis
AccountSidTwilio account identifierTenant/account scoping
To / FromRecipient and sender numbers or channelsOperational 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.

Inspect Twilio requests
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"
Inspect failed Twilio events
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"
List Twilio attempts
curl "https://api.fasthook.io/v1/attempts?event_id=evt_01JYTWILIO" \
  -H "Authorization: Bearer fhp_xxx" \
  -H "x-team-id: tm_3b5335b627084a838b"
Retry one Twilio event
curl -X POST "https://api.fasthook.io/v1/events/evt_01JYTWILIO/retry" \
  -H "Authorization: Bearer fhp_xxx" \
  -H "x-team-id: tm_3b5335b627084a838b"
Retry one Twilio request
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.

Twilio connection override URL
https://hook-xxxxxx.fasthook.io/twilio/status#rc=2&rp=ct,rt&ct=1000&rt=5000

Production checklist

  • Use one stable FastHook source URL per Twilio callback shape when signature validation must be simple.
  • Validate X-Twilio-Signature with 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.

Related guides