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

# Managing OTP Lifecycle

> Resend, invalidate, and inspect the active OTP for a (phone, app) pair without rebuilding the request flow.

Once a code is in flight, three lifecycle endpoints let you keep state coherent without rebuilding the request flow:

* [`POST /v1/otp/resend`](#resend) — issue a new OTP after invalidating the previous one.
* [`POST /v1/otp/invalidate`](#invalidate) — force-expire any active OTP for a phone scoped to your app.
* [`GET /v1/otp/status`](#status) — inspect the active OTP without triggering a send.

All three are scoped to the Developer App bound to your `X-API-Key`. Karibu resolves the app from the key — omit `app_key` when the key is bound. An OTP issued under app A cannot be touched via app B.

***

## Resend

Issue a new OTP after invalidating the previous one. Body shape and channel-specific fields are identical to [`/v1/otp/request`](/guides/otp-requesting-codes); only the path changes.

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

### 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 `+`.                                                                                        |
| `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                | —       | Replaces callback target for the new OTP window.                                                            |
| `callback_secret`   | string                | no                | —       | Optional signing secret for flake callbacks.                                                                |

`sender_id` and `message_template` are **SMS-only** — they appear in the SMS tab below and are silently ignored on `"call"` and `"whatsapp"`.

### Success response

```json theme={null}
{
  "success": true,
  "message": "OTP resent successfully.",
  "data": { "expires_at": "2026-05-08T12:45:01.000000" },
  "status_code": 200
}
```

Errors mirror `/request` exactly. See [Error scenarios](/Karibu-OTP/index#8-error-scenarios--quick-reference).

<Note>
  **`request` vs `resend` — what's the difference?** Functionally both invalidate any prior active OTP and dispatch a new one. Use `/resend` when the **end user** explicitly clicks "Resend code"; this lets you treat it differently in your analytics, rate-limits, or UI without changing payloads.
</Note>

<Note>
  **Switching channels on resend is allowed** — e.g. user clicks "I didn't get the SMS, send via WhatsApp instead." Just send the new `delivery_method`. The previous active OTP is invalidated regardless of which channel issued it.
</Note>

### Channel-specific payload & code samples

<Tabs>
  <Tab title="SMS">
    Customisable 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. |

    <CodeGroup>
      ```bash cURL theme={null}
      curl -X POST https://karibu.briq.tz/v1/otp/resend \
        -H "X-API-Key: YOUR_API_KEY" \
        -H "Content-Type: application/json" \
        -d '{
          "phone_number": "255712345678",
          "delivery_method": "sms",
          "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/resend",
          headers={"X-API-Key": API_KEY, "Content-Type": "application/json"},
          json={
              "phone_number": "255712345678",
              "delivery_method": "sms",
              "sender_id": "BRIQ OTP",
              "message_template": "Your verification code is {code}. It expires in {expiry} minutes.",
          },
          timeout=15,
      )
      print(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/resend`, {
        method: "POST",
        headers: { "X-API-Key": API_KEY, "Content-Type": "application/json" },
        body: JSON.stringify({
          phone_number: "255712345678",
          delivery_method: "sms",
          sender_id: "BRIQ OTP",
          message_template: "Your verification code is {code}. It expires in {expiry} minutes.",
        }),
      });
      console.log(await resp.json());
      ```

      ```php PHP theme={null}
      <?php
      $baseUrl = "https://karibu.briq.tz";
      $apiKey  = "YOUR_API_KEY";
      $ch = curl_init("$baseUrl/v1/otp/resend");
      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",
              "sender_id"        => "BRIQ OTP",
              "message_template" => "Your verification code is {code}. It expires in {expiry} minutes.",
          ]),
      ]);
      echo curl_exec($ch);
      curl_close($ch);
      ```
    </CodeGroup>
  </Tab>

  <Tab title="Voice call">
    The platform calls the recipient and reads the new code via TTS. No `sender_id` or `message_template`.

    **Channel-specific fields:** none.

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

      ```python Python theme={null}
      resp = requests.post(
          f"{BASE_URL}/v1/otp/resend",
          headers={"X-API-Key": API_KEY, "Content-Type": "application/json"},
          json={
              "phone_number": "255712345678",
              "delivery_method": "call",
          },
          timeout=15,
      )
      print(resp.json())
      ```

      ```javascript Node.js theme={null}
      const resp = await fetch(`${BASE_URL}/v1/otp/resend`, {
        method: "POST",
        headers: { "X-API-Key": API_KEY, "Content-Type": "application/json" },
        body: JSON.stringify({
          phone_number: "255712345678",
          delivery_method: "call",
        }),
      });
      console.log(await resp.json());
      ```

      ```php PHP theme={null}
      $ch = curl_init("$baseUrl/v1/otp/resend");
      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",
          ]),
      ]);
      echo curl_exec($ch);
      curl_close($ch);
      ```
    </CodeGroup>
  </Tab>

  <Tab title="WhatsApp">
    Routes through the platform-managed `briq_otp` template. Sender resolution is automatic — see [Requesting OTP codes → WhatsApp](/guides/otp-requesting-codes#channel-specific-payload--code-samples) for the full resolution order.

    **Channel-specific fields:** none.

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

      ```python Python theme={null}
      resp = requests.post(
          f"{BASE_URL}/v1/otp/resend",
          headers={"X-API-Key": API_KEY, "Content-Type": "application/json"},
          json={
              "phone_number": "255712345678",
              "delivery_method": "whatsapp",
          },
          timeout=15,
      )
      print(resp.json())
      ```

      ```javascript Node.js theme={null}
      const resp = await fetch(`${BASE_URL}/v1/otp/resend`, {
        method: "POST",
        headers: { "X-API-Key": API_KEY, "Content-Type": "application/json" },
        body: JSON.stringify({
          phone_number: "255712345678",
          delivery_method: "whatsapp",
        }),
      });
      console.log(await resp.json());
      ```

      ```php PHP theme={null}
      $ch = curl_init("$baseUrl/v1/otp/resend");
      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",
          ]),
      ]);
      echo curl_exec($ch);
      curl_close($ch);
      ```
    </CodeGroup>

    <Warning>
      **WhatsApp-specific 400s.** If the dispatcher cannot resolve a sender + APPROVED `briq_otp` template, the response is `success: false` with `NO_DEFAULT_SENDER` or `TEMPLATE_NOT_APPROVED` in the message. Fall back to SMS in your client.
    </Warning>
  </Tab>
</Tabs>

***

## Invalidate

Force-expire any active OTP for a phone scoped to the Developer App bound to your `X-API-Key`. Useful on logout, security events (suspicious activity, password change), or when the user changes their phone number.

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

### Request body

| Field          | Type                  | Required | Notes                                                                                                       |
| -------------- | --------------------- | -------- | ----------------------------------------------------------------------------------------------------------- |
| `phone_number` | string (E.164 digits) | yes      | Digits only, no `+`.                                                                                        |
| `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. |

### Success response

```json theme={null}
{
  "success": true,
  "message": "OTP invalidated successfully.",
  "data": null,
  "status_code": 200
}
```

<Note>
  **Idempotent.** Calling this when there is no active OTP also returns 200 — it simply has nothing to invalidate. Safe to fire from a logout handler without checking state first.
</Note>

<Note>
  **Callbacks cleared.** `/invalidate` also removes the callback association for that `(phone, app)` pair. Issue a new `/request` or `/resend` with `callback_url` if you need callbacks on the next OTP.
</Note>

### Code samples

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

  ```python Python theme={null}
  resp = requests.post(
      f"{BASE_URL}/v1/otp/invalidate",
      headers={"X-API-Key": API_KEY, "Content-Type": "application/json"},
      json={"phone_number": "255712345678"},
      timeout=10,
  )
  print(resp.json())
  ```

  ```javascript Node.js theme={null}
  const resp = await fetch(`${BASE_URL}/v1/otp/invalidate`, {
    method: "POST",
    headers: { "X-API-Key": API_KEY, "Content-Type": "application/json" },
    body: JSON.stringify({ phone_number: "255712345678" }),
  });
  console.log(await resp.json());
  ```

  ```php PHP theme={null}
  $ch = curl_init("$baseUrl/v1/otp/invalidate");
  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",
      ]),
  ]);
  echo curl_exec($ch);
  curl_close($ch);
  ```
</CodeGroup>

***

## Status

Inspect the currently active OTP for a phone, scoped to the Developer App bound to your `X-API-Key`, **without triggering a send**. Useful for UI countdowns, diagnostics, and idempotent UX (e.g. "an OTP was already sent — please check your phone").

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

### Query parameters

| Param          | Required | Notes                                                                                                       |
| -------------- | -------- | ----------------------------------------------------------------------------------------------------------- |
| `phone_number` | yes      | Digits only, no `+`.                                                                                        |
| `app_key`      | no       | Omit when API key is bound to a developer app. Legacy unbound keys may send this or use `X-App-ID` instead. |

### Success response

```json theme={null}
{
  "success": true,
  "message": "OTP status retrieved.",
  "data": {
    "is_valid": true,
    "expires_at": "2026-05-08T12:45:01.000000",
    "remaining_attempts": 3
  },
  "status_code": 200
}
```

### No active OTP

```json theme={null}
{
  "success": false,
  "message": "No active OTP found.",
  "data": null,
  "status_code": 404
}
```

<Warning>
  The HTTP response body wraps `status_code: 404`, but the underlying HTTP response itself is **200 OK**. Inspect `body.success` and `body.status_code`, not the raw HTTP status, when handling this endpoint.
</Warning>

### Code samples

<CodeGroup>
  ```bash cURL theme={null}
  curl -G https://karibu.briq.tz/v1/otp/status \
    -H "X-API-Key: YOUR_API_KEY" \
    --data-urlencode "phone_number=255712345678" \
  ```

  ```python Python theme={null}
  resp = requests.get(
      f"{BASE_URL}/v1/otp/status",
      headers={"X-API-Key": API_KEY},
      params={"phone_number": "255712345678"},
      timeout=10,
  )
  print(resp.json())
  ```

  ```javascript Node.js theme={null}
  const params = new URLSearchParams({
    phone_number: "255712345678",
  });
  const resp = await fetch(`${BASE_URL}/v1/otp/status?${params}`, {
    headers: { "X-API-Key": API_KEY },
  });
  console.log(await resp.json());
  ```

  ```php PHP theme={null}
  $query = http_build_query([
      "phone_number" => "255712345678",
  ]);
  $ch = curl_init("$baseUrl/v1/otp/status?$query");
  curl_setopt_array($ch, [
      CURLOPT_RETURNTRANSFER => true,
      CURLOPT_HTTPHEADER     => ["X-API-Key: $apiKey"],
  ]);
  echo curl_exec($ch);
  curl_close($ch);
  ```
</CodeGroup>

***

## Best practices

* **Use `/status` to drive countdown UI**, not speculative resends. Polling is cheap; firing duplicate `/request` calls invalidates the in-flight code and confuses the user.
* **Always invalidate on logout.** It's idempotent — no need to check state first.
* **Treat `is_valid: true, remaining_attempts: 0` as a locked OTP.** The user has burned all 3 attempts and must resend.

For full payload tables, error matrices, and the canonical reference, see the [Karibu OTP API reference](/Karibu-OTP/index).
