Test Twilio webhooks locally — signed status callbacks the SDKs verify
Status callbacks arrive in Twilio’s real form-param shape and verify through twilio-node’s own validateRequest.
Set up
One service block in stunt.yaml, then stunt up. The webhook_url is your test process — that's where signed deliveries land.
services:
twilio:
adapter: ./adapters/twilio-style
config:
webhook_url: http://localhost:9090/twiliostunt catalog add twilio-style
stunt up
# twilio-go: ClientParams{Client: &client.Client{Credentials: client.NewCredentials(sid, token), HTTPClient: rewriteClient}}
# twilio-node: twilio(sid, token, { httpClient: fetchBackedClient })What the suite proves
Asserted by the conformance suites in stunt's CI — not marketing copy. Every check below is a passing assertion:
- 01Message create returns an SM sid; SDK fetches drive the queued → sent → delivered lifecycle (derive-on-read, deterministic — no sleeps in your tests)
- 02The +15005550001 magic number fails like production
- 03List filters (To=) return only matching messages
- 04Each status transition fires a signed callback: base64(HMAC-SHA1(token, url + sorted key/value pairs)) — twilio-node’s validateRequest accepts every one
Bugs the harness caught
The callbacks couldn’t be verified by anyone
The mock delivered its own envelope body, not Twilio’s callback shape — so a receiver built from Twilio’s validation docs could never verify a delivery. The mock now POSTs the message resource as form params (MessageSid, MessageStatus, …) signed with Twilio’s real formula, and both SDK suites verify every callback.
Official SDKs refused the mock credentials entirely
The mock auth token contained underscores. Twilio’s real tokens are alphanumeric, and the official SDKs validate that client-side — twilio-go rejected the credentials before sending anything. The documented mock token is a realistic 32-hex string now.
Your webhook handler’s verification code — the part that matters — runs against deliveries that are byte-compatible with production.