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

# Verify

> One-time passcodes over WhatsApp.

WhatsApp OTP delivery is cheaper and more reliable than SMS in many markets. LazyApp handles generation, hashing, delivery, verification and abuse controls.

## Managed mode

The default. LazyApp generates the code, sends it, and verifies it later. You never see or store it.

**1. Send**

```bash theme={null}
curl https://lazyapp.in/v1/otp \
  -H "Authorization: Bearer $LAZYAPP_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"to": "+919812345670", "template_name": "login_code", "client_ip": "203.0.113.24"}'
```

`client_ip` is your end user's address, not your server's. It is optional; see [Limits](#limits).

```json theme={null}
{
  "id": "9f8b1c2d-4e5f-4a6b-8c7d-0e1f2a3b4c5d",
  "to": "+919812345670",
  "status": "sent",
  "expires_at": "2026-08-19T11:52:07+00:00",
  "mode": "managed"
}
```

Keep the `id`.

**2. Verify**

```bash theme={null}
curl https://lazyapp.in/v1/otp/verify \
  -H "Authorization: Bearer $LAZYAPP_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"id": "9f8b1c2d-...", "code": "482913"}'
```

```json theme={null}
{ "id": "9f8b1c2d-...", "status": "verified", "verified_at": "2026-08-19T11:44:20+00:00" }
```

A wrong code returns `422` with `"code": "otp_invalid"`. A verified challenge cannot be verified twice.

You can verify by `to` instead of `id` if carrying the id through your flow is awkward.

## Delivery-only mode

When your existing auth system already generates codes, LazyApp just delivers:

```json theme={null}
{
  "to": "+919812345670",
  "mode": "delivery_only",
  "code": "482913"
}
```

You verify it yourself. LazyApp does not retain the code.

## What LazyApp enforces

> **Never stored in plaintext**
>
> Codes are hashed with a keyed mechanism at creation. There is nothing to leak.

> **Redacted everywhere**
>
> Codes are stripped from request logs and from webhook payloads, so an OTP cannot leak
> to your own logging pipeline.

> **Attempt limits**
>
> Wrong guesses are capped per challenge, and a verified challenge cannot be replayed.

> **Abuse controls**
>
> Per-recipient daily caps, per-team and per-IP rate limits, and a resend delay to stop
> someone using your account to spam a number.

## Limits

Refusals carry a `code`, so your sign-in page can show the right message without matching English text. When waiting is the answer, the response is `429` with a `Retry-After` header and `retry_after` in the body.

| `code` | Status | When |
| - | - | - |
| `otp_resend_too_soon` | 429 | A new code was asked for within 30 seconds of the last one to the same number |
| `otp_daily_limit` | 422 | The number has had 20 codes today |
| `otp_client_ip_limit` | 429 | One `client_ip` asked for codes for more than 10 different numbers in an hour |
| `otp_locked` | 429 or 422 | 429: 10 wrong codes in an hour locked the number. While it is locked even the right code is refused and no new code is sent. 422: this challenge used up its attempts; send a new code |
| `otp_invalid` | 422 | Wrong code |
| `otp_expired` | 422 | The code is older than 10 minutes |
| `otp_not_found` | 422 | No pending challenge matches the `id` or `to` |
| `otp_template_required` | 422 | No authentication template, and the number has not messaged you in the last 24 hours |

```json theme={null}
{ "message": "Wait 21 more second(s) before requesting another code.", "code": "otp_resend_too_soon", "retry_after": 21 }
```

The per-IP rule only applies when you pass `client_ip`. A website sends every sign-in from its own server, so applying the rule to the server's address would stop it after a handful of users. Pass your end user's address and the rule protects you from one person cycling through numbers.

Sending and verifying have separate per-key rate limits (see [Rate limits](https://docs.lazyapp.in/reference/rate-limits.md)).

## Test mode

An `sk_test_` key sends through your sandbox number, so test codes reach real phones on your test recipient list and verify exactly as they do live.

## Templates

OTPs need an approved `AUTHENTICATION` template. Pass `template_name` to choose one, or let LazyApp use the workspace default. Authentication templates support one-tap autofill and copy-code buttons, which materially improve completion rates.

Without a template, a code can only go as plain text while the number has messaged you in the last 24 hours, because WhatsApp refuses free-form messages outside that window. Anywhere else the request is refused with `otp_template_required` rather than sent to fail.

The template goes out in the `language` you pass. Without one, LazyApp uses the template's language when it has only one, so an `en` or `hi` template works without configuration.

## Webhooks

You receive `otp.requested`, `otp.verified` and `otp.failed`. Codes are redacted from all three.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.