Verifications
Bird Verifications is a stateful OTP flow over SMS, voice, and email — with channel fallback when one degrades. Codes are generated, delivered, and matched server-side; your code only ever sees "verified" or "not."
Concept
Two steps. bird.verifications.start({ to, channel }) creates a verification and dispatches the code; bird.verifications.check({ id, code }) validates the user-entered value.
const { data, error } = await bird.verifications.start({
to: "+15005550006",
channel: "sms",
});
if (error) throw error;
console.log(data.id); // "ver_2bX91q..."
const { data, error } = await bird.verifications.check({
id: "ver_2bX91q...",
code: "482917", // what the user typed
});
if (error) throw error;
console.log(data.state); // "verified" | "pending" | "failed"
Channels
Three channels supported: sms, voice, and email. Pass fallback to chain channels — if the primary degrades (carrier reject, no delivery within the configured wait window), the runtime retries on the next channel automatically.
const { data, error } = await bird.verifications.start({
to: "+15005550006",
channel: "sms",
fallback: "voice",
});
if (error) throw error;
Code generation
Codes are generated server-side by Bird and never returned to your application — there's no leak surface, no way to log a real code by accident. Configurable knobs:
- Length — 4 to 8 digits, default 6.
- Alphabet —
numeric(default),alpha, oralphanumeric.
const { data, error } = await bird.verifications.start({
to: "delivered@bird.dev",
channel: "email",
code_length: 8,
alphabet: "alphanumeric",
});
TTL
Default time-to-live is 10 minutes. Configurable up to a maximum of 60 minutes via the ttl field (in seconds). After TTL the verification transitions to expired and any check call returns failed with reason: "expired".
await bird.verifications.start({
to: "+15005550006",
channel: "sms",
ttl: 1800, // 30 minutes
});
Rate limits per-recipient
Per recipient: 5 verifications per hour, 20 per day. Per IP: 100 per hour. Lower limits available on Scale and above for tighter fraud control. Over-limit calls return 429 rate_limited with Retry-After in the response headers.
Fraud scoring on start
The start response includes a risk_score from 0 to 100 — Bird's signal on the likelihood that the requested verification is fraudulent (number recycling, known fraud rings, velocity anomalies). Recommended: reject sends with risk_score > 80 unless your application has independent signal that overrides it.
const { data, error } = await bird.verifications.start({
to: "+15005550006",
channel: "sms",
});
if (error) throw error;
if (data.risk_score > 80) {
// hand off to step-up review or require an additional signal
}
Check semantics
Exact match required — codes are case-sensitive when the alphabet is alphanumeric, and whitespace is not trimmed. Each check call increments an attempt counter on the verification. Max 5 attempts per verification before lockout; after the fifth wrong attempt the verification transitions to expired and the user must request a new code.
| Check outcome | state | reason |
|---|---|---|
| Correct code, within TTL, attempts left | verified | — |
| Wrong code, attempts remaining | pending | incorrect_code |
| Wrong code, attempts exhausted | failed | max_attempts |
| Past TTL | failed | expired |
| Already verified | failed | already_verified |
Webhooks
Subscribe via the webhooks API to drive your own auth pipeline off verification lifecycle events.
| Event | Fires when |
|---|---|
verification.created | A verification was started and the code was queued for dispatch. |
verification.delivered | The channel confirmed delivery of the code to the recipient. |
verification.checked | A check call was made (right or wrong — payload carries the outcome). |
verification.expired | The verification expired (TTL elapsed or max attempts hit). |
Common patterns
2FA login flow
Start on login attempt, check on form submit. The verification ID is the only state your client needs to hold between the two HTTP requests.
// On POST /login (after password check):
const { data, error } = await bird.verifications.start({
to: user.phone, // recipient phone or email
channel: "sms",
});
if (error) return res.status(500).end();
// Return data.id to the client, render the 6-digit input form.
// On POST /login/verify:
const result = await bird.verifications.check({
id: req.body.verification_id,
code: req.body.code,
});
if (result.error || result.data.state !== "verified") {
return res.status(401).end();
}
// Issue session.
Magic-link variant
Start with mode: "link" to receive a one-tap email or SMS link instead of a typed code. The link encodes the verification ID and code; landing on your callback URL with both query params is equivalent to a successful check.
const { data, error } = await bird.verifications.start({
to: "delivered@bird.dev",
channel: "email",
mode: "link",
callback_url: "https://yourdomain.com/auth/callback",
});
if (error) throw error;
// Email is sent with a button that links to:
// https://yourdomain.com/auth/callback?id=ver_*&code=...
// Your callback handler posts those back to bird.verifications.check.