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

# Verify a One-Time Password Code with POST /v3/verify

> Use POST /v3/verify to validate a user-entered OTP code. Understand how mismatch (HTTP 200), expiry, and attempt-limit errors work.

After you send an OTP with [`POST /v3/request`](/guides/send-otp) or [`POST /v3/send`](/guides/send-otp), the user enters the code in your interface. You then call **`POST /v3/verify`** to check whether the code is correct. Understanding exactly how OTP.ID reports wrong codes, expired codes, and exhausted attempts is critical — a wrong code does **not** produce an HTTP error, so you must inspect the response body.

## Endpoint

```
POST https://api.otp.id/v3/verify
Authorization: Bearer <api_key>
```

## Request body

| Field    | Required | Description                                                |
| -------- | -------- | ---------------------------------------------------------- |
| `otp_id` | ✅        | The transaction ID returned by `/v3/request` or `/v3/send` |
| `otp`    | ✅        | The code the user entered in your UI                       |

## How verification works

<Steps>
  <Step title="User enters the code">
    Your frontend captures the OTP the user types and sends it to your backend.
  </Step>

  <Step title="Your backend calls POST /v3/verify">
    Pass `otp_id` (from when you sent the OTP) and `otp` (what the user entered).
  </Step>

  <Step title="Check data.verified in the response">
    A correct code returns `"verified": true`. A wrong code returns HTTP **200** with `"verified": false` — this is **not** a 4xx error. You must check the field explicitly.
  </Step>

  <Step title="Handle terminal error conditions">
    If the OTP has expired, been used, or the attempt limit is reached, OTP.ID returns HTTP **422**. Catch these and guide the user accordingly.
  </Step>
</Steps>

## curl example

```bash theme={null}
curl -X POST https://api.otp.id/v3/verify \
  -H "Authorization: Bearer <api_key>" \
  -H "Content-Type: application/json" \
  -d '{
    "otp_id": "OTP20260807ABCD000001",
    "otp": "482913"
  }'
```

## Response reference

### ✅ Successful verification (HTTP 200)

The code matched. The transaction is now marked as used and cannot be verified again.

```json theme={null}
{
  "success": true,
  "data": {
    "otp_id": "OTP20260807ABCD000001",
    "verified": true,
    "reason": ""
  },
  "error": null
}
```

### ❌ Wrong code — mismatch (HTTP 200)

<Note>
  A wrong code returns **HTTP 200**, not a 4xx error. The `success` field is
  `true` because the API call itself succeeded — the OTP simply did not match.
  Always check `data.verified` in your application code, never rely on HTTP
  status alone.
</Note>

Each wrong attempt increments an internal counter. You can inspect the current attempt count at any time with `GET /v3/otp/{otp_id}`. After **5 wrong attempts**, the transaction is permanently locked and returns `TOO_MANY_ATTEMPTS`.

```json theme={null}
{
  "success": true,
  "data": {
    "otp_id": "OTP20260807ABCD000001",
    "verified": false,
    "reason": "mismatch"
  },
  "error": null
}
```

### 🚫 Terminal errors (HTTP 422)

These conditions permanently end the transaction. You must issue a new OTP if the user needs to try again.

| `error.code`        | Meaning                                    |
| ------------------- | ------------------------------------------ |
| `OTP_EXPIRED`       | The TTL has elapsed since the OTP was sent |
| `TOO_MANY_ATTEMPTS` | The user entered 5 wrong codes             |
| `ALREADY_USED`      | The OTP was already verified successfully  |

**OTP\_EXPIRED**

```json theme={null}
{
  "success": false,
  "data": null,
  "error": {
    "code": "OTP_EXPIRED",
    "message": "OTP sudah kedaluwarsa"
  }
}
```

**TOO\_MANY\_ATTEMPTS**

```json theme={null}
{
  "success": false,
  "data": null,
  "error": {
    "code": "TOO_MANY_ATTEMPTS",
    "message": "Terlalu banyak percobaan"
  }
}
```

**ALREADY\_USED**

```json theme={null}
{
  "success": false,
  "data": null,
  "error": {
    "code": "ALREADY_USED",
    "message": "OTP sudah digunakan"
  }
}
```

## Application logic example

The example below shows how to handle all verification outcomes correctly in a Node.js/Express backend.

```javascript theme={null}
async function handleVerify(req, res) {
  const { otp_id, otp } = req.body;

  let apiRes;
  try {
    apiRes = await fetch('https://api.otp.id/v3/verify', {
      method: 'POST',
      headers: {
        'Authorization': `Bearer ${process.env.OTPID_API_KEY}`,
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({ otp_id, otp }),
    });
  } catch (networkError) {
    // Network failure — safe to retry
    return res.status(503).json({ message: 'Verification service unavailable, please try again.' });
  }

  const body = await apiRes.json();

  // ── Terminal errors (HTTP 422) ──────────────────────────────────────────────
  if (apiRes.status === 422) {
    const code = body.error?.code;

    if (code === 'OTP_EXPIRED') {
      return res.status(400).json({ message: 'Your code has expired. Please request a new OTP.' });
    }
    if (code === 'TOO_MANY_ATTEMPTS') {
      return res.status(400).json({ message: 'Too many incorrect attempts. Please request a new OTP.' });
    }
    if (code === 'ALREADY_USED') {
      return res.status(400).json({ message: 'This OTP has already been used.' });
    }
    // Unknown 422 — surface it
    return res.status(400).json({ message: body.error?.message ?? 'Verification failed.' });
  }

  // ── HTTP 200 — check data.verified, NOT the status code ───────────────────
  if (body.data?.verified === true) {
    // ✅ Success — proceed with your post-login logic
    return res.status(200).json({ message: 'Verified successfully.' });
  }

  if (body.data?.verified === false && body.data?.reason === 'mismatch') {
    // ❌ Wrong code — let the user try again (up to 5 attempts total)
    return res.status(400).json({ message: 'Incorrect code. Please try again.' });
  }

  // Unexpected response shape
  return res.status(500).json({ message: 'Unexpected verification response.' });
}
```

## Webhooks

A successful verification also fires the **`otp.verified`** webhook asynchronously to your configured endpoint. The webhook is useful for event-driven flows — for example, granting access on the server side without polling. See the [Webhooks guide](/guides/webhooks) for payload details and signature verification.

<Note>
  The webhook fires **after** both successful `POST /v3/verify` calls and
  automatic WhatsApp Inbound matches. It does not fire for failed or mismatched
  attempts.
</Note>
