Bird Verify

In preview

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 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
5
6
7
8
9
10
11
12
import { BirdClient } from "@messagebird/sdk";

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

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

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

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 and quotas built in.

    Per-recipient send caps, a per-verification guess limit, and a per-workspace daily quota, 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}.`,
});

Verify API FAQ

How do I add verification to my app?+
Two calls: create a verification for a recipient, then check the code they enter. The check is by recipient, so there's no verification id to store between the two — the configuration id (a constant) and the address are all you need.
Which channels can I verify over?+
Email, SMS, and WhatsApp today. Voice is rolling out as an additional channel, and the channel order, sender, and which channels are on are configurable per country — without a code change.
Do I have to store a verification id?+
No. Re-sending is the same create call (create-or-retry), and checking is by the recipient address, so a verification needs no per-request state on your side. An id is returned if you want to read status later, but it's optional.
What happens when someone enters the wrong code?+
The check returns 200 with a result — invalid while attempts remain, then failed once they're exhausted, or expired once the window passes. A wrong code is a normal outcome you branch on, not an HTTP error.
How are the codes generated and stored?+
Codes are generated with a cryptographic random source and stored only as an HMAC-SHA256 hash, compared in constant time. The plaintext code is never returned and never written to your stack or our logs. Length (4–10 digits), TTL, and attempt limits are configurable.
How much does it cost?+
Verify is in preview and isn't on the public price list yet. Get in touch and we'll walk you through pricing for your channels and volume.
Who do my users see the code from?+
Authifly, Bird's verification brand. It's the identity on every code your users receive: email arrives from otp@verify.authifly.com or your own verified domain, and SMS and WhatsApp are Authifly-branded. authifly.com is a public page that reassures recipients Authifly sends legitimate one-time codes on a business's behalf. Bird is the platform you build on; Authifly is what the recipient sees.

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.

Verify is in preview. 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