Gating Sensitive Actions with Twilio Verify: A Server-Side OTP Guide
Add phone-number ownership checks with Twilio Verify by creating a service, starting a verification, and validating the code server-side without ever handling the OTP yourself.
26 Sept 2025, 14:45 UTC

Desired outcome
Adding phone-number verification seems straightforward until you own the code storage, expiry timers, and SMS retry logic. Twilio Verify removes that operational burden: Twilio generates the one-time passcode, delivers it, and validates the submitted code. Your server only starts a verification, collects the user's input, and checks the final status. You never handle the raw OTP, so a database compromise does not expose valid codes.
The goal is to gate a sensitive action—account recovery, a high-value transaction, or a new device sign-in—behind proof that the user controls a specific phone number. Approve the action only after Twilio returns an approved status from a server-side check.
Prerequisites
- Credentials: Your Twilio Account SID and Auth Token (or an API Key). These must stay server-side; never embed them in mobile or browser code.
- Verify-capable channel: An SMS-capable sender such as a Messaging Service or a Twilio phone number. Voice, email, and WhatsApp are alternative channels with their own regional requirements.
- Server-side state: A record keyed to your end user (for example, a user ID or session token), not to the code. You will store the verification SID and timestamps, not the passcode.
- E.164 formatting: Recipient numbers must be in E.164 format (for example,
+15558675309).
Create and configure a Verify service
A Verify service holds the policy for your codes. Create one in the Twilio Console or via the API. The service SID begins with VA; store it in your server-side configuration and treat it as a secret.
Configure the service to match your risk and usability targets:
- Code length: Longer codes resist brute force but increase entry friction.
- Expiration window: Shorter windows reduce replay risk; balance this against SMS delivery latency.
- Allowed channels: Restrict to the channels you actually support (for example,
smsandcall).
These settings are per-service and have documented defaults. Confirm the current defaults in the Verify service settings before relying on them.
Start a verification
When the user submits a phone number, call the Verifications resource. Twilio generates the code and sends it. Your code must never generate, display, or log that value.
Run the following from your server environment with your credentials exported as environment variables. Replace VAXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX with your service SID and +15558675309 with the recipient's E.164 number.
curl -X POST https://verify.twilio.com/v2/Services/VAXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX/Verifications \
-u "$TWILIO_ACCOUNT_SID:$TWILIO_AUTH_TOKEN" \
-d "To=+15558675309" \
-d "Channel=sms"
Expected result: the response contains a verification SID and a pending status. If the send fails or is rate-limited, Twilio returns an error; handle it in your recovery path rather than surfacing the raw error to the user.
Check the code server-side
Collect the user-entered code and call the VerificationCheck resource with the same recipient and the submitted code.
curl -X POST https://verify.twilio.com/v2/Services/VAXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX/VerificationCheck \
-u "$TWILIO_ACCOUNT_SID:$TWILIO_AUTH_TOKEN" \
-d "To=+15558675309" \
-d "Code=123456"
Treat only an approved status as success. Anything else—including a non-approved status or an exception raised by your helper library—is a failed attempt. Map both shapes to the same user-facing recovery flow.
Expected checks and logging
| Check | Expected behavior |
|---|---|
| Start verification | Returns a verification SID with status pending |
| Successful code check | Returns status approved |
| Failed code check | Non-approved status or library exception; do not grant access |
Log the service SID, verification SID, channel, and timestamps for audit and debugging. Never log the passcode, your Auth Token, or full credential material. Mask phone numbers in logs (for example, +1555*****09) and never reveal to the user whether a number exists in your system.
Recovery options
Verification failures are normal; design for them explicitly.
- Wrong or expired code: Issue a new verification rather than allowing unlimited retries against the same code. Increment a server-side attempt counter and start a fresh verification when the user requests a resend.
- Rate limits: If a send or check is rate-limited, return a generic message such as "Too many attempts. Please try again shortly." Apply exponential backoff with jitter before retrying programmatically, and offer a fallback channel like voice.
- Send failures: If SMS delivery fails, offer a voice call fallback or a support path. Do not expose internal error codes to the end user.
Limitations and pre-launch verification
Verify is usage-metered; confirm current pricing and that your account and region support the chosen channel and sender before building. Channel availability, regional sender requirements, and error-code tables change over time.
Before go-live, verify the integration in a test account:
- Create a Verify service and start an SMS verification to a phone number you control. Confirm the code arrives and that VerificationCheck returns
approved. - Submit a deliberately wrong code and an expired code to observe the exact HTTP status and error code your helper library raises. Align your recovery paths to those results.
- Open the Verify service settings and confirm the configured code length, expiration, and rate limits match what your error handling assumes.
- Review Twilio's current Verify reference for channel support, regional sender requirements, and the error-code table.
Because Verify settings and channel availability are account- and region-specific, treat the exact error codes and defaults as items to confirm in your own test account rather than fixed constants.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.