Architecture note: Adding phone‑based 2FA with Twilio Verify
Guide for designing a minimal Twilio Verify‑based 2FA wrapper: requirements, data boundaries, ops checks, failure handling, and verification steps.
07 Nov 2025, 06:48 UTC

Requirements
The service must confirm a user’s possession of a phone number before allowing access to sensitive actions. Key non‑functional goals are:
- Verification code delivery latency under 2 seconds on average.
- Ability to retry sending a code with exponential back‑off.
- Never store the raw verification code; only a hashed reference to the Twilio verification SID may be persisted.
- Idempotent requests so that retries do not cause duplicate SMS charges.
Smallest suitable design
A thin wrapper service exposes a single HTTP endpoint POST /verify. The flow is:
- Caller supplies
phoneNumber(in E.164 format) and an optionalidempotencyKey. - The wrapper calls Twilio’s Verify API (
verify/v2/Services/{SERVICE_SID}/Verifications) to send a code. - Twilio returns a
verificationSid. The wrapper immediately hashes this SID (e.g., SHA‑256 with a per‑environment secret) and stores the hash alongside the user identifier. - The wrapper responds with
202 Acceptedand a client‑side token that references the hash (never the SID itself). - When the user submits the received code, the caller invokes
POST /checkwith the token and the code. The wrapper looks up the stored hash, retrieves the originalverificationSidfrom a short‑lived cache (or re‑queries Twilio if the cache missed), and calls Twilio’s Check endpoint (verify/v2/Services/{SERVICE_SID}/Verifications/{verificationSid}/Check). - Twilio responds with
approvedorpending; the wrapper maps this to HTTP 200 (success) or 400 (failure) and returns only a boolean outcome.
Implementation notes:
- Use Twilio’s official server‑side SDK (Node.js
twilioor Javacom.twilio.sdk) with TLS 1.2 enforced. - Generate the idempotency key from a UUID v4 if the caller does not provide one; pass it as the
Idempotency-Keyheader to Twilio. - Store the hashed SID in a fast cache (e.g., Redis) with a TTL matching the verification expiry (typically 10 minutes).
- All secrets (Twilio Account SID, Auth Token, Verify Service SID, hashing secret) are fetched at runtime from a secret manager (AWS Secrets Manager, HashiCorp Vault, etc.) using the service’s IAM role; they never appear in source code or container images.
Trust and data boundaries
The wrapper treats Twilio as an external trusted party for code delivery and verification. Trust boundaries are:
- In‑boundary: The wrapper’s internal API, secret manager, and cache.
- Out‑of‑boundary: Twilio’s Verify API, the public telephone network, and the end‑user’s device.
Data that crosses the boundary:
- Phone number (E.164) is sent to Twilio; the wrapper redacts it from all logs.
- Verification SID is never exposed to callers; only its hash persists.
- Logs must omit any substring matching the pattern
\d{6}(typical verification code) and the raw phone number.
Operational checks
To maintain reliability, monitor the following:
- Delivery success rate: Subscribe to Twilio status callbacks (
StatusCallback) and increment a counter for eachqueued,sent,delivered, orfailedevent. Alert if the failed‑to‑sent ratio exceeds 5 % over a 5‑minute window. - Rate‑limit headers: Twilio returns
X-Twilio-RateLimit-RemainingandX-Twilio-RateLimit-Reset. The wrapper should read these headers and, when remaining < 10, apply a local throttling delay (e.g., 100 ms) before the next request. - Latency metrics: Record time from wrapper receipt of
POST /verifyto Twilio’ssentcallback, and fromPOST /checkto Twilio’s check response. Target p95 < 2 s. - Error code tracking: Capture Twilio error codes (e.g., 20400 – invalid parameters, 20404 – verification not found, 20429 – too many requests) and emit them to a monitoring system for trend analysis.
Example command to verify that no verification codes appear in logs (run on a log‑aggregation host with read‑only access to the service’s log stream):
# Requires: grep, regex support, access to /var/log/twilio-wrapper/*.log
# Risk: none – read‑only scan.
grep -E '\b[0-9]{6}\b' /var/log/twilio-wrapper/*.log | wc -l
# Expected output: 0 (any non‑zero indicates a logging violation)
Failure modes and design‑change triggers
Transient throttling (error 20429): If Twilio returns “Too Many Requests”, the wrapper should:
- Respect the
Retry-Afterheader if present. - Apply exponential back‑off (starting at 500 ms, max 8 s) before retrying the send.
- After three consecutive throttles, optionally offer a fallback OTP channel (e.g., email‑based TOTP) to the user.
Pricing or regional availability change: A significant shift in Twilio’s SMS pricing or withdrawal of service from a region would prompt a review of the vendor abstraction layer. The wrapper could be extended to support an alternate provider (e.g., Authy, Nexmo) behind a common interface without changing the caller contract.
Data‑residency regulation: If a new law requires verification records to stay within a specific jurisdiction, the current design would need to:
- Deploy a Twilio Verify Service instance in the allowed region (Twilio supports regional Services).
- Store the hashed SID in a data store that complies with the residency rule (e.g., a regional Redis cluster or a cloud‑SQL instance).
- Ensure the secret manager used for Twilio credentials is also region‑locked.
Only when such a regulatory shift occurs would the wrapper’s storage layer be changed; the API contract (POST /verify and POST /check) remains stable.
Verification checklist (staging)
- Create a Twilio Verify Service in the Twilio Console (or via CLI) and note the
SERVICE_SID. - Configure the wrapper’s environment variables to point to the secret manager entries for Account SID, Auth Token, and the Service SID.
- Deploy the wrapper to a staging namespace (e.g., a Kubernetes dev cluster or AWS Lambda staging alias).
- Using a test phone number approved in the Twilio Verify sandbox, send a request to
POST /verifywith a known idempotency key. - Confirm the response is
202 Acceptedand that the logs contain no verification code or raw phone number (run the grep check above). - Retrieve the code from the Twilio sandbox UI, submit it to
POST /check, and verify a200 OKwith body{ "approved": true }. - Monitor the metrics endpoints for send latency, check latency, and error counters; ensure they stay within the targets defined above.
If any step fails, adjust the wrapper’s secret retrieval, idempotency handling, or logging filters before promoting to production.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.