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

# OTP Transaction Lifecycle, Statuses, and Expiry Rules

> Understand how an OTP transaction moves through creation, delivery, verification, expiry, and failure states — including TTL and attempt-limit rules.

Every OTP you create in OTP.ID is a transaction with its own unique identifier, status, and expiry window. Understanding the lifecycle of that transaction — from the moment you call the API to the moment the OTP is verified or expires — helps you build reliable, user-friendly authentication flows. This page walks through each stage in order, explains the status values you will encounter, and documents the clamping rules that govern TTL and OTP length.

***

## OTP Transaction ID Format

Every OTP transaction is assigned a unique ID at creation time. The format is:

```
OTP + YYYYMMDD + 4 random uppercase letters + 6-digit daily counter
```

**Example:** `OTP20260807ABCD000001`

| Segment       | Example    | Description                              |
| ------------- | ---------- | ---------------------------------------- |
| Prefix        | `OTP`      | Fixed string                             |
| Date          | `20260807` | Creation date (UTC) in `YYYYMMDD` format |
| Random suffix | `ABCD`     | 4 randomly generated uppercase letters   |
| Daily counter | `000001`   | Sequential counter, resets each UTC day  |

Store the `otp_id` returned in the API response — you need it to poll the transaction status and to verify the OTP.

***

## Transaction Statuses

An OTP transaction passes through the following statuses during its lifetime.

| Status              | Meaning                                                                                                 | Terminal?                            |
| ------------------- | ------------------------------------------------------------------------------------------------------- | ------------------------------------ |
| `pending`           | Created; delivery has not yet been confirmed. For `whatsapp_inbound`, the user has not yet replied.     | No                                   |
| `sent` / `success`  | The OTP was successfully delivered to the channel vendor. Balance is deducted at this point.            | No (can still be verified or expire) |
| `failed`            | The vendor could not deliver the OTP. Balance is **not** deducted. The HTTP response is still `200 OK`. | Yes                                  |
| `verified`          | The correct OTP was submitted and accepted.                                                             | Yes                                  |
| `expired`           | The TTL elapsed before the OTP was verified.                                                            | Yes                                  |
| `too_many_attempts` | Five consecutive wrong verification attempts were made.                                                 | Yes                                  |
| `already_used`      | A valid OTP that was already verified was submitted again.                                              | Yes                                  |

<Note>
  A `failed` delivery still creates a transaction record and still returns HTTP `200 OK`. Check the `status` field in the response body to distinguish a failed delivery from a network or authentication error.
</Note>

***

## Lifecycle Flow

The diagram below describes the full journey of an OTP transaction in plain text.

```
[Your Server]
     │
     │  POST /v3/request  or  POST /v3/send
     ▼
[OTP.ID API]  ──creates──►  Transaction  (status: pending)
     │
     │  Calls channel vendor
     ▼
[Channel Vendor]
     │
     ├── Delivery succeeds ──►  status: sent / success
     │                              │
     │                              │  User receives OTP
     │                              │
     │                              ▼
     │                         POST /v3/verify
     │                              │
     │                              ├── Code correct  ──►  status: verified  ✅
     │                              │
     │                              ├── Code wrong (attempt < 5)  ──►  status: sent (unchanged)
     │                              │                                   verified: false, reason: "mismatch"
     │                              │
     │                              ├── 5th wrong attempt  ──►  status: too_many_attempts  ❌
     │                              │
     │                              └── TTL exceeded  ──►  status: expired  ❌
     │
     └── Delivery fails  ──►  status: failed  ❌  (balance not deducted)
```

<Steps>
  <Step title="Create the transaction">
    Call `POST /v3/request` (with automatic OTP generation) or `POST /v3/send` (supplying your own OTP value). The API immediately returns a transaction object with `status: pending` and an `otp_id`.

    For `whatsapp_inbound`, the response also includes a `verification` block containing `wa_number`, `message`, `wa_link`, and `expires_at`. Display this information to your user right away.
  </Step>

  <Step title="OTP.ID attempts delivery">
    OTP.ID contacts the channel vendor (WhatsApp provider, SMS gateway, email relay, or telephony provider). If the vendor accepts the message, the status advances to `sent` or `success` and your balance is deducted. If the vendor rejects or cannot reach the destination, the status is set to `failed` and no charge is applied.
  </Step>

  <Step title="User receives the code">
    Your user reads the OTP from their WhatsApp message, SMS, email, missed-call log, or (for `whatsapp_inbound`) sends the pre-filled WhatsApp message. The OTP is valid until the TTL expires.
  </Step>

  <Step title="Your app submits the code for verification">
    When the user enters the code in your interface, call `POST /v3/verify` with the `otp_id` and the `otp` value the user provided.

    ```json theme={null}
    {
      "otp_id": "OTP20260807ABCD000001",
      "otp": "482910"
    }
    ```
  </Step>

  <Step title="OTP.ID returns the verification result">
    OTP.ID checks the submitted code and returns one of the following outcomes:

    * **Correct code** — HTTP `200`, `verified: true`. Status advances to `verified`.
    * **Wrong code (attempts remaining)** — HTTP `200`, `verified: false`, `reason: "mismatch"`. The transaction remains active; the user can try again.
    * **5th wrong attempt** — HTTP `422`, error code `TOO_MANY_ATTEMPTS`. Transaction is permanently locked.
    * **Expired** — HTTP `422`, error code `OTP_EXPIRED`.
    * **Already used** — HTTP `422`, error code `ALREADY_USED`.
  </Step>
</Steps>

***

## Polling Transaction Status

If you need to check the state of a transaction outside of the verification flow (for example, to confirm delivery or to build a status dashboard), use the status endpoint:

```http theme={null}
GET /v3/otp/{otp_id}
Authorization: Bearer <api_key>
```

**Example response:**

```json theme={null}
{
  "success": true,
  "data": {
    "otp_id": "OTP20260807ABCD000001",
    "status": "sent",
    "channel": "whatsapp",
    "number": "6281234567890",
    "expires_at": "2026-08-07 10:05:00"
  },
  "error": null
}
```

Poll this endpoint periodically for `whatsapp_inbound` transactions to detect when the user has sent their reply and the status advances to `verified`.

***

## TTL Clamping

The `ttl` field controls how many seconds the OTP remains valid after creation. OTP.ID applies the following clamping rules silently — values outside the valid range are adjusted rather than rejected.

| Input value  | Effective TTL               |
| ------------ | --------------------------- |
| Below 60     | Clamped to **60 seconds**   |
| 60 – 900     | Used as-is                  |
| Above 900    | Clamped to **900 seconds**  |
| Not provided | Defaults to **300 seconds** |

<Note>
  OTP.ID never returns a validation error for an out-of-range `ttl`. The value is silently clamped. Always read the `expires_at` field in the response to know the exact expiry time.
</Note>

***

## OTP Length Clamping

The `otp_length` field controls how many digits the generated OTP contains. The same silent clamping logic applies.

| Input value  | Effective length         |
| ------------ | ------------------------ |
| Below 4      | Clamped to **4 digits**  |
| 4 – 8        | Used as-is               |
| Above 8      | Clamped to **8 digits**  |
| Not provided | Defaults to **6 digits** |

***

## Verification Behaviour Reference

| Scenario                  | HTTP status | `verified` | `reason` / error code |
| ------------------------- | ----------- | ---------- | --------------------- |
| Correct code              | `200`       | `true`     | —                     |
| Wrong code (attempts 1–4) | `200`       | `false`    | `"mismatch"`          |
| 5th wrong attempt         | `422`       | —          | `TOO_MANY_ATTEMPTS`   |
| OTP expired               | `422`       | —          | `OTP_EXPIRED`         |
| OTP already verified      | `422`       | —          | `ALREADY_USED`        |

<Warning>
  A wrong-code response is **not** a server error — it is a successful API call that returns `verified: false`. Do not treat HTTP `200` with `verified: false` as an exception or retry it automatically. Show the user a "wrong code" message and let them try again (up to 5 attempts total).
</Warning>

<Tip>
  Build your verification UI to count remaining attempts on the client side. After 4 failed attempts, warn the user that one attempt remains before the transaction is permanently locked. You can also request a new OTP at any time if the user wants to start fresh.
</Tip>
