Verify API Security Best Practices
The Vonage Verify API is a secure, reliable service for phone number verification and multi-factor authentication (MFA). However, like any authentication system, its overall security depends equally on how your backend application integrates with it. This guide explains the most important security considerations you must address in your own implementation to protect your users.
Note: The best practices in this guide apply to all Verify API channels, including SMS, Voice, WhatsApp, Silent Authentication, and any other supported channel. Although some examples reference Silent Authentication flows, the underlying principles are universal.
Understand the Trust Model
The Vonage Verify API performs one specific function: it validates whether a given request_id and code pair is correct. What it cannot do is determine whether the person submitting that pair is the same person who originally triggered the verification request.
This is a critical distinction. The Verify API operates at the network level — it knows nothing about your user sessions, login state, or application context. Your backend is responsible for binding a verification request to a specific user and session. If your backend does not enforce this binding, an attacker who obtains a valid request_id and code pair (from a different session, a previous request, or by intercepting a client-side flow) can use it to authenticate as any phone number they target.
The security model should therefore be understood as two complementary layers:
- Vonage's responsibility: Generating a cryptographically correct
code, delivering it securely, and verifying therequest_id/codepair. - Your responsibility: Ensuring that the
request_idandcodesubmitted for verification are the ones your backend initiated for the authenticated user in the current session.
Always Initiate Verification Server-Side
Never allow your client application (mobile app, browser, or front end) to initiate a verification request directly to the Vonage Verify API. All calls to POST /v2/verify must be made from your backend server.
This ensures that:
- Your API credentials are never exposed to the client.
- The phone number used in the verification is the one stored in your system for that user — not a value supplied by the client at runtime.
- Your backend retains full control over the verification lifecycle.
Incorrect pattern (do not do this):
Client → POST /v2/verify (directly to Vonage, with phone number from user input)
Correct pattern:
Client → POST /your-backend/start-verification
Backend → POST /v2/verify (to Vonage, using phone number from your database)
Backend stores request_id in session/database
Backend → returns request_id (or nothing) to client
Store the request_id Server-Side and Bind It to the User Session
When the Vonage Verify API responds to a start-verification request, it returns a request_id. This value must be stored securely on your backend — for example, in a server-side session or a database record — and explicitly linked to:
- The specific user or account initiating the verification.
- The phone number being verified.
- The current authentication session or transaction.
When the client subsequently submits a code for verification, your backend must:
- Retrieve the
request_idfrom its own session/database (not from the client). - Call
POST /v2/verify/{request_id}using only the server-storedrequest_id.
Never accept the request_id as input from the client. If your backend takes the request_id from a client request and passes it directly to Vonage, an attacker can supply a request_id from any previous valid verification — including one for a different phone number or a different user — and the API will return a successful validation.
Correct implementation example (Node.js/Express):
// Start verification — called from your app backend only
app.post('/start-verification', async (req, res) => {
const user = await getUserFromSession(req.session.userId);
// Phone number comes from YOUR database, not the client request
const { request_id } = await vonage.verify.start({
brand: 'YourApp',
workflow: [{ channel: 'sms', to: user.phoneNumber }]
});
// Store request_id server-side, bound to the user session
req.session.pendingVerification = {
request_id,
phoneNumber: user.phoneNumber,
userId: user.id,
createdAt: Date.now()
};
res.json({ status: 'verification_started' });
});
// Check code — request_id is retrieved from session, never from client
app.post('/check-code', async (req, res) => {
const { code } = req.body;
const pending = req.session.pendingVerification;
if (!pending || !pending.request_id) {
return res.status(400).json({ error: 'No active verification for this session' });
}
const result = await vonage.verify.check(pending.request_id, code);
if (result.status === 'completed') {
// Clear the pending verification after success
delete req.session.pendingVerification;
res.json({ verified: true });
} else {
res.status(401).json({ verified: false });
}
});
Never Trust Client-Supplied Parameters for Security Decisions
Your client application may send data to your backend as part of the verification flow (for example, a request_id returned to the client for use in a Silent Authentication redirect). Treat all such values as untrusted input:
- Do not use a client-supplied
request_idto call the Verify check endpoint. - Do not use a client-supplied phone number to decide which user is being verified.
- Do not rely on the client-side state to determine whether a verification has succeeded.
The client's role is limited to: triggering actions in your backend (via authenticated API calls to your own server) and, in the case of Silent Authentication, executing the check_url redirect over the mobile carrier network. Your backend must independently track and verify the outcome.
Implement Per-Session Verification Lifecycle Management
Each verification attempt should be scoped to a single session and user. Implement the following lifecycle controls in your backend:
- One active request per user: Before starting a new verification, check whether a pending
request_idalready exists for that user. Cancel or expire it before initiating a new one. - Short expiry on pending requests: If the user does not complete verification within a reasonable time window (e.g., 5 minutes), invalidate the session-stored
request_idon your backend and require a fresh verification to start. - Single-use consumption: Once a verification is successfully completed, immediately remove the
request_idfrom your session/database. A completedrequest_idshould never be reusable within your application. - Invalidate on failed attempts: After a configurable number of failed code submissions, cancel the verification request and require the user to start again.
Apply Rate Limiting at the Application Level
While the Vonage Verify API includes built-in anti-fraud protections, you should also implement rate limiting in your own application to reduce the risk of enumeration, flooding, or brute-force attacks:
- Limit verification requests per phone number: Restrict how many verification requests can be initiated for a given phone number within a time window (e.g., no more than 3 requests per hour).
- Limit requests per user account or IP address: Prevent a single account or origin from triggering an excessive number of verification requests.
These controls are distinct from — and complementary to — Vonage's platform-level rate limiting and anti-fraud system. For more details on Vonage's built-in fraud protections, see the Anti-Fraud System guide.
Separate Registration from Verification
There are two distinct moments when phone number verification is used in typical applications:
- Registration (enrolling a phone number for 2FA): The user is adding a new phone number to their account. This is typically done once and must be associated with the authenticated user's account record.
- Verification (using 2FA at login): The user is proving they still own the phone number enrolled during registration.
These two flows require different security controls:
During registration, verify that the phone number being enrolled is not already associated with another account, and ensure the user is authenticated before adding the number.
During verification at login, ensure the phone number used in the Verify API request matches the one stored in your database for that user — never the one provided by the client at login time. An attacker who knows a user's phone number should not be able to trigger a verification request on their behalf.
Additional Guidance for Silent Authentication
Silent Authentication introduces a specific multi-step redirect flow where the check_url must be followed by the user's mobile device over the carrier network. Additional care is required:
- Store the
request_idimmediately after calling/v2/verifyand before returning any response to the client. Link it to the user session as described in the Store therequest_idServer-Side and Bind It to the User Session. - Do not pass the
request_idto the client unless strictly required for the Silent Authentication redirect flow. If you must pass it (for the client to follow thecheck_url), treat it as a short-lived, single-use token and verify it against your session on the code-check step. - Force cellular data for the redirect: The
check_urlmust be followed over the mobile carrier network, not Wi-Fi. If the request is made over Wi-Fi, it will result in an error and the carrier-level proof of SIM possession is lost. Use the Vonage Android or iOS SDK to enforce this when building native mobile apps. The SDKs also provide built-in connectivity checks, timeout management across redirects (up to 10 redirects, 5-second timeout each), and typed exceptions that allow you to respond to failures precisely. See the Silent Authentication Best Practices Guide for implementation details. - Check for mobile data connectivity before initiating Silent Authentication: If the device does not have an active mobile data connection, do not start the Silent Authentication step. Instead, proceed directly to the next channel in your workflow (SMS, Voice, etc.). Starting a Silent Authentication request without mobile data will fail and adds unnecessary latency before the fallback channel is reached. The Vonage SDK throws an
sdk_no_data_connectivityexception when this condition is detected during execution — your app should catch this and act immediately rather than waiting for the default 60-second timeout. - Handle SDK exceptions and trigger backend failover promptly: If the SDK throws an exception during the Silent Authentication flow (for example,
sdk_no_data_connectivity,sdk_connection_error, orsdk_redirect_error), your mobile app must notify your backend immediately. Your backend should then call thePOST /v2/verify/{request_id}/next_workflowendpoint to advance the verification to the next channel without waiting. If no action is taken, the platform will automatically time out after 60 seconds and move to the next workflow — but triggering this from your backend immediately minimizes the user's wait time and keeps the verification lifecycle under your control, consistent with the Implement Per-Session Verification Lifecycle Management. Do not rely on the client to self-resolve: the exception handling must result in a backend-driven state transition. - Always confirm the result server-side: Do not rely on a client-reported success from the Silent Authentication redirect. Your backend should receive the final verification status and make the authorization decision independently.
Summary Checklist
Before deploying a Verify API integration to production, confirm the following:
- All calls to
POST /v2/verifyare made server-side only. - The phone number in the verification request comes from your database, not from client input.
- The
request_idis stored server-side (session or database) and never accepted from the client. - The
request_idis bound to the specific user and session that initiated the verification. - Pending verifications are invalidated after a time limit.
- Completed or failed verifications are cleaned up immediately.
- Application-level rate limiting is in place per phone number, per user, and per IP.
- Registration and login verification flows are handled separately with distinct security checks.
- Silent Authentication redirect flows run over cellular data and outcomes are confirmed server-side.