Coming Soon

The OTP API for developers who need codes to land.

SMS-first, voice fallback in one attribute, fraud scoring at start time, per-recipient rate limits. Same auth, same idempotency, same webhooks as every other Bird channel — because the same engineering team built them all.

verify.ts
200 · 1.4s
import { BirdClient } from "@bird/sdk";

const bird = new BirdClient({ apiKey: process.env.BIRD_API_KEY! });

// 1. Start the verification.
const { data: start, error: startErr } = await bird.verifications.start({
  to:       "+15005550006",
  channel:  "sms",
  fallback: "voice",
  expires:  "10m",
}).safe();

if (startErr) throw startErr;
// → { id: "ver_8sQ91pZ4...", risk_score: 0.04, status: "pending" }

// 2. Check the code the user typed.
const { data: check, error: checkErr } = await bird.verifications.check({
  id:   start.id,
  code: "482917",
}).safe();

if (checkErr) throw checkErr;
console.log(check.status);
// → "approved"

5 minutes from npm install to first verification

Start a verification from the language you already use.

SDKs in every major runtime. The first verification goes to the sanctioned test number (+15005550006), so you can ship a CI check before you talk to a carrier.

1
2
3
4
5
6
7
8
9
10
import { BirdClient } from "@bird/sdk";

const bird = new BirdClient({ apiKey: process.env.BIRD_API_KEY! });

const { data, error } = await bird.verifications.start({
  to:       "+15005550006",
  channel:  "sms",
  fallback: "voice",
  expires:  "10m",
}).safe();

Ten things between the start call and the user's next screen.

Concrete primitives, named and audit-able. No "AI-powered fraud detection" hand-waving.

  1. 01

    SMS-first, voice fallback in one attribute

    Pass fallback: "voice" on start. If SMS doesn't land in N seconds, we call instead. No second integration.

  2. 02

    Fraud scoring at start time

    Every start returns a risk_score 0-1. Block the high-risk ones before you spend a cent on delivery.

  3. 03

    Per-recipient rate limits

    Configurable per-phone-number caps on attempts per hour, per day. Locks out brute-force on your dime.

  4. 04

    Carrier-aware routing

    We pick the route per carrier per country in real time. T-Mobile US is not the same wire as Reliance Jio.

  5. 05

    Codes generated server-side

    You never see the code; we never expose it on the wire on the way out. One less leak surface in your stack.

  6. 06

    Configurable TTL

    Default 10 minutes, range 30s-1h. Shorter TTL = lower fraud surface; longer = better mobile UX.

  7. 07

    Attempt counter with lockout

    Max attempts per verification (default 5). After lockout, check returns verification_locked for a clear UX.

  8. 08

    Voice OTP in 40+ languages

    Synthesized at send time, accent-correct per locale. Same bird.verifications.start surface — just channel: "voice".

  9. 09

    Webhook on every state change

    Events: verification.created, verification.delivered, verification.checked, verification.expired. Same HMAC envelope.

  10. 10

    Pay only when the code lands

    No charge for verification.failed. The fraud-score filter and the no-delivery refund keep the spend honest.

Why we build Verifications

Because codes have to land on the first try, and we can't ask carriers nicely.

OTP is the channel where every percent of delivery costs you a signup. We've run SMS for ten years across 240 direct-to-carrier connections, so when a code doesn't land we know whether it's the route, the carrier, the handset, or the fraud filter — and we route around it in real time. Bird Verifications is that route-selection logic, plus the fraud score, plus the voice fallback, plus the attempt counter, exposed as two endpoints with the same auth and webhook contract as every other Bird channel.

verify.ts
200 · 1.4s
import { BirdClient } from "@bird/sdk";

const bird = new BirdClient({ apiKey: process.env.BIRD_API_KEY! });

// 1. Start the verification.
const { data: start, error: startErr } = await bird.verifications.start({
  to:       "+15005550006",
  channel:  "sms",
  fallback: "voice",
  expires:  "10m",
}).safe();

if (startErr) throw startErr;
// → { id: "ver_8sQ91pZ4...", risk_score: 0.04, status: "pending" }

// 2. Check the code the user typed.
const { data: check, error: checkErr } = await bird.verifications.check({
  id:   start.id,
  code: "482917",
}).safe();

if (checkErr) throw checkErr;
console.log(check.status);
// → "approved"

Every state change is a webhook.

HMAC-signed payloads, replay-protected, idempotent. The same envelope on every Bird channel — learn one, you've learned them all.

POST /webhooks/bird
signed
{
  "type": "verification.delivered",
  "id":   "evt_5kQ81y...",
  "created_at": "2026-05-19T15:42:01.221Z",
  "data": {
    "verification_id": "ver_8sQ91pZ4",
    "to":              "+15005550006",
    "channel":         "sms",
    "carrier":         "T-Mobile USA",
    "risk_score":      0.04,
    "latency_ms":      1421
  }
}

Retry schedule: 5s, 30s, 5m, 30m, 2h, 6h, 12h. Dead-letter after the final attempt; every dead-lettered event is replayable from the dashboard or API.

  • verification.createdAccepted by the API; about to dispatch on the first channel.
  • verification.deliveredCarrier confirmed receipt on the recipient handset.
  • verification.checkedThe recipient submitted a code; payload includes approved or denied.
  • verification.fellbackSMS did not land in window; the voice fallback was dispatched.
  • verification.expiredTTL elapsed without a successful check.
  • verification.lockedMax attempts hit; further check calls return verification_locked.
  • verification.failedPermanent failure before send (invalid recipient, carrier reject).

If you've integrated SMS, you've integrated Verifications.

Same auth, same idempotency, same error envelope, same webhook shape. The difference is that Verifications generates the code, picks the route, runs the fraud filter, and handles the fallback — so you don't.

Verifications.

verifications
await bird.verifications.start({
  to:       "+15005550006",
  channel:  "sms",
  fallback: "voice",
});

One call. We pick the channel, the route, generate the code, run the fraud check, handle the lockout.

SMS.

sms
await bird.sms.send({
  from: "Bird",
  to:   "+15005550006",
  text: `Your code is ${code}.`,
});

The raw send, for when you want to own the code generation and the retry policy yourself.

From $0.04 per verification, all-in.

Priced per successful verification, SMS or voice. No charge for verification.failed. Volume discounts auto-apply above 100K/mo. No platform fee, no seat fee, no tier features locked behind annual commits.

Start with one channel.
Add the others when you're ready.

A test API key is yours immediately. Production unlocks when you add a payment method and verify a sender.

Get startedRead docsor

Using Claude Code, Cursor, or Codex? Point it at our MCP server — tools for every channel we expose, with scoped agent keys.

Cursor