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.
  • Alphabetnumeric (default), alpha, or alphanumeric.
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 outcomestatereason
Correct code, within TTL, attempts leftverified
Wrong code, attempts remainingpendingincorrect_code
Wrong code, attempts exhaustedfailedmax_attempts
Past TTLfailedexpired
Already verifiedfailedalready_verified

Webhooks

Subscribe via the webhooks API to drive your own auth pipeline off verification lifecycle events.

EventFires when
verification.createdA verification was started and the code was queued for dispatch.
verification.deliveredThe channel confirmed delivery of the code to the recipient.
verification.checkedA check call was made (right or wrong — payload carries the outcome).
verification.expiredThe 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.

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.

Inizia con un canale.
Aggiungi gli altri quando sei pronto.

Una chiave API di test è subito tua. La produzione si sblocca quando aggiungi un metodo di pagamento e verifichi un mittente.

Usi Claude Code, Cursor o Codex? Puntali al nostro server MCP — 141 strumenti, uno per endpoint API, con chiavi agent a scope limitato.