Bird Verify
In previewThe verification API with nothing to store.
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.
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.
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.
- 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.
- 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.
- 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.
- 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.
- 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.
- 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.
- 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.
- 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.
- 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
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.
Explore the Verify platform
Each capability in depth. One API, one set of keys.
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.
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.
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.
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.
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.
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.
await bird.sms.send({
from: "Bird",
to: "+15551234567",
text: `Your code is ${code}.`,
category: "authentication",
});Verify API FAQ
How do I add verification to my app?+
Which channels can I verify over?+
Do I have to store a verification id?+
What happens when someone enters the wrong code?+
How are the codes generated and stored?+
How much does it cost?+
Who do my users see the code from?+
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.comVerification 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.