Skip to main content

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:
Authorization: Bearer YOUR_API_KEY
Never expose your API key in client-side code or public repositories. Always make API calls from your backend.

Getting Your API Key

  1. Log in to your EasyOTP dashboard
  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:

Response Format

All API responses follow a consistent structure:

Success Response

{
  "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

{
  "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 CodeMeaning
200Success - The request was successful
400Bad Request - Invalid parameters or malformed request
401Unauthorized - Invalid or missing API key
402Payment Required - Insufficient credits
403Forbidden - API key is disabled
404Not Found - Resource doesn’t exist
429Too Many Requests - Rate limit exceeded
500Internal 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 TypeRestrictionPurpose
Per Minute2 OTP sendsPrevents rapid-fire spam and resend abuse
Per Hour15 OTP sendsAllows flexibility for legitimate retries
Per 24 Hours50 OTP sendsPrevents 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:
{
  "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:
{
  "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

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;
    }
  }
}

Best Practices

Limit verification attempts on your frontend (3-5 attempts) before users hit the API limit. This provides a better user experience.
Wait at least 30-60 seconds between allowing users to request new codes. This prevents accidental abuse.
Store rate limit information locally to avoid unnecessary API calls when limits are reached.
Use the logs dashboard to track your API usage patterns and identify potential issues.
Always check for 429 responses and use the retry_after field to inform users when they can try again.
Test mode recipients (e.g., +15555550100) are exempt from per-recipient rate limits, but API key limits still apply.

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 NumberBehaviorCode to Use
+15555550100Always succeeds123456
+15555550101Always fails with “Invalid code”Any code
+15555550102Expires immediately (timeout simulation)123456 (but will be expired)

Test Email Addresses

Email AddressBehaviorCode to Use
[email protected]Always succeeds123456
[email protected]Always fails with “Invalid code”Any code

Example Test Flow

// 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
Test recipients don’t actually send messages and won’t consume credits. Use them to test your integration without costs.

Code Examples

Complete Verification Flow

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);

Need Help?