Point the Twilio SDK at a Verify-compatible mock in your test environment. The mock generates the code, exposes it to your tests over HTTP, and approves verificationChecks.create when the right code is submitted. Your application code stays the same.
Why Verify is harder to test than plain SMS
With Programmable Messaging, your app writes the message, so a test can intercept the text. With Verify, your app only calls two endpoints:
POST /v2/Services/{ServiceSid}/Verificationsasks Twilio to create and send a code.POST /v2/Services/{ServiceSid}/VerificationCheckasks Twilio whether the code the user typed is right.
The code itself only exists inside Twilio and on the user's phone. A fake that stubs messages.create doesn't help, and stubbing both Verify calls to always return approved means your test never exercises a wrong-code path.
What about Twilio's test credentials?
Twilio's test credentials are designed for a small set of resources: Messages, Calls, IncomingPhoneNumbers and Lookup. Requests to other resources are rejected, so they don't cover Verify, and they don't give you a message you can read back either.
Using a Verify-compatible mock
otpmock implements both Verify endpoints with Twilio's paths, parameters and response shapes:
- Starting a verification generates a 6-digit code, stores the text "Your verification code is: 123456" in your inbox, and returns
status: "pending". - A check with the right code returns
status: "approved"andvalid: true. - A wrong code returns
status: "pending"andvalid: false. The fifth wrong attempt cancels the verification. - Checking an approved or expired verification returns 404 with Twilio error 20404, as Twilio does.
Connect the SDK
import twilio from "twilio";
import { OtpMockHttpClient } from "./twilio-node-client.mjs";
export const sms = twilio(process.env.TWILIO_ACCOUNT_SID, process.env.TWILIO_AUTH_TOKEN, {
httpClient: process.env.OTPMOCK_URL ? new OtpMockHttpClient(process.env.OTPMOCK_URL) : undefined,
});
// unchanged application code
await sms.verify.v2.services(VERIFY_SID).verifications.create({ to: phone, channel: "sms" });
const check = await sms.verify.v2.services(VERIFY_SID).verificationChecks.create({ to: phone, code });In the test environment, set TWILIO_AUTH_TOKEN to your otpmock API key. The client keeps every path and rewrites the host, so both api.twilio.com and verify.twilio.com traffic goes to otpmock.
Read the code in a test
const since = Date.now() - 5_000;
await page.getByRole("button", { name: "Send code" }).click();
const { code } = await otp.waitForCode(phone, { since });
// happy path
await page.getByLabel("Code").fill(code);
// or test the wrong-code path first
await page.getByLabel("Code").fill(code === "000000" ? "111111" : "000000");
await expect(page.getByText("Incorrect code")).toBeVisible();FAQ
Does my Verify Service SID need to exist?
No. otpmock accepts any Service SID and keeps verifications separate per SID, so you can use the same SID as in production or a placeholder.
Which channels are supported?
The channel parameter is accepted and echoed back, and the code is delivered to the inbox for every channel. Use sms in tests.
Is the official twilio-node SDK supported?
Yes. otpmock is tested against the official twilio package for Node.js, version 5, for both Programmable Messaging and Verify.
The free plan includes 100 messages a month; each verification started counts as one.
Get a free API key