Testing recipe
Test Twilio Verify with real SMS receipt
Keep Twilio Verify responsible for sending and validating the code. Phonekit receives the SMS at your selected test number. The test passes when Twilio’s Verification Check approves the code, then your application can exercise its own post-verification behavior.
Prepare a verification service and selected receiver
Use a dedicated staging Twilio Verify service and a Phonekit number authorized by the test key. Configure that service's destination permissions and applicable sender policies. Any trial-account destination restrictions still apply. Establish that the chosen number receives this sender's verification flow before automating it.
Install the server-side JS/TS SDK in your test project. Install it with npm install @phonekit/sdk. Keep its key in the test runner environment.
Install the Twilio helper library and Playwright in the test project. Set TWILIO_ACCOUNT_SID, TWILIO_AUTH_TOKEN, TWILIO_VERIFY_SERVICE_SID, PHONEKIT_API_KEY, and TEST_PHONE_NUMBER in the runner environment. Keep the Twilio secret and the Phonekit key server-side.
npm install --save-dev @playwright/test twilioUse the Playwright configuration with one worker and a 180-second test deadline. Each concurrently running verification workflow needs its own number or explicit serialization.
Send, receive and check the code
import { test, expect } from "@playwright/test"
import twilio from "twilio"
import { Phonekit } from "@phonekit/sdk"
function required(name: string): string {
const value = process.env[name]
if (!value) throw new Error(`Set ${name}`)
return value
}
test("Twilio Verify approves the delivered SMS code", async () => {
const to = required("TEST_PHONE_NUMBER")
const number = new Phonekit({ apiKey: required("PHONEKIT_API_KEY") }).number(
to
)
const sender = twilio(
required("TWILIO_ACCOUNT_SID"),
required("TWILIO_AUTH_TOKEN")
)
const service = sender.verify.v2.services(
required("TWILIO_VERIFY_SERVICE_SID")
)
const after = await number.mark()
let verificationSid: string | undefined
let approved = false
let stage = "send"
try {
const verification = await service.verifications.create({
to,
channel: "sms",
})
verificationSid = verification.sid
expect(verification.status).toBe("pending")
stage = "receipt"
const code = await number.waitForOtp({ after, timeoutMs: 120_000 })
stage = "verification check"
const checked = await service.verificationChecks.create({ to, code })
approved = checked.status === "approved"
expect(approved).toBe(true)
} catch {
throw new Error(`Twilio Verify acceptance failed at ${stage}`)
} finally {
// An approved verification is already finished; cancel only unfinished work.
if (verificationSid && !approved) {
try {
await service
.verifications(verificationSid)
.update({ status: "canceled" })
} catch {
console.error("Verify test cleanup also failed")
}
}
}
})npx playwright test tests/twilio-verify.spec.ts --workers=1The send call creates a verification attempt; the subsequent check submits the received code to the same service. approved is the success assertion. The cleanup cancels unfinished work after a failure. See Verification operations and Verification Check.
Handle cooldowns and repeated attempts deliberately
Twilio documents a default token validity of ten minutes and reuse of the same token during its validity window until successful verification. Do not write a resend test that assumes each SMS contains a different code. Check your service's actual validity and policies, then assert the behavior your application promises. Verify rate limits and timeouts.
Use bounded attempts rather than repeatedly calling send until a text appears. A rate-limit response is a send/check failure; an SMS receive timeout is a different stage. If the application implements backoff or resend, test that behavior separately from this acceptance fixture.
Add the application assertion you need
This provider-level test proves that Twilio approves the code delivered to the selected number. It does not prove that your application grants the correct session or records a verified phone. Drive the application's own send/check endpoints and finish with a protected-state assertion for that layer; the Playwright recipe shows the browser boundary.
Record the operation and safe status metadata. Avoid attaching full provider errors, message bodies, or credential-bearing HTTP requests to public test logs. Preserve the original test failure if cleanup also fails, and diagnose cleanup separately in your runner's reporting policy.
Common questions
Is Phonekit replacing Twilio Verify?
No. Twilio sends and checks the verification; Phonekit provides the receiving company number and reads its inbox for the test. The test deliberately calls Twilio’s normal Verification Check rather than treating receipt as successful verification.
Should a resend always produce a different code?
No. Twilio documents token reuse within the active validity window until successful verification. Assert the configured service and application behavior instead of requiring a changed code.
Why use a dedicated Verify service?
It keeps acceptance-test configuration and verification attempts separate from production activity. The receiver still needs its own workflow allocation: a service boundary does not prevent two tests from sending to the same Phonekit number.
What does the cleanup do?
When the attempt remains unfinished, it requests canceled status for that verification. Approved attempts have already completed. Cleanup does not undo an application account change or replace the application’s staging reset.
Does an approved check prove my application works?
It proves the provider accepted the delivered code. Add the application’s session, verified-phone or protected-page assertion to cover what the application does with that result.