Sending verifications
Verifying a user is two calls: POST /v1/verify/verifications sends a passcode to an email address or phone number, and POST /v1/verify/verifications/check submits what the user typed and tells you whether it matched. Bird generates the code, delivers it, stores only a hash, and enforces expiry and attempt limits; your app never sees or stores the code itself. Full request and response schemas live in the API reference for creating a verification and checking a code.
Send a code
The smallest valid request is a to recipient:
Ejemplo de código
curl -X POST https://us1.platform.bird.com/v1/verify/verifications \
-H "Authorization: Bearer bk_us1_..." \
-H "Content-Type: application/json" \
-d '{
"to": { "email_address": "user@example.com" }
}'Use your regional host (https://us1.platform.bird.com or https://eu1.platform.bird.com) with a matching bk_{region}_... key.
Recipient
to names who you're verifying: an email_address, a phone_number in E.164 format, or both. The recipient determines the delivery channels. An email address resolves to the email channel. A phone number resolves to the phone channels available in its destination country, in the order your country configuration sets there: SMS first with WhatsApp as fallback, by default. Supplying both addresses gives Bird more than one way to reach the user: if a send on one channel fails, delivery advances to the next channel in the plan.
Options
options overrides settings for this request only:
- code_length: passcode length for this verification, 4 to 8 digits, overriding the default.
- channels: reorder or narrow the delivery channels for this request. List channel names (sms, whatsapp, email) in the order to try them; a channel you omit is not used, and a name not in the recipient's resolved plan is ignored. You can't add a channel this way, only trim or reorder what the recipient and country configuration already allow, and a list that leaves no usable channel fails the request with 422.
Metadata
metadata is a free-form object returned on every read; use it to carry your own user ID or session reference. Sender choices and the verification settings don't ride on the request: they come from your workspace's configuration, managed in the dashboard (see Verification settings).
The response
Ejemplo de código
{
"id": "vrf_01ky7q1fdze3695yvyz7z9nm3a",
"status": "pending",
"reason": null,
"to": { "email_address": "user@example.com" },
"channels": [{ "channel": "email" }],
"last_channel": "email",
"expires_at": "2026-07-23T14:55:58Z",
"verified_at": null,
"created_at": "2026-07-23T14:45:58Z",
"updated_at": "2026-07-23T14:45:58Z"
}channels is the ordered delivery plan this verification resolved to (a phone recipient lists its phone channels in attempt order), and last_channel is where the most recent code went. expires_at is when the verification lapses if no correct code arrives; resends don't extend it.
Check the code
Submit whatever the user typed to POST /v1/verify/verifications/check, keyed by the same recipient; no verification ID needed. Supply exactly the to set you created the verification with: one created with both addresses isn't found by either address alone.
Ejemplo de código
curl -X POST https://us1.platform.bird.com/v1/verify/verifications/check \
-H "Authorization: Bearer bk_us1_..." \
-H "Content-Type: application/json" \
-d '{
"to": { "email_address": "user@example.com" },
"code": "123456"
}'The response says whether it matched:
Ejemplo de código
{
"success": false,
"reason": "incorrect_code",
"attempts_remaining": 4,
"verification": {
"id": "vrf_01ky7q1fdze3695yvyz7z9nm3a",
"status": "pending",
"reason": null,
"to": { "email_address": "user@example.com" },
"channels": [{ "channel": "email" }],
"last_channel": "email",
"expires_at": "2026-07-23T14:55:58Z",
"verified_at": null,
"created_at": "2026-07-23T14:45:58Z",
"updated_at": "2026-07-23T14:46:38Z"
}
}Two behaviors here trip up first integrations:
- A wrong code is a 200, not an error. success: false with a reason (incorrect_code, expired, attempts_exhausted) is a normal answer; attempts_remaining tells you how many tries are left. Reserve your error handling for actual request failures.
- A verification is checkable exactly once it resolves. Once a verification reaches a final state, verified or otherwise, further checks return 404. Treat the first definitive answer as the answer; don't re-check to "confirm".
If the user asked for a new code, call the create endpoint again with the same recipient: the in-progress verification is reused rather than replaced. Once the resend cooldown has elapsed (60 seconds by default) a fresh code goes out; within the cooldown the call returns the live verification without sending again. Every code sent for the live verification stays valid until it resolves or expires, so the user can enter whichever one arrived.
Statuses
A verification is pending until it resolves into a final state, with reason saying why:
| Status | Meaning | Reason |
|---|---|---|
| verified | A correct code arrived in time | none |
| failed | Too many incorrect attempts | attempts_exhausted |
| expired | The window elapsed before a correct code | ttl_elapsed |
The status enum also reserves canceled and blocked for future use: no verification resolves to them today, but handle them as terminal if they appear. reason is an open enum; treat an unrecognized value as a future reason, not an error.
Track verifications in the dashboard
The Verifications page lists every verification the workspace created, filterable by status. Each row opens a detail view with the recipient, the resolved channel plan, the last channel used, expiry and verification times, and the metadata you attached: everything except the code itself, which is never stored or shown.

Verification settings
The Configure page sets the workspace's verification cycle. Each field shows the effective value: your override where you've set one, otherwise Bird's platform default.
- Duration: how long a code remains valid. Default 10 minutes; 1 minute to 999 minutes.
- Maximum Retries: how many check attempts before the verification fails with attempts_exhausted. Default 5; 1 to 10.
- Retry Delay: the cooldown before a new code can be sent to the same recipient. Default 60 seconds; 0 to 3600.

Code length isn't a field on this page: codes default to 6 digits, numeric, and options.code_length sets 4 to 8 digits per request.
Abuse guardrails
Independent of your settings, Verify enforces platform caps to keep OTP traffic from being weaponized, whether against your wallet (SMS pumping) or against a victim's inbox:
- 5 sends per recipient per rolling hour, counted per address.
- 10 checks per recipient per minute, throttling brute-force code guessing on top of the attempt limit.
Hitting a cap returns 429; back off and retry after the period in the Retry-After header. Your account's overall request limits are separate and plan-scaled; see Rate limits.
Retrying safely
Both endpoints accept the Idempotency-Key header: send it with a unique value per logical request, and a retry after a timeout or dropped connection replays the original response instead of processing the request again, so a retried create doesn't send a second code and a retried check doesn't consume another attempt. Replayed responses carry an Idempotency-Replay header. See idempotency for key format and retention.
Cost and billing
A verification is billed per code sent, not per verification: each delivery is charged to your wallet at that channel's rate for the destination, so a resend, or a fallback that moves delivery to a second channel, adds one charge per send. Sends on free routes cost nothing, a send rejected before it was billed is never charged, and checks are free. Payment methods & wallet covers balance and top-ups.
Next steps
| Page | What it covers |
|---|---|
| Senders & branding | What the code messages look like and how to send from your own domain |
| Country configuration | Per-country channel order, enablement, and sender overrides |
| Events | The verification lifecycle and delivery events, and their webhook payloads |
| Idempotency | Safe retries with the Idempotency-Key header |
| API reference: create a verification | Send-endpoint schema and error details |
| API reference: check a code | Check-endpoint schema and error details |