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

# OTP (One-Time Passwords)

> Overview of Briq's OTP API — prerequisites, behavioral rules, and the end-to-end verification flow.

OTP (one-time passwords) are short, time-limited numeric codes sent to a user's phone to confirm identity for sign-in, registration, or sensitive actions. Briq's OTP API generates the code, delivers it via **SMS**, **voice call**, or **WhatsApp**, and then verifies what the user submits — all scoped to a Developer App resolved from your bound API key.

This page is the integrator's overview: what to set up, the rules the API enforces, and how the pieces fit together. For payload-level detail, see the [Karibu OTP API reference](/Karibu-OTP/index).

## Prerequisites

To use OTP services you need:

1. A **workspace** with `developer_access = true`. Toggle this in the Briq UI under workspace settings, or see [Workspaces](/guides/workspaces).
2. A **Developer App** linked to that workspace. See [Developer Apps](/guides/developer-apps).
3. An **active developer API key** (`X-API-Key` header) **bound to that Developer App** — at key creation or via the attach endpoint. Karibu resolves the app from the key; bound keys need only `X-API-Key` on every OTP call.
4. Recipient phone numbers in **E.164 digits-only** format (no `+`, no spaces) — e.g. `255712345678`.

<Note>
  Bind every OTP API key to a Developer App and keep it server-side. Never expose your API key to client apps or browsers — have your frontend call your backend, which then calls Karibu with `X-API-Key` only.

  Legacy integrations with an **unbound** API key may scope requests via the optional `X-App-ID` header or an `app_key` field in the body. New integrations should bind the key instead.
</Note>

## Behavioral rules to internalize

These rules drive correct UX and prevent the most common integration bugs. Read them once before writing code.

| Rule                                     | Detail                                                                                                                                                                                                                                     |
| ---------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| **Single active OTP per `(phone, app)`** | `request` and `resend` invalidate any prior unused, unexpired OTP for the same phone + app **before** issuing a new one. There is never more than one active OTP for a phone within an app.                                                |
| **Hashed at rest**                       | OTP codes are bcrypt-hashed in the database. The plaintext is delivered only via SMS/call to the recipient. The server will never return it in any response.                                                                               |
| **3-attempt cap on `verify`**            | After 3 wrong codes the OTP is auto-locked (marked used) and `remaining_attempts` becomes `0`. The user must request a new OTP.                                                                                                            |
| **Phone format**                         | E.164 digits only, no `+`. Invalid input → 400 `"Invalid phone number"`.                                                                                                                                                                   |
| **Cross-app isolation**                  | Verify, resend, invalidate, and status all operate only on OTPs issued under the same Developer App (resolved from your bound API key). An OTP issued under app A cannot be touched via app B.                                             |
| **Default expiry**                       | 10 minutes. Configure per call via `minutes_to_expire`.                                                                                                                                                                                    |
| **Default OTP length**                   | 6 digits. Configure per call via `otp_length`.                                                                                                                                                                                             |
| **Delivery channel**                     | `"sms"` (default), `"call"`, or `"whatsapp"`. Each has its own payload shape — see the per-channel tabs in [Requesting OTP codes](/guides/otp-requesting-codes).                                                                           |
| **Channel-specific fields**              | `sender_id` and `message_template` are **SMS-only** — silently ignored on `"call"` and `"whatsapp"`. Voice reads the code via TTS; WhatsApp uses the approved `briq_otp` template.                                                         |
| **Optional per-request callbacks**       | Pass `callback_url` on `request`/`resend` to receive async `flake.verified` / `flake.failed` webhooks on verify outcomes. Best-effort — use `/verify` as the auth source of truth. See [Flake lifecycle callbacks](/guides/otp-callbacks). |

## End-to-end flow

A typical phone-verification flow:

1. **User enters phone** in your UI → your backend calls `POST /v1/otp/request` with the appropriate `delivery_method`. → see [Requesting OTP codes](/guides/otp-requesting-codes).
2. **User receives SMS, voice call, or WhatsApp message** with the plaintext code.
3. **User submits code** in your UI → your backend calls `POST /v1/otp/verify`. → see [Validating OTP codes](/guides/otp-validating-codes).
   * On `success: true` → mark phone verified and continue.
   * On `success: false` → drive the UX off `data.remaining_attempts`.
4. **User clicks "Resend"** → your backend calls `POST /v1/otp/resend` (which invalidates the prior code automatically). You may switch channels here — e.g. retry on WhatsApp after an SMS didn't arrive. → see [Managing OTP lifecycle](/guides/otp-lifecycle).
5. **User logs out / changes phone** → your backend calls `POST /v1/otp/invalidate`.
6. **Optional**: poll `GET /v1/otp/status` to drive countdown timers or detect that an OTP is already in-flight before issuing a new one.
7. **Optional**: pass `callback_url` on `request` or `resend` if you want async `flake.verified` / `flake.failed` notifications at your HTTPS endpoint when the user verifies. → see [Flake lifecycle callbacks](/guides/otp-callbacks).

## Standard response envelope

Every OTP endpoint returns the same JSON envelope:

```json theme={null}
{
  "success": true,
  "message": "Human-readable summary",
  "data": { /* endpoint-specific payload, or null */ },
  "status_code": 200
}
```

Inspect `body.success` and `body.status_code` rather than the raw HTTP status — `/v1/otp/status` returns HTTP 200 wrapping `status_code: 404` when no active OTP exists.

Auth, host, and validation errors (401/403/422) follow FastAPI's `{"detail": "..."}` shape instead — they bypass the envelope.

***

For complete payload tables, error matrices, and copy-pasteable client snippets in cURL, Python, Node.js, and PHP, see the [Karibu OTP API reference](/Karibu-OTP/index).

<Note>
  Need to send WhatsApp messages beyond OTP, such as notifications, templates, media, or interactive flows? See the [WhatsApp guide](/guides/whatsapp).
</Note>
