> ## Documentation Index
> Fetch the complete documentation index at: https://docs.useboom.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# One-time codes

> Send a login or verification code over WhatsApp and check what the person typed, with two API calls.

A **one-time code** is the short number you ask a person to type to prove they
control a phone: a login, a sign-up, a booking confirmation. Boom sends the code
over WhatsApp and remembers it; your backend then asks Boom whether what the
person typed is right. Boom handles the code itself (generating it, resending
it, expiring it, limiting wrong guesses). What a valid code unlocks stays in
your app.

Today codes go out **over WhatsApp only**.

## Before you start

* One-time codes are available to every workspace. You need a connected
  WhatsApp number and an approved authentication template.
* Go to **One-time codes** in the dashboard and choose the WhatsApp number the
  codes are sent from, then an approved **authentication template** (or create
  one there; it must be approved by WhatsApp first). Turn codes on.
* The **template sets the message and the expiry.** WhatsApp writes the text of an
  authentication message, and the minutes a code stays valid are the ones set on
  the template. A request can't change either. If the template shows no expiry,
  Boom treats the code as valid for 10 minutes.
* Use an [API key](https://docs.useboom.ai/authentication.md) from your backend.

> [!WARNING]
>
> **Any of your organization's API keys can send and verify codes.** Keys are
> not scoped to endpoints today, so create a dedicated key for the backend that
> sends codes, and never put it in a browser or mobile app.

The examples use these two variables. Note that the base here is the host, not
the `/api/v1/cdp` base other pages use:

```bash theme={null}
export BASE="https://www.useboom.ai"   # or https://dev.useboom.ai
export BOOM_KEY="your-api-key"
```

## Send a code

```bash theme={null}
curl -X POST "$BASE/api/v1/otp/send" \
  -H "Authorization: Bearer $BOOM_KEY" \
  -H "Content-Type: application/json" \
  -d '{"phone":"+5215512345678"}'
```

```json theme={null}
{
  "otpId": "cm9x4k2a10000",
  "status": "PENDING",
  "expiresAt": "2026-11-09T15:10:00.000Z",
  "resent": false,
  "reference": null,
  "personExternalId": null
}
```

Pass **exactly one** of:

| Field | Notes |
| - | - |
| `phone` | The recipient in E.164 format, e.g. `+5215512345678`. |
| `personExternalId` | Your id for a [person](https://docs.useboom.ai/cdp/overview.md) already in Boom; the code goes to their stored phone. `422 recipient_has_no_phone` if they have none. |

`reference` (optional, up to 255 characters) is your label for what the code is
for, such as an appointment id. It is echoed back and can be used to verify.

Keep the `otpId`: it is the simplest way to verify.

## Verify a code

```bash theme={null}
curl -X POST "$BASE/api/v1/otp/verify" \
  -H "Authorization: Bearer $BOOM_KEY" \
  -H "Content-Type: application/json" \
  -d '{"otpId":"cm9x4k2a10000","code":"123456"}'
```

```json theme={null}
{ "valid": true, "status": "VERIFIED", "attemptsRemaining": 5 }
```

Identify the code with **exactly one** of `otpId`, `phone` or `personExternalId`
(the last two together with the same `reference` you sent with, if any). `code`
is the 6 to 10 digits the person entered.

A wrong code is **not an HTTP error**: you get `200` with `valid: false`.

| `status` | Means |
| - | - |
| `PENDING` | Still open. If `valid` is `false`, the person can try again |
| `VERIFIED` | Correct. A code verifies **once**; checking it again returns `valid: false` |
| `EXPIRED` | The template's expiry passed |
| `LOCKED` | Too many wrong guesses; send a new code |

`attemptsRemaining` counts the wrong guesses left before the code locks. The
limit is set on the dashboard (5 by default).

Boom also stops guessing across codes: after 20 wrong codes for the same phone
within 24 hours, further attempts are refused (`valid: false`, the code stays
`PENDING`, `attemptsRemaining` doesn't move) until older failures age out.

`404 otp_not_found` means no code matches. `422 otp_unverifiable` means Boom
can no longer check that code; send a new one.

## Resending

Calling **send** again for the same person while their code is still valid sends
**the same code** again and returns the same `otpId` with `resent: true`. The
expiry does not move. Once the code has expired, the next send creates a new one.

Two limits apply, both set on the dashboard (defaults shown):

* **Cooldown** (30 seconds): a send to a phone too soon after the last send to it is refused.
* **Hourly cap** (5 sends per phone per hour).

Both count **per phone**, across every person and `reference` on it. If the code
is about to expire when you call send, Boom issues a **new code with a new
`otpId`** (`resent: false`) instead of resending one the person can barely use.

Both answer `429`, and the error body tells you how long to wait:

```json theme={null}
{
  "error": {
    "code": "resend_cooldown",
    "message": "A code was just sent to this recipient. Try again later.",
    "retryAfterSeconds": 12
  }
}
```

`code` is `resend_cooldown` or `too_many_sends`. Read `retryAfterSeconds` from the
body: these `429`s don't set a `Retry-After` header. Two requests for the same
phone at the same moment are treated as one, and the second gets
`resend_cooldown`.

## People who share a phone

A family, a clinic front desk or a shared work phone can mean several people on
one number. By default a phone has **one live code**. To give each person their
own, pass `personExternalId` or `reference` on **both** send and verify:

```bash theme={null}
curl -X POST "$BASE/api/v1/otp/send" \
  -H "Authorization: Bearer $BOOM_KEY" -H "Content-Type: application/json" \
  -d '{"phone":"+5215512345678","reference":"appointment_881"}'

curl -X POST "$BASE/api/v1/otp/verify" \
  -H "Authorization: Bearer $BOOM_KEY" -H "Content-Type: application/json" \
  -d '{"phone":"+5215512345678","reference":"appointment_881","code":"123456"}'
```

A code sent for one `reference` (or person) never verifies another's. A verify by
bare `phone` only matches codes that were sent without either. The cooldown and
the hourly cap count per **phone**, not per person: one patient's send also
starts the cooldown for the others on that number.

## Errors

Errors use the [standard shape](https://docs.useboom.ai/rate-limits-and-errors.md#error-shape).

| Status | `code` | When |
| - | - | - |
| `400` | `validation_failed` | Not exactly one of `phone` / `personExternalId` (or `otpId` / `phone` / `personExternalId` on verify), or a malformed `code` |
| `401` | | Missing or invalid API key |
| `404` | `otp_not_found` | No code matches (verify) |
| `422` | `otp_not_configured` | No number and template chosen, or the template or number stopped being usable. Check the dashboard page |
| `422` | `recipient_has_no_phone` | Invalid phone, unknown `personExternalId`, or a person with no phone |
| `422` | `recipient_suppressed` | The recipient is on Do Not Contact |
| `422` | `unpriced` | The send has no price on your plan yet. Contact Boom |
| `422` | `otp_unverifiable` | The code can no longer be checked; send a new one |
| `429` | `resend_cooldown`, `too_many_sends` | See [Resending](#resending); wait `retryAfterSeconds` |
| `503` | `send_failed` | WhatsApp didn't accept the message. Safe to retry |

> [!NOTE]
>
> Boom does not apply a limit across your whole organization: only the
> per-phone cooldown, hourly cap and wrong-guess limits above. Protecting your
> endpoint from abuse (for example someone requesting codes for numbers that
> aren't theirs) is up to your app.

## Billing and the inbox

Each code that WhatsApp **delivers** is billed as an authentication template
message, and so is each delivered resend. The **Send a test code** button on the
dashboard sends and bills a real code too.

Codes appear in the person's conversation like any outbound message, but the code
itself is shown as dots (`••••••`) in the inbox and in the stored message. Boom keeps the code encrypted only while it can
still be resent and clears it once the code is verified, expired or locked. Code
records, with the phone number they were sent to, are deleted after 90 days.

Sending a code doesn't reopen a closed conversation or assign it to anyone.

## Related

* [Authentication](https://docs.useboom.ai/authentication.md): API keys and environments
* [Message logs](https://docs.useboom.ai/message-logs.md): delivery status for each code sent
* [Rate limits & errors](https://docs.useboom.ai/rate-limits-and-errors.md)


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.