Bird Verify

The verification API with nothing to store.

Set up in:
Cursor

Send a one-time code over email, SMS, or WhatsApp, then check it by recipient, with no verification id to keep between the two calls. The channel order, sender, and code rules are configuration per country, not a rebuild. Same auth and idempotency as every other Bird channel, because the same team built them all. Voice is rolling out next.

verify.ts
200 · pending
import { BirdClient } from "@messagebird/sdk";

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

// Send the code, then check it by recipient.
await bird.verify.verifications.create({
  to: { phone_number: "+15551234567" },
}).safe();

const { data } = await bird.verify.verifications.check({
  to:   { phone_number: "+15551234567" },
  code: userInput,
}).safe();

Two calls from npm install to a verified user

Send a code, then check it, in the language you already use.

Create-or-retry sends the code; check confirms it by recipient. Two calls, and no verification id to thread between them.

1
2
3
4
const verification = await bird.verify.verifications.create({
  to: { phone_number: "+15551234567" },
});
console.log(verification.id, verification.status);

Ten things you don't build when verification is the API.

Concrete primitives, named and configurable. No hand-waving.

  1. 01

    Create or retry in one call.

    Re-post the same recipient and we resume the live session and resend once the cooldown passes. No separate resend endpoint, no duplicate verifications.

  2. 02

    Check by target. Store nothing.

    Submit the recipient and the code; we resolve the session from the configuration-and-recipient pair. There's no verification id to persist between send and check.

  3. 03

    Email, SMS, and WhatsApp at launch.

    The recipient you pass picks the channel: an email address verifies by email, a phone number by SMS or WhatsApp. Switching channels is a one-field change, not a new integration. Voice is rolling out next, and automatic fallback between channels is on the way.

  4. 04

    Per-country channel order, as config.

    Set the channel order, the sender, and which channels are on, per country, as a first-class configuration resource rather than a support ticket.

  5. 05

    Codes you never see.

    Generated with a cryptographic random source, stored only as an HMAC, compared in constant time. The plaintext code never touches your stack or our logs.

  6. 06

    Configurable code, TTL, attempts.

    Six-digit default, 4–10 configurable; a 10-minute window; 5 attempts; a 60-second resend cooldown, set per configuration. Code length can also be overridden per request.

  7. 07

    Every code stays valid until the session ends.

    A delayed message and a freshly resent code both verify, because we don't invalidate the earlier code when a new one goes out.

  8. 08

    A wrong code is a 200, not an exception.

    Check answers with a boolean result — did this code verify, yes or no — and a reason that elaborates when it didn't: invalid, expired, already verified, or out of attempts. You branch on a field, never on a thrown error.

  9. 09

    Rate limits built in.

    Per-recipient send caps and a per-verification guess limit, each a 429 with Retry-After, so brute force runs out before you do.

  10. 10

    Same contract as the rest of Bird.

    Bearer auth, an idempotency key, typed vrf_ ids, one error envelope. The handler you wrote for email already fits verification.

Store nothing between send and check.

Most verification APIs hand you an id to persist, look up, and submit the code against. Bird resolves the session from the recipient, so there's no per-verification state on your side.

Most verification APIs

Create returns an id you store, then look the verification up to check the code against it.

id-keyed.ts
const { id } = await api.verifications.create({
  to: "+15551234567",
});
// persist id somewhere, then later…
await api.verifications.check({ id, code });

Bird Verify

Check by recipient. There's nothing to thread between the two calls.

by-target.ts
await bird.verify.verifications.create({
  to: { phone_number: "+15551234567" },
}).safe();
// no id to store; check by the same recipient
await bird.verify.verifications.check({
  to: { phone_number: "+15551234567" }, code,
}).safe();

Per-country routing is configuration.

Set the channel order, sender, and which channels are on per country: WhatsApp-first in one market, SMS-only in another. It's a first-class config resource, resolved into each verification's plan. See channel orchestration.

per-country.ts
200
await bird.verify.verifications.configurations.countries.upsert(
  "vfc_login",
  "BR",
  { channels: [
    { channel: "whatsapp", state: "enabled" },
    { channel: "sms", state: "enabled" },
  ] },
).safe();

Verification is also a product decision: the same API powers two-factor authentication and passwordless login. Validating a number first? Pair it with Lookup. Silent network authentication and TOTP authenticator apps are on the roadmap.

Why we build Verify

Because the code that lets a user in shouldn't need its own database table.

OTP is the channel where a code that doesn't arrive is a signup that doesn't happen. Bird already runs email and SMS at scale, so Verify is that delivery plus the code generation, the session, the per-country channel plan, and the rate limits, behind two endpoints that store nothing on your side and answer with the same shape as every other Bird channel.

verify.ts
200 · pending
import { BirdClient } from "@messagebird/sdk";

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

// Send the code, then check it by recipient.
await bird.verify.verifications.create({
  to: { phone_number: "+15551234567" },
}).safe();

const { data } = await bird.verify.verifications.check({
  to:   { phone_number: "+15551234567" },
  code: userInput,
}).safe();

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

Same auth model, same idempotency contract, same error envelope. The difference is that Verify generates the code, picks the channel, and runs the rate limits, so you don't.

Verify

One call sends the code; one call checks it by recipient. We own the code, the session, and the limits.

verify.ts
await bird.verify.verifications.create({
  to: { phone_number: "+15551234567" },
});

SMS

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

notify.ts
await bird.sms.send({
  from:     "Bird",
  to:       "+15551234567",
  text:     `Your code is ${code}.`,
  category: "authentication",
});

The sender your users see: Authifly

Your end users receive their codes from Authifly, Bird's verification brand. On the shared senders the OTP email comes from otp@verify.authifly.com and the SMS names Authifly in the message, so codes arrive under one consistent identity you don't have to operate. If a recipient gets an unexpected code, authifly.com reassures them that Authifly sends legitimate one-time codes on a business's behalf. Authifly is operated by Bird B.V.

Visit authifly.com

Verification on the same platform as the rest of your messaging.

Start a build today, or talk to us about the channels, volume, and pricing you need.

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.

Using Claude Code, Cursor, or Codex? Copy a setup prompt and your agent installs the Bird CLI and skills for you. Pick yours:

Cursor