Twilio Verify is the right tool for sending OTPs — it handles templating, delivery, and attempt limits across channels. But it's not built for testing the flow from the other side: your CI pipeline needs a real phone number it can poll for an incoming SMS code, verify your app's logic extracted and submitted it correctly, and throw the number away. Twilio's sandbox mode requires pre-registering test numbers, which doesn't fit an automated suite that wants a disposable number per run.
AxioAPI's Verify API is built for that second use case: it gives you receivable numbers and polls them for incoming codes, with the code already extracted from the message text.
If you haven't read it yet, the OTP testing with an SMS API guide covers picking a number and polling pattern end to end — this post goes a layer deeper into verification-specific polling and backoff.
The endpoint
POST https://axioapi.com/api/v1/verify/wait Content-Type: application/json { "number": "+15551234567", "since": "2026-11-17T10:00:00Z" } since is optional — pass the timestamp right before you triggered your app's OTP send, so you don't accidentally pick up a stale code from an earlier test run on the same number.
const API = 'https://axioapi.com/api/v1'; const headers = { Authorization: `Bearer ${process.env.AXIOAPI_KEY}`, 'Content-Type': 'application/json', Accept: 'application/json', }; async function waitForCode(number, since, { timeoutMs = 60000, intervalMs = 3000 } = {}) { const deadline = Date.now() + timeoutMs; while (Date.now() < deadline) { const res = await fetch(`${API}/verify/wait`, { method: 'POST', headers, body: JSON.stringify({ number, since }), }); const { data } = await res.json(); const withCode = (data.items || []).find((item) => item.code); if (withCode) return withCode.code; await new Promise((resolve) => setTimeout(resolve, intervalMs)); } throw new Error('No OTP arrived in time'); } The response already runs a regex over the message body and populates code on each item when it finds something that looks like a 4–8 digit code — you don't need to write your own extraction logic for the common "Your code is 123456" format, though you should double check it against your own app's exact SMS copy before relying on it in CI.
Using it in a test
test('OTP login completes', async ({ page }) => { const number = process.env.TEST_PHONE_NUMBER; // a number from your Verify pool const requestedAt = new Date().toISOString(); await page.goto('https://staging.example.com/login'); await page.fill('[name=phone]', number); await page.click('button[type=submit]'); const code = await waitForCode(number, requestedAt); await page.fill('[name=otp]', code); await page.click('button[type=submit]'); await expect(page.getByText('Welcome back')).toBeVisible(); }); Why polling, not webhooks, for this
A webhook-based setup needs a publicly reachable URL during CI, which most pipelines don't have without extra tunneling infra. Polling verify/wait on a short interval is slower per-check but has zero infrastructure requirements — the tradeoff that matters for a test suite, not for production OTP delivery.
Keep the poll interval at a few seconds rather than hammering the endpoint in a tight loop; each poll is billed, and polling faster than the SMS actually arrives just burns credits without getting you the code any sooner. Current pricing is on the Verify API page.
Want to see it work before wiring it into CI? The SMS API page lists the public test numbers you can poll right now, and the pricing page has the free-tier credit limits if you just want to try a few requests first.

