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.
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.
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.
- 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.
- 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.
- 03
Per-recipient rate limits
Configurable per-phone-number caps on attempts per hour, per day. Locks out brute-force on your dime.
- 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.
- 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.
- 06
Configurable TTL
Default 10 minutes, range 30s-1h. Shorter TTL = lower fraud surface; longer = better mobile UX.
- 07
Attempt counter with lockout
Max attempts per verification (default 5). After lockout, check returns verification_locked for a clear UX.
- 08
Voice OTP in 40+ languages
Synthesized at send time, accent-correct per locale. Same bird.verifications.start surface — just channel: "voice".
- 09
Webhook on every state change
Events: verification.created, verification.delivered, verification.checked, verification.expired. Same HMAC envelope.
- 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.
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.
{
"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.
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.
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.