---
title: Verify (OTP)
description: "POST /v1/verify sends a verification code, POST /v1/verify/:id/check validates the candidate code."
---

import Endpoint from '../../../components/Endpoint.astro';

Verify is the OTP channel. Two endpoints: one to send a
verification code to a recipient, one to check the code they
enter back. The code itself is hashed at rest and never returned
in any response. You only learn whether the candidate matched.

## Channels

V1 supports `sms` only. Email and WhatsApp verify share the same
table and lifecycle and will land on this endpoint in a later
release.

## Start a verification

<Endpoint method="POST" path="/v1/verify" />

```bash
curl https://api.zevsend.com/v1/verify \
  -H "Authorization: Bearer sk_live_..." \
  -H "Content-Type: application/json" \
  -d '{
    "to": "+2348012345678",
    "channel": "sms"
  }'
```

### Body parameters

| Field     | Type   | Required | Description                                                          |
| --------- | ------ | -------- | -------------------------------------------------------------------- |
| `to`      | string | yes      | Recipient phone in E.164 format (`+2348012345678`).                  |
| `channel` | string | no       | `sms`. Default: `sms`.                                               |

### Response

```http
HTTP/1.1 202 Accepted
Content-Type: application/json

{
  "id": "vrf_01HXYZ...",
  "channel": "sms",
  "to": "+2348012345678",
  "status": "pending",
  "sandbox": false,
  "expires_at": "2025-05-24T12:44:56.789Z",
  "error_message": null
}
```

Save the `id`. You'll pass it back to the check endpoint along
with the code the user entered.

The code expires in **10 minutes** and is single-use. After the
expiry the verification flips to `expired` regardless of whether
the user ever tried.

If the carrier refused the message at submit (invalid number,
network outage, etc.), the response returns `status: "failed"`
and the `error_message` field carries the carrier text. You will
not be charged for a failed dispatch.

## Check a verification

<Endpoint method="POST" path="/v1/verify/:id/check" />

```bash
curl https://api.zevsend.com/v1/verify/vrf_01HXYZ.../check \
  -H "Authorization: Bearer sk_live_..." \
  -H "Content-Type: application/json" \
  -d '{ "code": "847291" }'
```

### Body parameters

| Field  | Type   | Required | Description                              |
| ------ | ------ | -------- | ---------------------------------------- |
| `code` | string | yes      | Candidate code the user entered (4–10 chars). |

### Response

```http
HTTP/1.1 200 OK
Content-Type: application/json

{
  "id": "vrf_01HXYZ...",
  "verified": true,
  "status": "verified",
  "remaining_attempts": 4
}
```

| Field                | Type    | Description                                                                |
| -------------------- | ------- | -------------------------------------------------------------------------- |
| `verified`           | boolean | `true` on the call that accepted the right code.                           |
| `status`             | string  | Lifecycle state, see below.                                                |
| `remaining_attempts` | number  | How many wrong-code attempts the verification has left before exhaustion. |

## Lifecycle

| State       | Meaning                                                                 |
| ----------- | ----------------------------------------------------------------------- |
| `pending`   | Code sent, awaiting a check.                                            |
| `verified`  | Correct code presented. Terminal.                                       |
| `expired`   | 10-minute window lapsed without a successful check. Terminal.           |
| `exhausted` | 5 wrong codes presented. Terminal.                                      |

Wrong codes increment the `attempts` counter without flipping
status until the limit. The fifth wrong code flips the
verification to `exhausted`; further checks return the terminal
state without consuming another attempt.

## Pricing

Verify is priced per `start` call. The rate is a flat amount per
currency, set by the platform. Open the
[Pricing and billing](/guide/pricing#verify-otp) page for the
current numbers in your team's currency.

The matching `check` call is free. Wrong-code submissions don't
incur extra charges either. You pay once when the code goes
out, regardless of how the user fares at typing it.

Sandbox verifies are always free. Switch your team to live to
start draining credit on Verify sends.

## Errors

| Status | Code                       | Cause                                                            |
| ------ | -------------------------- | ---------------------------------------------------------------- |
| `400`  | `invalid_phone`            | `to` isn't a valid E.164 number.                                 |
| `400`  | `insufficient_credit`      | Zev Credit balance can't cover the verify rate. Top up + retry.  |
| `403`  | `feature_not_on_plan`      | Your plan doesn't include Verify. Upgrade to a paid plan.        |
| `404`  | `verification_not_found`   | Wrong `id` on the check endpoint, or the verification was on a different team. |
| `403`  | `team_suspended`           | The team is suspended. Contact support.                          |