# OTPLY Integration Guide

**Add email OTP verification to any project in under 10 minutes.**

OTPLY is a simple OTP-as-a-Service API. You call one endpoint to send a 6-digit code to a user's email, and another to verify what they typed. OTPLY handles code generation, secure storage, expiry, retry limits, and the branded email delivery — you handle nothing but two HTTP calls.

Base URL: `https://api.otply.xyz:8246/api/v1`

---

## 1. Get your API credentials

1. Create an account at **https://otply.xyz/signup.html**
2. Log in — your **API Key** (`otpm_...`) and **API Secret** are on the dashboard
3. Every account starts on the **Trial Plan: 10 free OTP emails per month** (refills monthly, no card required)
4. Subscribe (**$9.99/month** or **$99.99/year**) for **unlimited** OTP emails

Authentication is two headers on every request:

```
X-API-Key: otpm_your_key_here
X-API-Secret: your_secret_here
```

> ⚠️ **Server-side only.** Never put these headers in browser JavaScript or mobile app code — anyone could read them and send OTPs on your bill. Always call OTPLY from your backend.

---

## 2. The two calls that do everything

### Send an OTP

```
POST /api/v1/send-otp
Content-Type: application/json

{
  "email": "user@example.com",
  "purpose": "login",            // optional, default "verification"
  "expiry_minutes": 10           // optional, 1-60, default 10
}
```

Response `200`:

```json
{
  "success": true,
  "message": "OTP sent successfully",
  "expires_in": 600,
  "reference_id": "42"
}
```

(On the Trial Plan the message also shows your remaining quota, e.g. `"... (Trial Plan: 9 of 10 remaining this month)"`.)

### Verify an OTP

```
POST /api/v1/verify-otp
Content-Type: application/json

{
  "email": "user@example.com",
  "otp": "482913",
  "purpose": "login"             // must match the purpose used when sending
}
```

Response `200`:

```json
{ "success": true, "message": "OTP verified successfully", "verified_at": "2026-07-29T18:45:00" }
```

Wrong code:

```json
{ "success": false, "message": "Invalid OTP. 2 attempt(s) remaining." }
```

**Rules baked in for you:** codes are 6 digits; expire after `expiry_minutes`; a maximum of **3 wrong attempts** invalidates the code; a code is deleted the moment it's verified (single use); and the same recipient can only be sent a new code every **30 seconds** (anti-spam) — sending again sooner returns `429` with the wait time.

### About `purpose`

`purpose` namespaces your OTPs. A code sent with `"purpose": "login"` can only be verified with `"purpose": "login"`. Use different purposes for different flows so they never collide — e.g. `signup`, `login`, `password-reset`, `delete-account`, `payout-confirm`. A user can have one active OTP *per purpose* simultaneously.

---

## 3. Common integration patterns

### Pattern A — Email verification at signup
1. User submits your signup form → create the account **unverified**
2. Call `send-otp` with `purpose: "signup"`
3. Show a "enter the 6-digit code" screen
4. Call `verify-otp` → on `success: true`, mark the account verified

### Pattern B — Two-factor login (2FA)
1. User enters correct username + password
2. Call `send-otp` with `purpose: "login"` → show code screen
3. `verify-otp` succeeds → issue your session/JWT

### Pattern C — Password reset
1. User enters their email on "Forgot password"
2. Call `send-otp` with `purpose: "password-reset"`
3. `verify-otp` succeeds → let them set a new password

### Pattern D — Sensitive-action confirmation
Before destructive or high-value actions (delete account, change payout details, large transfer): send with `purpose: "confirm-action"`, proceed only on successful verification.

---

## 4. Code examples

### cURL

```bash
curl -X POST https://api.otply.xyz:8246/api/v1/send-otp \
  -H "X-API-Key: $OTPLY_KEY" -H "X-API-Secret: $OTPLY_SECRET" \
  -H "Content-Type: application/json" \
  -d '{"email":"user@example.com","purpose":"login"}'
```

### Node.js (Express)

```javascript
const OTPLY = 'https://api.otply.xyz:8246/api/v1';
const HEADERS = {
  'X-API-Key': process.env.OTPLY_KEY,
  'X-API-Secret': process.env.OTPLY_SECRET,
  'Content-Type': 'application/json'
};

// Send
app.post('/auth/request-otp', async (req, res) => {
  const r = await fetch(`${OTPLY}/send-otp`, {
    method: 'POST', headers: HEADERS,
    body: JSON.stringify({ email: req.body.email, purpose: 'login' })
  });
  res.status(r.status).json(await r.json());
});

// Verify
app.post('/auth/verify-otp', async (req, res) => {
  const r = await fetch(`${OTPLY}/verify-otp`, {
    method: 'POST', headers: HEADERS,
    body: JSON.stringify({ email: req.body.email, otp: req.body.otp, purpose: 'login' })
  });
  const data = await r.json();
  if (data.success) {
    // issue your session / JWT here
  }
  res.json(data);
});
```

### Python (any framework)

```python
import os, requests

OTPLY = "https://api.otply.xyz:8246/api/v1"
HEADERS = {
    "X-API-Key": os.environ["OTPLY_KEY"],
    "X-API-Secret": os.environ["OTPLY_SECRET"],
}

def send_otp(email: str, purpose: str = "verification") -> dict:
    r = requests.post(f"{OTPLY}/send-otp", headers=HEADERS,
                      json={"email": email, "purpose": purpose}, timeout=30)
    return r.json()

def verify_otp(email: str, otp: str, purpose: str = "verification") -> bool:
    r = requests.post(f"{OTPLY}/verify-otp", headers=HEADERS,
                      json={"email": email, "otp": otp, "purpose": purpose}, timeout=30)
    return r.json().get("success", False)
```

### PHP

```php
<?php
function otply(string $endpoint, array $body): array {
    $ch = curl_init("https://api.otply.xyz:8246/api/v1/$endpoint");
    curl_setopt_array($ch, [
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_POST => true,
        CURLOPT_HTTPHEADER => [
            'X-API-Key: ' . getenv('OTPLY_KEY'),
            'X-API-Secret: ' . getenv('OTPLY_SECRET'),
            'Content-Type: application/json'
        ],
        CURLOPT_POSTFIELDS => json_encode($body),
        CURLOPT_TIMEOUT => 30
    ]);
    $out = json_decode(curl_exec($ch), true);
    curl_close($ch);
    return $out ?: ['success' => false, 'message' => 'Connection error'];
}

// Send:   otply('send-otp',   ['email' => $email, 'purpose' => 'login']);
// Verify: otply('verify-otp', ['email' => $email, 'otp' => $code, 'purpose' => 'login']);
```

---

## 5. Other endpoints

| Endpoint | Method | Purpose |
|---|---|---|
| `/account` | GET | Your plan, trial quota remaining, and usage stats (sent / verified) |
| `/checkout` | POST | Create a subscription checkout — body `{"billing_cycle": "monthly"}` or `"yearly"`, returns a payment URL |
| `/billing-portal` | GET | Link to manage payment method, invoices, cancellation |
| `/subscription/cancel` | POST | Turn off auto-renewal (access continues until the paid period ends) |
| `/health` | GET | Service status (no auth needed) — useful for your monitoring |

---

## 6. Errors & handling

| HTTP | Meaning | What to do |
|---|---|---|
| `401` | Bad API key/secret | Check headers and credentials from your dashboard |
| `403` | Trial quota exhausted (10/month used) | Subscribe for unlimited, or wait for next month's refill |
| `422` | Invalid input (bad email, OTP not 6 digits, expiry out of 1–60) | Fix the request body |
| `429` | Resend cooldown — same recipient within 30 s | Wait the seconds indicated in the message; show a countdown in your UI |
| `500` | Email delivery or internal failure | Retry after a moment; trial quota is auto-refunded on failed sends |

Note that a **wrong OTP is not an HTTP error** — verify returns `200` with `"success": false` so your code should branch on the `success` field, not the status code.

---

## 7. Best practices

- **Keep credentials in environment variables**, never in source control or client-side code.
- **Use distinct `purpose` values per flow** so a signup code can't be replayed on a password reset.
- **Show a resend button with a 30-second countdown** — matching the API's cooldown gives users a clear UX instead of surprise errors.
- **Don't reveal whether an email is registered.** On flows like password reset, respond "If that address exists, we've sent a code" regardless — call OTPLY only when the account exists.
- **Handle the 3-attempt lockout gracefully**: after "Maximum verification attempts exceeded", offer to send a fresh code.
- **Rate-limit your own public endpoints** (the ones your users hit) — OTPLY protects recipients from spam, but your signup form should also throttle per-IP to protect your quota/bill.
- **Monitor your success rate** on the dashboard: consistently low verification rates usually mean deliverability issues (spam folders) or UX friction on your code-entry screen.

---

## 8. Quick reference card

```
BASE       https://api.otply.xyz:8246/api/v1
AUTH       X-API-Key + X-API-Secret headers (server-side only)
SEND       POST /send-otp     {email, purpose?, expiry_minutes?}
VERIFY     POST /verify-otp   {email, otp, purpose?}
CODES      6 digits · single use · 3 attempts max · default 10 min expiry
COOLDOWN   30 s per recipient+purpose
PLANS      Trial: 10 emails/month free · Subscribers: unlimited ($9.99/mo or $99.99/yr)
```

*OTPLY — Simple, Fast, Reliable. https://otply.xyz*
