With Vonage Verify v2, Vonage generates the code, so your test cannot know it. In test environments, point the Vonage SDK's apiHost at otpmock. otpmock generates the code, stores it in the inbox for that number and returns completed when your app calls checkCode with it. Application code stays the same.
Why Verify is harder to test than plain SMS
When your app sends its own code with the SMS API, a mock only has to store the text. With Verify, the provider creates the code and later checks it. A test needs both halves: a way to read the code, and a check endpoint that accepts it.
Setup
Create the client as usual and pass otpmock as the host when OTPMOCK_URL is set. In the test environment, set VONAGE_APPLICATION_ID to your otpmock API key. The SDK still signs a JWT with your private key; otpmock reads the application_id claim to find your account.
import { Vonage } from "@vonage/server-sdk";
export const vonage = new Vonage(
{
apiKey: process.env.VONAGE_API_KEY,
apiSecret: process.env.VONAGE_API_SECRET,
applicationId: process.env.VONAGE_APPLICATION_ID, // otpmock API key in tests
privateKey: process.env.VONAGE_PRIVATE_KEY,
},
process.env.OTPMOCK_URL ? { restHost: process.env.OTPMOCK_URL, apiHost: process.env.OTPMOCK_URL } : {},
);Your application code (unchanged)
export async function startVerification(phone) {
const { requestId } = await vonage.verify2.newRequest({
brand: "Acme",
workflow: [{ channel: "sms", to: phone.replace(/^\+/, "") }],
});
return requestId;
}
export async function confirm(requestId, code) {
return (await vonage.verify2.checkCode(requestId, code)) === "completed";
}The test
import { test, expect } from "@playwright/test";
import { OtpMock } from "./otpmock";
const otp = new OtpMock({ baseUrl: process.env.OTPMOCK_URL!, apiKey: process.env.OTPMOCK_API_KEY! });
test("login with Vonage Verify", async ({ page }) => {
const phone = otp.randomPhone();
await page.goto("/login");
await page.fill("[name=phone]", phone);
const since = Date.now() - 5000;
await page.click("text=Send code");
const { code } = await otp.waitForCode(phone, { since });
await page.fill("[name=code]", code);
await page.click("text=Verify");
await expect(page).toHaveURL(/dashboard/);
});Download the helper with curl -O https://otpmock.com/sdk/otpmock.ts.
Behaviour that matches Vonage
- A wrong code returns 400; after three wrong attempts the request returns 410, so you can test lockout screens.
- A second request to the same number while one is pending returns 409, as Vonage does.
code_lengthand a customcodein the request are honoured.- Cancelling with
DELETE /v2/verify/{request_id}works.
Full endpoint list on the Vonage provider page.
FAQ
Does otpmock check the JWT signature?
No. It only reads the application_id claim to find your account, so your normal private key works and no Vonage key is shared with otpmock.
Does this cover the SMS API and Messages API too?
Yes. vonage.sms.send and vonage.messages.send with channel sms are emulated; the text lands in the same inbox.
The free plan includes 100 messages a month. No card required.
Get a free API key