# Verify your first customer

Bird Verify confirms that someone controls an email address or phone number: you ask Bird to send a one-time passcode, the person types it back into your app, and you ask Bird whether it matched. Bird generates the code, delivers it, stores only a hash, and enforces expiry and attempt limits — so your app never sees or stores the code itself.

This page takes you through it end to end by verifying **your own email address**, which needs no setup at all: email codes go out under Bird's shared [Authifly](https://authifly.com) sender, so there's no domain to verify and no balance to fund. [Verifying a phone number](#verify-a-phone-number-instead) is the same two calls once you've funded SMS.

## 1. Create an API key

In the dashboard, go to [**Developers → API keys**](https://bird.com/dashboard/w/api-keys) and create a key. Keys are scoped to a region and look like `bk_us1_...` or `bk_eu1_...` — the region in the prefix tells you which API host to call: `https://us1.platform.bird.com` or `https://eu1.platform.bird.com`.

![The API Keys page in the Bird dashboard, listing keys with their masked prefix, scopes, and last-used time](/images/docs/dashboard-api-keys.png)

The full key is shown **once**, at creation time. Copy it somewhere safe, then export it for the snippets below:

```bash
export BIRD_API_KEY="bk_us1_..."
```

## 2. Send a code

Create a verification for the address you want to confirm. The only required field is `to` — put your own email address in it so you can read the code that arrives.

### curl

```bash
curl -X POST https://us1.platform.bird.com/v1/verify/verifications \
  -H "Authorization: Bearer $BIRD_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "to": { "email_address": "you@example.com" }
  }'
```

If your key starts with `bk_eu1_`, call `https://eu1.platform.bird.com` instead.

### TypeScript

```bash
npm install @messagebird/sdk
```

```typescript
import { BirdClient } from "@messagebird/sdk";

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

const verification = await bird.verify.verifications.create({
  to: { email_address: "you@example.com" },
});

console.log(verification.id, verification.status); // "vrf_…", "pending"
```

Bird accepts the request and sends the code right away:

```json
{
  "id": "vrf_01jzp0dv4qf6vb1nc23yjc8f2e",
  "status": "pending",
  "to": { "email_address": "you@example.com" },
  "channels": [{ "channel": "email" }],
  "last_channel": "email",
  "expires_at": "2026-07-16T12:24:07Z"
}
```

There's no verification ID to store: the check in step 3 is keyed by the same recipient. The email arrives from **Authifly OTP** `<otp@verify.authifly.com>` with the subject **"Your verification code"** and a six-digit code that's valid for 10 minutes by default.

## 3. Check the code

Take the code from your inbox and submit it, keyed by the same recipient:

### curl

```bash
curl -X POST https://us1.platform.bird.com/v1/verify/verifications/check \
  -H "Authorization: Bearer $BIRD_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "to": { "email_address": "you@example.com" },
    "code": "482913"
  }'
```

### TypeScript

```typescript
const result = await bird.verify.verifications.check({
  to: { email_address: "you@example.com" },
  code: "482913",
});

console.log(result.success); // true
```

A correct code comes back `success: true`, and the embedded verification flips to `verified`:

```json
{
  "success": true,
  "verification": { "id": "vrf_01jzp0dv4qf6vb1nc23yjc8f2e", "status": "verified" }
}
```

That's the whole flow. Two things worth knowing before you wire it into a signup:

- **A wrong code is a `200`, not an error.** You get `success: false` with a `reason` (`incorrect_code`, `expired`, `attempts_exhausted`) and an `attempts_remaining` count — a normal answer to branch on, not an exception. The verification fails for good after 5 wrong tries by default.
- **A verification resolves once.** After it reaches `verified` (or fails or expires), checking it again returns a `404`. Treat the first definitive answer as the answer. If the user needs a new code, call the create endpoint again with the same recipient — an in-progress verification is reused, and a fresh code goes out once the 60-second resend cooldown has passed.

Every verification you create shows up on the [**Verifications**](https://bird.com/dashboard/w/verify/verifications) page in the dashboard, with its status, recipient, channel, and timing — everything except the code itself, which is never stored.

![The Verifications page listing verifications with status, recipient, channel, and creation time columns](/images/docs/dashboard-verify-verifications.png)

## Verify a phone number instead

To verify over SMS, put a phone number in `to` in [E.164](https://en.wikipedia.org/wiki/E.164) format instead of an email address:

```bash
curl -X POST https://us1.platform.bird.com/v1/verify/verifications \
  -H "Authorization: Bearer $BIRD_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "to": { "phone_number": "+15551234567" }
  }'
```

The check is identical — swap `email_address` for the same `phone_number`. Two differences from the email path: SMS draws on your workspace's SMS balance (an unfunded workspace fails the send), and the code is delivered under the Authifly sender ID rather than a message you can brand yet.

## Reach the user on both channels

You don't have to pick one channel up front. Put **both** an `email_address` and a `phone_number` in a single `to`, and Bird resolves a delivery plan from your [country configuration](/docs/guides/verify/countries) — which channels are available for that destination and the order to try them. It works down that plan until a send is accepted, so if the code can't go out over one channel it fails over to the next (SMS to email, or email to SMS) rather than failing outright. Check the code exactly as above, with the same `to` you created the verification with — the user enters whichever code reached them.

## Next steps

- [Sending verifications](/docs/guides/verify/sending-verifications) — options, statuses, resends, settings, and rate limits in depth.
- [Country configuration](/docs/guides/verify/countries) — enable countries and set per-country channel order.
- [Senders & branding](/docs/guides/verify/senders) — what the code messages look like and how to send email from your own domain.
- [Verify API reference](/docs/api/reference/create-verification) — the full request and response schema.