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

# Verify

> Verify phone numbers with one-time codes. OTPBay generates the code, delivers it over Telegram or SMS, and checks it.

The Verify API is the simplest way to confirm that a user owns a phone number. You make two calls:

1. [Start a verification](/api-reference/verify/start) with the phone number. OTPBay generates a code and sends it.
2. [Check the code](/api-reference/verify/check) your user enters.

OTPBay handles code generation, expiry, the wrong-code limit, the fallback from Telegram to SMS, and refunds for codes that never arrive.

## How a verification works

```mermaid theme={null}
flowchart TD
  A[Start verification] --> C[Code sent on Telegram]
  A -->|Channel is SMS| D[Code sent by SMS]
  C -->|Can't deliver, or<br/>fallback wait passes| D
  C --> E{User enters code}
  D --> E
  E -->|Correct| F([approved])
  E -->|Too many wrong codes| G([failed])
  E -->|Expiry passes| H([expired])
```

A verification is `pending` until it reaches one of three final statuses:

| Status     | When                                                                                           |
| ---------- | ---------------------------------------------------------------------------------------------- |
| `approved` | The user entered the correct code.                                                             |
| `expired`  | The code wasn't entered before `expires_at`.                                                   |
| `failed`   | The code couldn't be sent, or the user used up all their wrong codes. `last_error` says which. |

Final statuses don't change. To try again, start a new verification.

## Start a verification

```bash theme={null}
curl -X POST https://api.otpbay.com/v1/verifications \
  -H "Authorization: Bearer $OTPBAY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "to": "+14155552671",
    "metadata": { "user_id": "usr_1042" },
    "callback": "https://example.com/webhooks/otpbay"
  }'
```

| Field      | Required | Description                                                                                    |
| ---------- | -------- | ---------------------------------------------------------------------------------------------- |
| `to`       | Yes      | Phone number in international format.                                                          |
| `metadata` | No       | Your own data, returned in responses and webhooks. See [metadata](/concepts/formats#metadata). |
| `callback` | No       | URL for a [webhook](/webhooks) when the verification finishes.                                 |

The response is `201` with the verification. Store its `sid` with your user's session.

## Check the code

```bash theme={null}
curl -X POST https://api.otpbay.com/v1/verifications/VE6650c3a1b2c3d4e5f6a7b8c9/check \
  -H "Authorization: Bearer $OTPBAY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "code": "482913" }'
```

The response is always `200` while the verification is `pending`. Read `valid`:

* `"valid": true` — the code matched and `status` is now `approved`.
* `"valid": false` — the code is wrong. `checks_left` went down by one. When it reaches `0`, the verification is `failed`.

Checking a verification that is already `approved`, `expired` or `failed` returns `409 VERIFICATION_NOT_PENDING`.

<Tip>
  Show `checks_left` to your user after a wrong code, so they know how many tries remain.
</Tip>

## Telegram with SMS fallback

By default, Verify sends the code on Telegram, which costs less than SMS in most countries. If Telegram can't deliver it, OTPBay sends **the same code** by SMS. Your user can enter the code from either message.

OTPBay switches to SMS in two cases, both set in Verify settings:

* **Telegram can't deliver.** The number has no Telegram account, or Telegram rejects or revokes the message. `fallback_reason` is `telegram_failed`.
* **The fallback wait passes.** Telegram hasn't delivered the code after this many seconds (default 60). `fallback_reason` is `timeout`.

The fallback happens at most once and only before the code expires. When it happens, `channel` changes to `sms` and a second entry appears in `attempts`. The Telegram send is refunded — you pay only for the channel that delivered.

## Configure Verify

Open **Messaging → Verify** in the dashboard. A verification uses the settings in place when it starts; later changes don't affect verifications already running.

| Setting                            | Default                            | Range                                       |
| ---------------------------------- | ---------------------------------- | ------------------------------------------- |
| Channel                            | Telegram                           | Telegram or SMS                             |
| SMS fallback                       | On                                 | Only with the Telegram channel              |
| Switch when Telegram can't deliver | On                                 | —                                           |
| Fallback wait                      | 60 seconds                         | 10–600 seconds, less than the expiry        |
| Code length                        | 6 digits                           | 4–8 digits                                  |
| Code expires after                 | 10 minutes                         | 1–60 minutes                                |
| Wrong codes allowed                | 5                                  | 1–10                                        |
| Sender ID                          | The SMS default sender ID          | Any approved sender ID                      |
| SMS text                           | `Your verification code is {code}` | Up to 320 characters, must include `{code}` |
| Callback URL                       | None                               | Public `http` or `https` URL                |

<Note>
  If the channel is SMS, or SMS fallback is on, you need an approved [sender ID](/guides/sms#sender-ids). Without one, starting a verification fails with `422 SENDER_ID_REQUIRED`.
</Note>

Verify sends its SMS even when the SMS channel is turned off for direct sends, and it doesn't use the SMS channel's default callback.

## Pricing and refunds

Each verification costs a **\$0.02 Verify fee** plus the price of each channel send. When you start a verification, your balance must cover the fee and the first send, or the request fails with `402 INSUFFICIENT_BALANCE`.

When the verification finishes, OTPBay refunds what wasn't delivered:

* A Telegram send that wasn't delivered in time is refunded.
* The Verify fee is refunded if no channel delivered the code.
* An SMS the carrier accepted is not refunded.

`cost` on the verification always shows the current total after refunds. See [Billing](/billing) for details.

## Follow a verification

You don't need to poll: check the code when your user submits it. To react to verifications that expire or fail, set a `callback`. OTPBay sends one [webhook](/webhooks) when the verification reaches `approved`, `expired` or `failed`, after refunds are settled.

You can also [get a verification](/api-reference/verify/get) at any time to see its attempts and status.

## Best practices

* **Start verifications from your backend.** Rate-limit them per user and per phone number to stop people draining your balance.
* **Keep the `sid` server-side.** Tie it to the user's session rather than trusting a `sid` sent by the client.
* **Don't resend by starting new verifications in a loop.** Wait for the fallback or for the code to expire first.
