# AkidOTP — Developer Integration Guide

Everything you need to integrate AkidOTP verification into your app. All endpoints,
JSON shapes, and limit values reflect the current production API.

**Base URL:** `https://akidotp.com` — all paths below are relative to it.
**Content type:** send `Content-Type: application/json` on every `POST`.

---

## 1. Introduction

AkidOTP is a multi-channel verification API. You verify your own end-users' phone
numbers and email addresses through four channels:

- **WhatsApp, Telegram, SMS — the REVERSE model.** AkidOTP does **not** send a code to
  the user. You display a short code and the user sends that code **to AkidOTP** from
  their own device (by tapping a pre-filled WhatsApp / Telegram / SMS deep link).
  Receiving the message from their number is what proves they own it.
- **Email — the FORWARD model.** AkidOTP emails a 6-digit code to the user and the user
  types it back to you, which you confirm through the API.

---

## 2. Authentication

### Getting an API key

Create a developer account and generate a key from the AkidOTP dashboard, or use the
auth API directly:

| Action | Method + path | Auth | Body | Success |
|---|---|---|---|---|
| Sign up | `POST /api/auth/signup` | — | `{ "name", "email", "password", "verificationToken" }` | `201 { "success": true, "token": "<JWT>", "developer": { … } }` |
| Log in (step 1) | `POST /api/auth/login` | — | `{ "email", "password" }` | `200 { "success": true, "otpSent": true }` |
| Log in (step 2) | `POST /api/auth/login` | — | `{ "email", "code" }` | `200 { "success": true, "token": "<JWT>", "developer": { … } }` |
| **Create key** | `POST /api/auth/keys` | `Authorization: Bearer <JWT>` | optional `{ "name" }` | `201 { "success": true, "key": "<64-char hex>", "apiKey": { … } }` |
| List keys | `GET /api/auth/keys` | `Authorization: Bearer <JWT>` | — | `200 { "success": true, "keys": [ … ] }` |

Notes:
- Login is two-step: submit credentials first (this emails a one-time code), then submit
  that code to receive your dashboard JWT.
- Your usable API key is the top-level **`key`** value returned by `POST /api/auth/keys`.

### The header to send on every verification request

```
X-API-Key: <your key>
```

No other identifier is required. Your account and plan are resolved server-side from the
key — you do **not** send a developer id or app id.

---

## 3. Phone channels — WhatsApp / Telegram / SMS (`/api/mobileverify`)

### START

```
POST /api/mobileverify/start
X-API-Key: <your key>
Content-Type: application/json

{ "phone": "+218912345678" }
```

**Request body**

| Field | Required | Notes |
|---|---|---|
| `phone` | **yes** | E.164 format (leading `+`). |

**Which field picks the channel?** There is **no request field that limits the response
to a single channel.** `phone` is the only field the endpoint uses. The response returns
deep links for **every channel your plan allows**, and you choose which one to present to
your user. (You may optionally include `"channel": "whatsapp"` or `"channel": "telegram"`
to steer the plan permission check; it does not change which links you receive.)

**Success `200`** — fields present depend on your plan:

```jsonc
{
  "success": true,
  "code": "483920",                 // 6-digit code to show your user
  "socketRoom": "9f3c…d21a",        // session handle (32 hex chars); use it to poll
  "expiresIn": 120,                 // seconds (no absolute expiry timestamp is returned)

  // Included if your plan allows WhatsApp:
  "whatsappLink": "https://wa.me/<number>?text=483920",
  "whatsappPersonalLink": "intent://…",   // Android: WhatsApp personal
  "whatsappBusinessLink": "intent://…",   // Android: WhatsApp Business

  // Included if your plan allows Telegram:
  "telegramLink": "https://t.me/<bot>?start=483920",

  // Included if your plan allows SMS and SMS quota remains:
  "smsNumber": "<inbound number>",
  "smsBody": "483920",
  "smsLink": "sms:<number>?body=483920",     // Android separator
  "smsLinkIOS": "sms:<number>&body=483920",  // iOS separator
  "smsRemaining": 99                          // a number, or "unlimited"
}
```

Use the links exactly as returned. There is no separate session id — `socketRoom` is your
handle. The session is short-lived — see `expiresIn` (seconds) in the start response.

**Validation error:** missing `phone` → `400 { "success": false, "error": "Phone number required" }`.

### What the end-user does

- **WhatsApp:** taps `whatsappLink` (or a personal/business link) → WhatsApp opens with the
  code pre-filled → the user **sends** it to the AkidOTP number.
- **Telegram:** taps `telegramLink` → the AkidOTP bot opens with the code → the user taps
  **Start/Send** and shares their contact when prompted.
- **SMS:** taps `smsLink` / `smsLinkIOS` (or manually texts `smsBody` to `smsNumber`) → the
  user **sends** the SMS to the AkidOTP inbound number.

### Result — (a) Polling

```
GET /api/mobileverify/status?room=<socketRoom>
X-API-Key: <your key>
```

| State | Response |
|---|---|
| Verified | `{ "verified": true, "phone": "+218912345678" }` |
| Pending (still waiting) | `{ "verified": false, "expired": false, "mismatch": false }` |
| Wrong number replied | `{ "verified": false, "mismatch": true }` |
| Expired / no session | `{ "verified": false, "expired": true }` |

The **verified** response includes the confirmed `phone`. Poll roughly every 3 seconds
until `verified` or `expired` is true.

**The verified result is single-use.** `/status` serves `verified: true` only until the first read plus a short grace (~30 s), then withholds it — a stale poll cannot be replayed. Treat polling as **advisory**; the signed **webhook** (below) is the authoritative, one-shot result — rely on it for authentication decisions.

### Result — (b) Webhook

If you set a **webhook URL** on your API key (in the dashboard), AkidOTP `POST`s a signed
notification to it when a reverse verification succeeds.

- **Transport:** `POST`, **HTTPS only**, 10-second timeout, one automatic retry after
  5 seconds. Webhook delivery never affects the verification result itself.
- **Headers you receive:**
  ```
  Content-Type: application/json
  X-AkidOTP-Signature: <hex>
  User-Agent: AkidOTP-Webhook/1.0
  ```
- **Signature:** `X-AkidOTP-Signature` is the **HMAC-SHA256** of the **exact raw JSON
  request body**, keyed with your API key's **webhook secret**, hex-encoded. **To verify:**
  recompute HMAC-SHA256 over the raw request body using your webhook secret and compare it
  (constant-time) against the header value.

**Payloads.** WhatsApp and Telegram share one shape; **SMS uses a different shape** — handle
both.

WhatsApp / Telegram:
```json
{
  "phone": "+218912345678",
  "requestId": "9f3c…d21a",
  "channel": "whatsapp",
  "status": "verified",
  "verifiedAt": "2026-07-05T12:34:56.000Z"
}
```
(Telegram is identical with `"channel": "telegram"`. `requestId` equals the `socketRoom`
from START.)

SMS (verified):
```json
{
  "event": "verification.verified",
  "channel": "sms",
  "phone": "+218912345678",
  "timestamp": "2026-07-05T12:34:56.000Z"
}
```

SMS also sends a mismatch event when a different number replies:
```json
{
  "event": "verification.mismatch",
  "channel": "sms",
  "phone": "+218912345678",
  "senderPhone": "+218900000000",
  "timestamp": "2026-07-05T12:34:56.000Z"
}
```

Treat success as `status === "verified"` **or** `event === "verification.verified"`.

---

## 4. Email channel (`/api/verify/email`)

Forward OTP: AkidOTP emails a 6-digit code and the user types it back. **Synchronous — no
webhook.**

### START — send the code

```
POST /api/verify/email/send
X-API-Key: <your key>
Content-Type: application/json

{ "phone": "+218912345678", "email": "user@example.com" }
```

Both `phone` and `email` are **required**.

**Success `200`:**
```json
{ "sent": true, "message": "Verification code sent to user@example.com" }
```

Missing a field → `400 { "error": "Phone and email are required" }`.

### VERIFY — check the code

```
POST /api/verify/email/confirm
X-API-Key: <your key>
Content-Type: application/json

{ "email": "user@example.com", "code": "483920" }
```

The body is `{ email, code }` — there is **no session id**; the code is matched by email.

**Success `200` — returns a TOKEN (JWT).** Definitively: the success response includes a
signed JWT in `token`, plus the confirmed `phone` and `email`:
```json
{
  "verified": true,
  "phone": "+218912345678",
  "email": "user@example.com",
  "token": "<JWT>"
}
```

**Token scope — email only.** The returned JWT proves **email** ownership, not phone ownership: it contains **no `phone` claim**. (The JSON response body still echoes the `phone` you supplied, but it is not verified — do not treat it as phone-ownership proof.)

**Failure responses** (all `401`, distinguished by message):

| Case | Status | Body |
|---|---|---|
| Wrong code | `401` | `{ "error": "Incorrect code. Please try again." }` |
| Expired code | `401` | `{ "error": "Code has expired. Please request a new one." }` |
| No code on file | `401` | `{ "error": "No OTP found for this email. Please request a new code." }` |
| Too many attempts | `401` | `{ "error": "Too many incorrect attempts. Please request a new code." }` |
| Missing field | `400` | `{ "error": "Email and code are required" }` |

**Webhook?** **No.** The email channel is check-only — it never fires a webhook. The result
is returned synchronously in the `confirm` response.

---

## 5. Code & limits

| Setting | Value |
|---|---|
| OTP format | Short numeric code |
| OTP expiry | Short-lived — expires after a few minutes |
| Max verify attempts (email) | Limited — the code is invalidated after a few incorrect tries |
| Reverse session lifetime | Short-lived — see `expiresIn` in the start response |
| Resend | Brief cooldown between resends |

**After max attempts (email):** once the attempt limit is reached, the code is
invalidated; the next attempt returns *"Too many incorrect attempts. Please request a new
code."* and you must request a new code.

**Resend / regenerate:**
- There is **no separate resend endpoint.** To resend, call the same START endpoint again.
- START is **not idempotent** — re-calling `POST /api/verify/email/send` (email) or
  `POST /api/mobileverify/start` (phone) **generates a new code** and supersedes the
  previous one, which stops working.

**Rate limits:** all `/api/*` requests are rate-limited per IP. Honor HTTP `429` and back off using the `retryAfter` value returned in the response body:
```json
{ "error": "Too many requests. Please try again later.", "code": "RATE_LIMITED", "retryAfter": <seconds> }
```

---

## 6. Plans & channel access

| Plan | WhatsApp | Telegram | SMS | Email | Monthly verifications | SMS / month |
|---|:---:|:---:|:---:|:---:|---|---|
| **Free** | — | — | — | ✅ | 1,000 | 0 |
| **Starter** | ✅ | ✅ | ✅ | ✅ | 10,000 | 50 |
| **Growth** | ✅ | ✅ | ✅ | ✅ | 100,000 | 200 |
| **Enterprise** | ✅ | ✅ | ✅ | ✅ | Unlimited | Unlimited |

The reverse channels (WhatsApp / Telegram / SMS) require **Starter or higher.** Email is
available on **every** tier.

**What actually limits you:**
- Your **overall monthly verification count** is enforced on every call — exceeding your
  tier's limit returns `429`. (Enterprise = unlimited.)
- Your **SMS monthly allowance** is enforced when you call `POST /api/mobileverify/start`.
  Once it is used up, the SMS links are omitted from the response while WhatsApp, Telegram,
  and Email continue to work.
- Email usage counts against your **overall** monthly verification limit; there is no
  separate email-only sub-limit applied.

**Upgrades take effect immediately.** Your plan and limits are read live from your account
on every request, so an upgrade (or downgrade/expiry) applies to your very next call — no
key rotation needed. An expired paid plan is treated as Free until renewed.

---

## 7. Errors

| Condition | Status | Example body |
|---|---|---|
| Missing API key | `401` | `{ "success": false, "error": "API key required. Include X-API-Key header." }` |
| Invalid / revoked key | `401` | `{ "success": false, "error": "Invalid or revoked API key." }` |
| Channel not on plan | `403` | `{ "success": false, "error": "WhatsApp verification is not available on the free plan. Upgrade to Starter or higher.", "upgrade": true }` |
| Monthly limit exceeded | `429` | `{ "success": false, "error": "Monthly verification limit reached (1,000). Upgrade your plan for more.", "currentUsage": 1000, "limit": 1000, "plan": "free", "upgrade": true }` |
| Rate limit exceeded | `429` | `{ "error": "Too many requests. Please try again later.", "code": "RATE_LIMITED", "retryAfter": <seconds> }` |
| Bad input (missing phone) | `400` | `{ "success": false, "error": "Phone number required" }` |
| Invalid code (email) | `401` | `{ "error": "Incorrect code. Please try again." }` |
| Expired code (email) | `401` | `{ "error": "Code has expired. Please request a new one." }` |

---

## 8. Quick-start curl examples

Replace `KEY`, phone, and email with your values. `SOCKETROOM` and `CODE` come from the
START response.

### WhatsApp
```bash
# 1) Start
curl -X POST https://akidotp.com/api/mobileverify/start \
  -H "X-API-Key: KEY" -H "Content-Type: application/json" \
  -d '{"phone":"+218912345678"}'
# → { "code":"483920", "socketRoom":"SOCKETROOM", "whatsappLink":"https://wa.me/…?text=483920", ... }

# 2) Show CODE to the user and open whatsappLink; the user SENDS it to AkidOTP on WhatsApp.

# 3) Poll for the result
curl "https://akidotp.com/api/mobileverify/status?room=SOCKETROOM" \
  -H "X-API-Key: KEY"
# → { "verified": true, "phone": "+218912345678" }
```

### Telegram
```bash
# 1) Start
curl -X POST https://akidotp.com/api/mobileverify/start \
  -H "X-API-Key: KEY" -H "Content-Type: application/json" \
  -d '{"phone":"+218912345678"}'
# → { "code":"483920", "socketRoom":"SOCKETROOM", "telegramLink":"https://t.me/AkidOTPVerify_Bot?start=483920", ... }

# 2) User opens telegramLink, taps Start, and shares their contact.

# 3) Poll for the result
curl "https://akidotp.com/api/mobileverify/status?room=SOCKETROOM" \
  -H "X-API-Key: KEY"
# → { "verified": true, "phone": "+218912345678" }
```

### SMS
```bash
# 1) Start
curl -X POST https://akidotp.com/api/mobileverify/start \
  -H "X-API-Key: KEY" -H "Content-Type: application/json" \
  -d '{"phone":"+218912345678"}'
# → { "code":"483920", "socketRoom":"SOCKETROOM",
#     "smsNumber":"<inbound number>", "smsBody":"483920",
#     "smsLink":"sms:<number>?body=483920", "smsLinkIOS":"sms:<number>&body=483920", ... }

# 2) User taps smsLink (or texts smsBody to smsNumber) and SENDS it.

# 3) Poll for the result
curl "https://akidotp.com/api/mobileverify/status?room=SOCKETROOM" \
  -H "X-API-Key: KEY"
# → { "verified": true, "phone": "+218912345678" }
```

### Email
```bash
# 1) Send the code
curl -X POST https://akidotp.com/api/verify/email/send \
  -H "X-API-Key: KEY" -H "Content-Type: application/json" \
  -d '{"phone":"+218912345678","email":"user@example.com"}'
# → { "sent": true, "message": "Verification code sent to user@example.com" }

# 2) User reads the emailed 6-digit code. Confirm it:
curl -X POST https://akidotp.com/api/verify/email/confirm \
  -H "X-API-Key: KEY" -H "Content-Type: application/json" \
  -d '{"email":"user@example.com","code":"483920"}'
# → { "verified": true, "phone": "+218912345678", "email": "user@example.com", "token": "<JWT>" }
```
