Verify best practices
Verify ships with defaults that suit most one-time-passcode flows. This guide covers the decisions the defaults do not make for you: how long a code should live, how many digits it needs, which channel is appropriate for what you are actually verifying, and how to tell a delivery problem from a verification problem.
Where a public standard has a view, this guide quotes it. The standard for passcode authentication is NIST SP 800-63B, Digital Identity Guidelines: Authentication and Authenticator Management, fetched 3 September 2026. Quotations are from the current revision. The older Revision 3 that much OTP advice still cites carries NIST's own banner: superseded by SP 800-63-4 as of 1 August 2025.
Keep the code six digits or more
Bird's codes default to 6 digits and options.code_length accepts 4 to 8 per request (Verification settings). The default is the right choice and the low end of that range is not:
The verifier SHALL generate random authentication secrets that are at least six decimal digits (or equivalent) in length using an approved random bit generator
A 4- or 5-digit code is inside what the API accepts and outside what the standard permits. Shorten a code only for a flow where you have decided passcode entropy is not doing the security work.
Keep the lifetime at ten minutes or less
Duration defaults to 10 minutes and accepts 1 to 999 (Verification settings). The default sits exactly on the standard's ceiling:
In all cases, the authentication SHALL be considered invalid unless completed within 10 minutes.
So the range runs well past the limit. Raising Duration to reduce support contacts about expired codes is the one setting change that quietly takes a flow outside 800-63B. If codes are expiring before users reach them, the fix is usually the send path rather than the window.
Expect two layers of rate limiting
A check attempt is bounded twice, and the two limits answer different threats.
Your setting. Maximum Retries defaults to 5 and accepts 1 to 10; past it the verification fails with attempts_exhausted. The standard requires this bound to exist for a code this short, and adds a rule worth knowing:
Generating a new authentication secret SHALL NOT reset the failed authentication count.
The bound has a ceiling, and this is the one place where every value Verify offers is comfortably inside the standard:
the verifier SHALL limit consecutive failed authentication attempts using a specific authenticator on a single subscriber account to no more than 100 by disabling that authenticator
Maximum Retries tops out at 10, so no configuration you can choose reaches 100. That is the mirror of the two sections above: code length and duration each have a supported value that leaves 800-63B, and the attempt limit has none.
Bird's platform caps, which apply whatever you configure: 5 sends per address per rolling hour across starting and resending, and 10 checks per recipient address set per minute. Both return 429 with a Retry-After header (Abuse guardrails). These exist to stop OTP traffic being weaponised against your wallet or a stranger's handset, so treat a 429 here as the system working rather than as a limit to engineer around.
Choose the channel for what you are verifying
Verify delivers over email, SMS, WhatsApp and Telegram (Channels). The standard treats two of those very differently, and the distinction is about what the code is for rather than which channel carries it.
Email is not an authentication factor. 800-63B is categorical:
Email SHALL NOT be used for out-of-band authentication because it may be vulnerable to:
- Access using only a password
- Interception in transit or at intermediate mail servers
- Rerouting attacks, such as those caused by Domain Name System (DNS) spoofing
The same passage carves out the case Verify's email channel is built for:
Confirmation codes that are sent to validate email addresses or are issued as recovery codes (see Sec. 4.2.1.2) are not authentication processes and not affected by the above prohibition.
So an email code that proves someone controls an address is in scope of the standard's exception and fine. An email code used as a second factor at sign-in is not. If your flow is the second one, do not reach for the email channel.
SMS and voice are permitted but restricted. Delivery over the telephone network carries three duties, in two consecutive sentences:
Accordingly, verifiers SHALL ensure that alternative authenticator types are available to all subscribers and SHOULD remind subscribers of this limitation of PSTN out-of-band authenticators before binding one or more devices controlled by the subscriber. Verifiers SHOULD consider risk indicators (e.g., device swap, SIM change, number porting, other abnormal behavior) before using the PSTN to deliver an out-of-band authentication secret.
The SHALL is a product requirement rather than a Verify setting: if SMS is your only factor, you do not meet it. Verify's per-country channel order helps with delivery, because WhatsApp is tried first in most countries with SMS as the fallback (Channels), but SMS, WhatsApp and Telegram are all delivery paths for one authenticator type, out-of-band, so moving between them is not offering an alternative type. The two SHOULDs are the ones a product team implements: a note at the point a user binds a phone number, and a look at porting and SIM-change signals before sending to one.
Read a failure before you resend
A code that does not arrive and a code that arrives and is mistyped need different handling, and the events tell them apart (Events).
verify.attempt.undelivered carries a reason, and what happens next depends on the recipient's channel plan. On a recipient with more than one channel available it does not end the verification: Bird advances to the next channel, which gets its own verify.attempt.sent, so a single undelivered attempt is not a dead verification and needs no resend from you. On a single-channel recipient there is nothing to advance to, and the verification ends.
That ending needs handling of its own, and one event identifies it. The failed status is shared: a verification lands there both when too many incorrect passcodes were submitted, with reason attempts_exhausted, and when no planned channel could deliver one, with reason undeliverable. Only the second emits verify.verification.failed. So on a single-channel recipient it is that event rather than the status that tells you nobody received a code.
One property to design around rather than discover, and this one is Bird's own rather than the standard's: Events states that delivery reports are indicative rather than guaranteed, because carriers and mailbox providers vary in what they confirm and how quickly. In some markets an attempt event arrives minutes late or does not distinguish delivery from acceptance. Build the "I didn't get a code" path on POST /v1/verify/verifications/next-channel, which advances the verification, rather than on waiting for a delivery event that may not come.
Retry the request, not the verification
All three Verify endpoints accept Idempotency-Key (Retrying safely). After a timeout or a dropped connection, retrying with the same key replays the original response: no second code goes out and no check attempt is consumed. Without a key, a retry after a lost response sends another code and spends another send against the hourly cap.
Know what a send costs
Billing attaches to each code sent, at the channel's rate for the destination, and a resend or a fallback adds a charge per send (Cost and billing). Two consequences shape a retry policy:
- A verification that falls through several channels costs several sends, so a wide channel plan is a cost decision as much as a delivery one.
- Telegram bills at a different point. Telegram is asked whether the number can receive a message, and the charge attaches when that answer is yes, at a flat rate worldwide. A number that cannot be reached is free and advances to the next channel with nothing billed. So a charge means accepted for delivery rather than arrived: a code that then goes undelivered stays charged, and the verification pays again for the channel it falls through to. Where that second charge is not wanted, take Telegram out of the channel order for those countries on the Countries page (Cost and billing).
Next steps
- Sending verifications: the settings, the guardrails and the full status list
- Events: every session and delivery event with its payload
- Countries: per-country channel order and availability
- Senders and branding: what each message looks like