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

# API Reference

> Complete API documentation for EasyOTP

## Overview

The EasyOTP API provides a simple, RESTful interface for sending and verifying one-time passwords. All requests and responses use JSON, and the API uses standard HTTP response codes.

## Base URL

All API requests should be made to:

```
https://app.easyotp.dev/api/v1
```

## Authentication

EasyOTP uses API keys for authentication. Include your API key in every request using one of these methods:

<CodeGroup>
  ```bash Bearer Token (Recommended) theme={null}
  Authorization: Bearer YOUR_API_KEY
  ```

  ```bash Custom Header theme={null}
  x-api-key: YOUR_API_KEY
  ```
</CodeGroup>

<Warning>
  Never expose your API key in client-side code or public repositories. Always make API calls from your backend.
</Warning>

### Getting Your API Key

1. Log in to your [EasyOTP dashboard](https://app.easyotp.dev)
2. Navigate to the API Keys section
3. Click "Create API Key"
4. Copy and store your key securely

## Endpoints

EasyOTP has two main endpoints:

<CardGroup cols={2}>
  <Card title="Send OTP" icon="mail" href="/api-reference/send">
    `POST /api/v1/send` - Send a verification code via SMS, Email, or Voice
  </Card>

  <Card title="Verify OTP" icon="check" href="/api-reference/verify">
    `POST /api/v1/verify` - Verify a code that was previously sent
  </Card>
</CardGroup>

## Response Format

All API responses follow a consistent structure:

### Success Response

```json theme={null}
{
  "success": true,
  "verification_id": "11f951d5-32d1-4b49-bdda-7da248e2615c",
  "expires_at": "2024-01-01T12:05:00.000Z",
  "request_id": "7b4d6022-7260-4568-b6b7-29c366c47bbc"
}
```

### Error Response

```json theme={null}
{
  "error": "Error message describing what went wrong",
  "request_id": "7b4d6022-7260-4568-b6b7-29c366c47bbc"
}
```

The `request_id` is included in all responses and can be used when contacting support to help debug issues.

## HTTP Status Codes

The API uses standard HTTP status codes:

| Status Code | Meaning                                                 |
| ----------- | ------------------------------------------------------- |
| `200`       | Success - The request was successful                    |
| `400`       | Bad Request - Invalid parameters or malformed request   |
| `401`       | Unauthorized - Invalid or missing API key               |
| `402`       | Payment Required - Insufficient credits                 |
| `403`       | Forbidden - API key is disabled                         |
| `404`       | Not Found - Resource doesn't exist                      |
| `429`       | Too Many Requests - Rate limit exceeded                 |
| `500`       | Internal Server Error - Something went wrong on our end |

## Rate Limits

To ensure fair usage and system stability, the API implements comprehensive rate limiting at both the API key and recipient levels.

### API Key Limits

These limits apply to your entire API key and are enforced by the API key authentication system:

* **120 requests per minute** per API key
* Limits are applied across all endpoints using the same API key
* Rate limiting is automatically handled - you'll receive a `429 Too Many Requests` response when limits are exceeded

### Per-Recipient Limits (Send)

To prevent spam and abuse, these limits apply per phone number or email address:

| Limit Type       | Restriction  | Purpose                                         |
| ---------------- | ------------ | ----------------------------------------------- |
| **Per Minute**   | 2 OTP sends  | Prevents rapid-fire spam and resend abuse       |
| **Per Hour**     | 15 OTP sends | Allows flexibility for legitimate retries       |
| **Per 24 Hours** | 50 OTP sends | Prevents long-term abuse while allowing testing |

When a per-recipient limit is exceeded, the API returns a `429` response with a `retry_after` field indicating when you can try again.

### Verification Attempt Limits

To prevent brute-force attacks on verification codes:

* **5 failed attempts per code**: After 5 incorrect attempts on the same verification code, the code is locked for 15 minutes
* **Lockout period**: 15 minutes before you can try again (or request a new code)
* **Automatic lockout**: The system automatically locks the verification code after the limit is reached
* **Recommendation**: Implement client-side retry limits (3-5 attempts) before the lockout to provide better user experience

### Rate Limit Responses

When you exceed a rate limit, you'll receive a `429 Too Many Requests` response with a `retry_after` field (in seconds):

**Per-Recipient Send Limit Example:**

```json theme={null}
{
  "error": "Rate limit exceeded: Maximum 2 verification codes per minute. Please try again in 42 seconds.",
  "request_id": "7b4d6022-7260-4568-b6b7-29c366c47bbc",
  "retry_after": 42
}
```

**Verification Attempt Limit Example:**

```json theme={null}
{
  "error": "Too many failed attempts. Please try again in 15 minutes or request a new code.",
  "request_id": "7b4d6022-7260-4568-b6b7-29c366c47bbc",
  "retry_after": 900
}
```

### Handling Rate Limits

<CodeGroup>
  ```javascript Node.js theme={null}
  async function sendWithRetry(recipient, maxRetries = 3) {
    for (let i = 0; i < maxRetries; i++) {
      try {
        const response = await fetch('https://app.easyotp.dev/api/v1/send', {
          method: 'POST',
          headers: {
            'Authorization': `Bearer ${API_KEY}`,
            'Content-Type': 'application/json',
          },
          body: JSON.stringify({
            channel: 'sms',
            recipient: recipient,
            message: 'Your verification code is: {code}'
          })
        });

        const data = await response.json();

        if (response.status === 429 && data.retry_after) {
          if (i < maxRetries - 1) {
            await new Promise(resolve => setTimeout(resolve, data.retry_after * 1000));
            continue;
          }
        }

        return data;
      } catch (error) {
        if (i === maxRetries - 1) throw error;
      }
    }
  }
  ```

  ```python Python theme={null}
  import time
  import requests

  def send_with_retry(recipient, max_retries=3):
      for i in range(max_retries):
          response = requests.post(
              'https://app.easyotp.dev/api/v1/send',
              headers={
                  'Authorization': f'Bearer {API_KEY}',
                  'Content-Type': 'application/json',
              },
              json={
                  'channel': 'sms',
                  'recipient': recipient,
                  'message': 'Your verification code is: {code}'
              }
          )
          
          data = response.json()
          
          if response.status_code == 429 and 'retry_after' in data:
              if i < max_retries - 1:
                  time.sleep(data['retry_after'])
                  continue
          
          return data
      
      raise Exception("Max retries exceeded")
  ```
</CodeGroup>

### Best Practices

<AccordionGroup>
  <Accordion title="Implement client-side limits">
    Limit verification attempts on your frontend (3-5 attempts) before users hit the API limit. This provides a better user experience.
  </Accordion>

  <Accordion title="Add delays between resends">
    Wait at least 30-60 seconds between allowing users to request new codes. This prevents accidental abuse.
  </Accordion>

  <Accordion title="Cache rate limit info">
    Store rate limit information locally to avoid unnecessary API calls when limits are reached.
  </Accordion>

  <Accordion title="Monitor your usage">
    Use the logs dashboard to track your API usage patterns and identify potential issues.
  </Accordion>

  <Accordion title="Handle 429 responses gracefully">
    Always check for 429 responses and use the `retry_after` field to inform users when they can try again.
  </Accordion>
</AccordionGroup>

<Note>
  Test mode recipients (e.g., `+15555550100`) are exempt from per-recipient rate limits, but API key limits still apply.
</Note>

## Idempotency

The send endpoint generates a new verification code on each request. If you need to retry a failed request, wait for the previous request to time out or use a different flow.

## Webhooks

Webhooks are not currently supported but are planned for a future release. Subscribe to our changelog for updates.

## Testing

### Test Mode

All API keys can be used in production immediately. For testing without consuming credits, you can use specific test phone numbers and email addresses. These won't actually send messages and won't consume credits.

### Test Phone Numbers

| Phone Number   | Behavior                                 | Code to Use                    |
| -------------- | ---------------------------------------- | ------------------------------ |
| `+15555550100` | Always succeeds                          | `123456`                       |
| `+15555550101` | Always fails with "Invalid code"         | Any code                       |
| `+15555550102` | Expires immediately (timeout simulation) | `123456` (but will be expired) |

### Test Email Addresses

| Email Address      | Behavior                         | Code to Use |
| ------------------ | -------------------------------- | ----------- |
| `test@easyotp.dev` | Always succeeds                  | `123456`    |
| `fail@easyotp.dev` | Always fails with "Invalid code" | Any code    |

### Example Test Flow

```javascript theme={null}
// Send to test number
const sendResponse = await fetch('https://app.easyotp.dev/api/v1/send', {
  method: 'POST',
  headers: {
    'Authorization': 'Bearer YOUR_API_KEY',
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    channel: 'sms',
    recipient: '+15555550100',
    message: 'Your verification code is: {code}'
  })
});

const { verification_id } = await sendResponse.json();

// Verify with the test code
const verifyResponse = await fetch('https://app.easyotp.dev/api/v1/verify', {
  method: 'POST',
  headers: {
    'Authorization': 'Bearer YOUR_API_KEY',
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    verification_id: verification_id,
    code: '123456'
  })
});

const result = await verifyResponse.json();
console.log(result.verified); // true
```

<Note>
  Test recipients don't actually send messages and won't consume credits. Use them to test your integration without costs.
</Note>

## Code Examples

### Complete Verification Flow

<CodeGroup>
  ```javascript Node.js theme={null}
  const EASYOTP_API_KEY = process.env.EASYOTP_API_KEY;
  const BASE_URL = 'https://app.easyotp.dev/api/v1';

  async function sendOTP(phoneNumber) {
    const response = await fetch(`${BASE_URL}/send`, {
      method: 'POST',
      headers: {
        'Authorization': `Bearer ${EASYOTP_API_KEY}`,
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({
        channel: 'sms',
        recipient: phoneNumber,
        message: 'Your verification code is: {code}',
        expires_in: 300
      })
    });
    
    if (!response.ok) {
      const error = await response.json();
      throw new Error(error.error);
    }
    
    return await response.json();
  }

  async function verifyOTP(verificationId, code) {
    const response = await fetch(`${BASE_URL}/verify`, {
      method: 'POST',
      headers: {
        'Authorization': `Bearer ${EASYOTP_API_KEY}`,
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({
        verification_id: verificationId,
        code: code
      })
    });
    
    if (!response.ok) {
      const error = await response.json();
      throw new Error(error.error);
    }
    
    return await response.json();
  }

  const result = await sendOTP('+1234567890');
  console.log('OTP sent:', result.verification_id);

  const verification = await verifyOTP(result.verification_id, '123456');
  console.log('Verified:', verification.verified);
  ```

  ```python Python theme={null}
  import os
  import requests

  EASYOTP_API_KEY = os.environ.get('EASYOTP_API_KEY')
  BASE_URL = 'https://app.easyotp.dev/api/v1'

  def send_otp(phone_number):
      response = requests.post(
          f'{BASE_URL}/send',
          headers={
              'Authorization': f'Bearer {EASYOTP_API_KEY}',
              'Content-Type': 'application/json',
          },
          json={
              'channel': 'sms',
              'recipient': phone_number,
              'message': 'Your verification code is: {code}',
              'expires_in': 300
          }
      )
      response.raise_for_status()
      return response.json()

  def verify_otp(verification_id, code):
      response = requests.post(
          f'{BASE_URL}/verify',
          headers={
              'Authorization': f'Bearer {EASYOTP_API_KEY}',
              'Content-Type': 'application/json',
          },
          json={
              'verification_id': verification_id,
              'code': code
          }
      )
      response.raise_for_status()
      return response.json()

  result = send_otp('+1234567890')
  print(f"OTP sent: {result['verification_id']}")

  verification = verify_otp(result['verification_id'], '123456')
  print(f"Verified: {verification['verified']}")
  ```
</CodeGroup>

## Need Help?

<CardGroup cols={2}>
  <Card title="Support" icon="life-ring" href="mailto:support@easyotp.dev">
    Contact our support team
  </Card>

  <Card title="Status Page" icon="signal" href="https://status.easyotp.dev">
    Check API status and uptime
  </Card>
</CardGroup>
