Anti-abuse & code security

In preview

The code is a secret. We treat it like one.

Set up in:
Cursor

A one-time code is only as good as the way it's generated, stored, and rate-limited. Bird Verify generates codes with a cryptographic source, stores only a hash, compares in constant time, and caps both sends and guesses — so a leaked log or a brute-force loop gets an attacker nowhere. Fraud scoring builds on this base 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();

Security that's on by default, not an add-on.

Every verification on the Bird Verify API carries the same protections: the code is generated server-side, never returned, and stored only as a hash; checks run in constant time and against a bounded attempt budget; and sends are capped per recipient and per workspace. You don't opt in or wire these up — they're how the API behaves, whether you run it as two-factor login or passwordless sign-in.

Five protections on every verification.

No setup step, no add-on SKU.

  1. 01

    Cryptographic generation.

    Codes are drawn from a cryptographic random source, uniform across the code space, not a predictable counter or timestamp.

  2. 02

    Hashed at rest, never on the wire out.

    Only an HMAC-SHA256 of each code is stored; the plaintext is never returned by the API and never written to your stack or our logs.

  3. 03

    Constant-time comparison.

    Submitted codes are compared in constant time, so an attacker learns nothing from how long a check takes.

  4. 04

    Attempt lockout.

    Each session has a bounded number of checks (5 by default). Once they're spent the session fails, so guessing can't run forever.

  5. 05

    Send caps and quotas.

    A per-recipient send cap, a resend cooldown, and a per-workspace daily quota bound the spend and the abuse surface, each a 429 with Retry-After.

Guessing runs out before your users do.

A wrong code comes back as a result with the attempts remaining, and the session fails once the budget is spent, so a brute-force loop hits a wall instead of an open door.

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

// wrong code, attempts left → { result: "invalid", attempts_remaining: 2 }
// budget spent, session done → { result: "failed", attempts_remaining: null }
Coming next

Coming next: fraud signals and SMS-pumping protection.

The per-send history Verify records today is the groundwork for a fraud layer we're building now. It rides the same create and check calls — so adopting it later is a config change, not a re-integration.

Risk signals on create. Pass device, IP, and request context on a verification, and high-risk attempts get a blocked outcome before a code is ever sent — so you're not paying to message an attacker.

SMS-pumping and AIT protection. Per-country and per-prefix send caps plus a per-workspace spend ceiling shut down the artificially-inflated-traffic attack that drives OTPs to premium number ranges for carrier revenue share.

Built on what's already there. Risk decisioning reads the attempt history Verify keeps from day one, and the blocked outcome is already part of the status model — so the fraud layer lands without reshaping your integration.

Verification security FAQ

Where is the one-time code stored?+
Only as an HMAC-SHA256 hash. Bird generates the code with a cryptographic random source, sends it, and keeps the hash to compare against — the plaintext is never returned by the API or written to logs.
How do you stop someone brute-forcing the code?+
Each session has a bounded number of checks (5 by default), and each one is compared in constant time so timing leaks nothing. Once the budget is spent the session fails, so an attacker can't keep guessing.
What about SMS pumping and artificially inflated traffic?+
Per-recipient send caps and a per-workspace daily quota bound the spend today. Dedicated fraud scoring and SMS-pumping protection are coming next, building on the per-send history Verify already records.
Do these protections cost extra?+
No. Cryptographic generation, hashed storage, constant-time checks, attempt lockout, and send caps are how every verification behaves — there's no security tier to buy.
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.

Codes generated, stored, and rate-limited the way they should be.

Security is built into Bird Verify, not sold on top: the channels, the code, and the limits are the same two endpoints.

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