> ## 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.

# Requesting OTP Codes

> Generate a fresh OTP and deliver it to a phone number via SMS, voice call, or WhatsApp using POST /v1/otp/request.

Use `POST /v1/otp/request` to generate a fresh numeric OTP and deliver it to a phone via SMS (default), voice call, or WhatsApp. Requests are scoped to the Developer App bound to your `X-API-Key` — Karibu resolves the app automatically; you do not send `app_key` in the body when the key is bound. Calling it always invalidates any prior unused, unexpired OTP for the same `(phone, app)` before issuing the new one, so there is never more than one active OTP for a phone within an app.

Each channel has its own payload shape. The fields `sender_id` and `message_template` are **SMS-only** — they exist for SMS branding/wording control and are silently ignored on `"call"` and `"whatsapp"`. Voice reads the code via TTS; WhatsApp routes through the platform-managed `briq_otp` template with automatic sender resolution.

Default to `"sms"` for the broadest reach. Use `"call"` for SMS-restricted regions or accessibility needs, and `"whatsapp"` for app-installed users with chat-first habits.

## Endpoint

```http theme={null}
POST https://karibu.briq.tz/v1/otp/request
```

**Headers:** `X-API-Key` (required — bind to a Developer App for OTP), `Content-Type: application/json` (required).

## Common request fields

These fields are the same on every channel:

| Field               | Type                  | Required          | Default | Notes                                                                                                       |
| ------------------- | --------------------- | ----------------- | ------- | ----------------------------------------------------------------------------------------------------------- |
| `phone_number`      | string (E.164 digits) | yes               | —       | Digits only, no `+`. e.g. `255712345678`.                                                                   |
| `app_key`           | string                | no                | —       | Omit when API key is bound to a developer app. Legacy unbound keys may send this or use `X-App-ID` instead. |
| `delivery_method`   | string                | yes (recommended) | `"sms"` | One of `"sms"`, `"call"`, `"whatsapp"`.                                                                     |
| `otp_length`        | int                   | no                | `6`     | Length of the generated code.                                                                               |
| `minutes_to_expire` | int                   | no                | `10`    | TTL in minutes.                                                                                             |
| `callback_url`      | string (HTTPS)        | no                | —       | Optional per-request webhook URL for async flake lifecycle events. Active for this OTP's lifetime.          |
| `callback_secret`   | string                | no                | —       | Optional HMAC secret for `X-Briq-Signature` on callbacks.                                                   |

<Note>
  `callback_url` and `callback_secret` are optional. When set, Karibu associates them with this OTP and POSTs `flake.verified` or `flake.failed` to your URL after verify outcomes. See [Flake lifecycle callbacks](/guides/otp-callbacks) for the full flow and testing guide.
</Note>

## Success response

The same shape on every channel:

```json theme={null}
{
  "success": true,
  "message": "OTP Code sent successfully.",
  "data": { "expires_at": "2026-05-08T12:34:56.000000" },
  "status_code": 200
}
```

<Warning>
  The plaintext code is **never** returned. The recipient receives it via SMS, voice call, or WhatsApp only. Don't try to capture or log it on the server.
</Warning>

## Channel-specific payload & code samples

<Tabs>
  <Tab title="SMS">
    Default channel. Customisable per call via `sender_id` and `message_template`.

    **Channel-specific fields:**

    | Field              | Type   | Required | Default                 | Notes                                                                                  |
    | ------------------ | ------ | -------- | ----------------------- | -------------------------------------------------------------------------------------- |
    | `sender_id`        | string | no       | `OTP_DEFAULT_SENDER_ID` | The SMS sender ID shown to the recipient. Must be approved for your account.           |
    | `message_template` | string | no       | server default          | Must contain `{code}`. `{expiry}` is also substituted. Falls back if `{code}` missing. |

    **Exact payload:**

    ```json theme={null}
    {
      "phone_number": "255712345678",
      "delivery_method": "sms",
      "otp_length": 6,
      "minutes_to_expire": 10,
      "sender_id": "BRIQ OTP",
      "message_template": "Your verification code is {code}. It expires in {expiry} minutes."
    }
    ```

    <CodeGroup>
      ```bash cURL theme={null}
      curl -X POST https://karibu.briq.tz/v1/otp/request \
        -H "X-API-Key: YOUR_API_KEY" \
        -H "Content-Type: application/json" \
        -d '{
          "phone_number": "255712345678",
          "delivery_method": "sms",
          "otp_length": 6,
          "minutes_to_expire": 10,
          "sender_id": "BRIQ OTP",
          "message_template": "Your verification code is {code}. It expires in {expiry} minutes."
        }'
      ```

      ```python Python theme={null}
      import requests

      BASE_URL = "https://karibu.briq.tz"
      API_KEY  = "YOUR_API_KEY"
      resp = requests.post(
          f"{BASE_URL}/v1/otp/request",
          headers={"X-API-Key": API_KEY, "Content-Type": "application/json"},
          json={
              "phone_number": "255712345678",
              "delivery_method": "sms",
              "otp_length": 6,
              "minutes_to_expire": 10,
              "sender_id": "BRIQ OTP",
              "message_template": "Your verification code is {code}. It expires in {expiry} minutes.",
          },
          timeout=15,
      )
      print(resp.status_code, resp.json())
      ```

      ```javascript Node.js theme={null}
      const BASE_URL = "https://karibu.briq.tz";
      const API_KEY  = "YOUR_API_KEY";
      const resp = await fetch(`${BASE_URL}/v1/otp/request`, {
        method: "POST",
        headers: {
          "X-API-Key": API_KEY,
          "Content-Type": "application/json",
        },
        body: JSON.stringify({
          phone_number: "255712345678",
          delivery_method: "sms",
          otp_length: 6,
          minutes_to_expire: 10,
          sender_id: "BRIQ OTP",
          message_template: "Your verification code is {code}. It expires in {expiry} minutes.",
        }),
      });
      console.log(resp.status, await resp.json());
      ```

      ```php PHP theme={null}
      <?php
      $baseUrl = "https://karibu.briq.tz";
      $apiKey  = "YOUR_API_KEY";
      $ch = curl_init("$baseUrl/v1/otp/request");
      curl_setopt_array($ch, [
          CURLOPT_RETURNTRANSFER => true,
          CURLOPT_POST           => true,
          CURLOPT_HTTPHEADER     => [
              "X-API-Key: $apiKey",
              "Content-Type: application/json",
          ],
          CURLOPT_POSTFIELDS     => json_encode([
              "phone_number"      => "255712345678",
              "delivery_method"   => "sms",
              "otp_length"        => 6,
              "minutes_to_expire" => 10,
              "sender_id"         => "BRIQ OTP",
              "message_template"  => "Your verification code is {code}. It expires in {expiry} minutes.",
          ]),
      ]);
      $response = curl_exec($ch);
      $status   = curl_getinfo($ch, CURLINFO_HTTP_CODE);
      curl_close($ch);
      echo $status . "\n" . $response;
      ```
    </CodeGroup>
  </Tab>

  <Tab title="Voice call">
    The platform calls the recipient and reads the code via TTS. There is **no** `sender_id` or `message_template` field — both are SMS-only and ignored on this channel.

    **Channel-specific fields:** none.

    **Exact payload:**

    ```json theme={null}
    {
      "phone_number": "255712345678",
      "delivery_method": "call",
      "otp_length": 6,
      "minutes_to_expire": 10
    }
    ```

    <CodeGroup>
      ```bash cURL theme={null}
      curl -X POST https://karibu.briq.tz/v1/otp/request \
        -H "X-API-Key: YOUR_API_KEY" \
        -H "Content-Type: application/json" \
        -d '{
          "phone_number": "255712345678",
          "delivery_method": "call",
          "otp_length": 6,
          "minutes_to_expire": 10
        }'
      ```

      ```python Python theme={null}
      import requests

      BASE_URL = "https://karibu.briq.tz"
      API_KEY  = "YOUR_API_KEY"
      resp = requests.post(
          f"{BASE_URL}/v1/otp/request",
          headers={"X-API-Key": API_KEY, "Content-Type": "application/json"},
          json={
              "phone_number": "255712345678",
              "delivery_method": "call",
              "otp_length": 6,
              "minutes_to_expire": 10,
          },
          timeout=15,
      )
      print(resp.status_code, resp.json())
      ```

      ```javascript Node.js theme={null}
      const BASE_URL = "https://karibu.briq.tz";
      const API_KEY  = "YOUR_API_KEY";
      const resp = await fetch(`${BASE_URL}/v1/otp/request`, {
        method: "POST",
        headers: { "X-API-Key": API_KEY, "Content-Type": "application/json" },
        body: JSON.stringify({
          phone_number: "255712345678",
          delivery_method: "call",
          otp_length: 6,
          minutes_to_expire: 10,
        }),
      });
      console.log(resp.status, await resp.json());
      ```

      ```php PHP theme={null}
      <?php
      $baseUrl = "https://karibu.briq.tz";
      $apiKey  = "YOUR_API_KEY";
      $ch = curl_init("$baseUrl/v1/otp/request");
      curl_setopt_array($ch, [
          CURLOPT_RETURNTRANSFER => true,
          CURLOPT_POST           => true,
          CURLOPT_HTTPHEADER     => [
              "X-API-Key: $apiKey",
              "Content-Type: application/json",
          ],
          CURLOPT_POSTFIELDS     => json_encode([
              "phone_number"      => "255712345678",
              "delivery_method"   => "call",
              "otp_length"        => 6,
              "minutes_to_expire" => 10,
          ]),
      ]);
      echo curl_exec($ch);
      curl_close($ch);
      ```
    </CodeGroup>
  </Tab>

  <Tab title="WhatsApp">
    Delivers the OTP through the platform-managed `briq_otp` WhatsApp Business template. There is **no** `sender_id` or `message_template` field — sender resolution is automatic and the wording is fixed by the approved Meta template.

    **Sender resolution order** (you don't pass any sender field; the dispatcher picks one):

    1. A non-default WhatsApp sender owned by your developer user with an APPROVED `briq_otp` template.
    2. The sender whose Infobip `phone_number_id` matches the platform-wide `WHATSAPP_PHONE_NUMBER_ID` env value.
    3. The legacy `is_default = TRUE` sender row.

    **Channel-specific fields:** none.

    **Exact payload:**

    ```json theme={null}
    {
      "phone_number": "255712345678",
      "delivery_method": "whatsapp",
      "otp_length": 6,
      "minutes_to_expire": 10
    }
    ```

    <CodeGroup>
      ```bash cURL theme={null}
      curl -X POST https://karibu.briq.tz/v1/otp/request \
        -H "X-API-Key: YOUR_API_KEY" \
        -H "Content-Type: application/json" \
        -d '{
          "phone_number": "255712345678",
          "delivery_method": "whatsapp",
          "otp_length": 6,
          "minutes_to_expire": 10
        }'
      ```

      ```python Python theme={null}
      import requests

      BASE_URL = "https://karibu.briq.tz"
      API_KEY  = "YOUR_API_KEY"
      resp = requests.post(
          f"{BASE_URL}/v1/otp/request",
          headers={"X-API-Key": API_KEY, "Content-Type": "application/json"},
          json={
              "phone_number": "255712345678",
              "delivery_method": "whatsapp",
              "otp_length": 6,
              "minutes_to_expire": 10,
          },
          timeout=15,
      )
      print(resp.status_code, resp.json())
      ```

      ```javascript Node.js theme={null}
      const BASE_URL = "https://karibu.briq.tz";
      const API_KEY  = "YOUR_API_KEY";
      const resp = await fetch(`${BASE_URL}/v1/otp/request`, {
        method: "POST",
        headers: { "X-API-Key": API_KEY, "Content-Type": "application/json" },
        body: JSON.stringify({
          phone_number: "255712345678",
          delivery_method: "whatsapp",
          otp_length: 6,
          minutes_to_expire: 10,
        }),
      });
      console.log(resp.status, await resp.json());
      ```

      ```php PHP theme={null}
      <?php
      $baseUrl = "https://karibu.briq.tz";
      $apiKey  = "YOUR_API_KEY";
      $ch = curl_init("$baseUrl/v1/otp/request");
      curl_setopt_array($ch, [
          CURLOPT_RETURNTRANSFER => true,
          CURLOPT_POST           => true,
          CURLOPT_HTTPHEADER     => [
              "X-API-Key: $apiKey",
              "Content-Type: application/json",
          ],
          CURLOPT_POSTFIELDS     => json_encode([
              "phone_number"      => "255712345678",
              "delivery_method"   => "whatsapp",
              "otp_length"        => 6,
              "minutes_to_expire" => 10,
          ]),
      ]);
      echo curl_exec($ch);
      curl_close($ch);
      ```
    </CodeGroup>

    <Warning>
      **WhatsApp-specific 400s.** If the dispatcher cannot resolve a sender + APPROVED `briq_otp` template (e.g. the platform default isn't configured, or a custom one is in `DRAFT`/`PAUSED`), the response is `success: false` with a message describing which check failed (`NO_DEFAULT_SENDER`, `TEMPLATE_NOT_APPROVED`). Fall back to SMS in your client when this happens.
    </Warning>
  </Tab>
</Tabs>

## Error responses

| HTTP | Cause                                                                                                                                 | Shape                                          |
| ---- | ------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------- |
| 400  | invalid phone, send failure, unsupported `delivery_method`, WhatsApp dispatcher errors (`NO_DEFAULT_SENDER`, `TEMPLATE_NOT_APPROVED`) | envelope with `success: false` and a `message` |
| 401  | missing/expired `X-API-Key`                                                                                                           | `{"detail": "Invalid or expired API key"}`     |
| 403  | wrong host, API key not bound to a developer app, `app_key`/`X-App-ID` mismatch with the bound app, workspace not dev-accessible      | `{"detail": "..."}`                            |
| 422  | malformed body (e.g. non-digit phone, missing fields)                                                                                 | FastAPI validation error                       |

For the full cross-endpoint error reference, see [Error scenarios](/Karibu-OTP/index#8-error-scenarios--quick-reference).

## Best practices

* **Validate the phone client-side** before calling the API: digits-only, country code present, plausible length.
* **Store `data.expires_at`** so your UI can drive a countdown timer; consider [polling `/v1/otp/status`](/guides/otp-lifecycle#status) instead of triggering re-sends speculatively.
* **Cascade channels for reliability.** A common pattern: try `"whatsapp"` first; on a dispatcher 400, fall back to `"sms"`; offer `"call"` as a manual third option.
* **Don't promise custom WhatsApp wording.** The `briq_otp` template is fixed by Meta — `sender_id` and `message_template` are silently ignored on `"whatsapp"` and `"call"`.
* **Rate-limit your own users.** The API does not enforce per-phone request limits — your application should.
* **Never log the plaintext code.** It only exists in transit to the recipient.

## What's next

* Once the code is sent, verify what the user submits — see [Validating OTP codes](/guides/otp-validating-codes).
* For "Resend code" buttons, lockouts on logout, and live countdowns, see [Managing OTP lifecycle](/guides/otp-lifecycle).
