Optora API v2
A lightweight, production-ready OTP email verification API. Send one-time codes or magic links, verify them in one step, and get notified via webhooks — all over a simple REST interface.
How it works
/api/otp/generate with an email/api/otp/verify/api/otp/send-link with an emailAuthentication
Optora is a self-hosted API — no API keys or tokens are required on requests. Secure it at the network or proxy level (firewall, reverse proxy, allowlist) for production use.
Base URL
All endpoints are relative to your deployment URL. The panel below shows your current deployment's base URL, detected automatically.
API Playground
Test all endpoints live from your browser. Requests go directly to this deployment — real emails will be sent.
Enter the email and code that was sent in the Send Code tab above.
Paste the requestId returned from Send Code or Send Link.
Fires 6 rapid requests to the same email — the last few should return 429 Too Many Requests.
Send Verification Code
Generates a one-time code and emails it to the user. Use the returned requestId to check verification status later.
Request body
| Field | Type | Required | Description |
|---|---|---|---|
| string | required | Recipient email address | |
| organization | string | optional | Shown in the email as the sender name. Default: "Verification" |
| subject | string | optional | Email subject line. Default: "Your verification code" |
| type | "numeric" | "alpha" | "alphanumeric" | optional | OTP character set. Default: "numeric" |
Example request
curl -X POST https://your-app.vercel.app/api/otp/generate \ -H "Content-Type: application/json" \ -d '{ "email": "user@example.com", "organization": "Acme Inc", "type": "numeric", "subject": "Your verification code" }'
Response
{
"message": "Verification code sent to your email",
"mode": "code",
"requestId": "b7e3a2f1c4d8e9a0b1c2d3e4",
"validityMinutes": 5
}
Send Magic Link
Emails a one-click verification link. When clicked, the link verifies the token server-side and optionally fires a webhook to your backend.
Request body
| Field | Type | Required | Description |
|---|---|---|---|
| string | required | Recipient email address | |
| organization | string | optional | Sender name shown in the email. Default: "Verification" |
| subject | string | optional | Email subject line. Default: "Verify your email" |
| webhookUrl | string | optional | URL to POST to after the user clicks the link. Must start with http:// or https:// |
Example request
curl -X POST https://your-app.vercel.app/api/otp/send-link \ -H "Content-Type: application/json" \ -d '{ "email": "user@example.com", "organization": "Acme Inc", "webhookUrl": "https://your-server.com/hooks/verified" }'
Response
{
"message": "Verification link sent to your email",
"mode": "link",
"requestId": "c9f4b3e2d5a6b7c8d9e0f1a2",
"validityMinutes": 5,
"webhookRegistered": true
}
Verify Code
Validates an OTP submitted by the user. Returns success if the code matches and has not expired.
Request body
| Field | Type | Required | Description |
|---|---|---|---|
| string | required | The email address used when generating the OTP | |
| otp | string | number | required | The code entered by the user |
Example request
curl -X POST https://your-app.vercel.app/api/otp/verify \ -H "Content-Type: application/json" \ -d '{"email": "user@example.com", "otp": "482910"}'
Verify Magic Link
This endpoint is called automatically when the user clicks the link in their email. You do not need to call it from your backend.
Query parameters
| Param | Type | Required | Description |
|---|---|---|---|
| token | string | required | The verification token included in the magic link |
Check Status
Poll the verification status of a request by its ID. Useful as a reliable fallback when webhooks are not available.
Path parameters
| Param | Type | Required | Description |
|---|---|---|---|
| requestId | string | required | ID returned by /otp/generate or /otp/send-link |
Response
{
"requestId": "b7e3a2f1c4d8e9a0",
"email": "user@example.com",
"verified": true,
"verifiedAt": "2026-05-28T10:30:00.000Z",
"expiresAt": "2026-05-28T10:35:00.000Z"
}
Rate Limiting
Requests are rate-limited per IP and email address. Configure thresholds with environment variables.
| Setting | Default | Description |
|---|---|---|
| Max requests | 5 | Per IP or email per window. Set via RATE_LIMIT_MAX_REQUESTS |
| Window | 15 min | Rolling window duration. Set via RATE_LIMIT_WINDOW_MINUTES |
| Response | 429 | Includes retryAfterSeconds in the JSON body |
Webhooks
Optora can notify your server when a magic-link verification completes. Pass webhookUrl in the /otp/send-link request.
Payload delivered to your server
{
"event": "email.verified",
"email": "user@example.com",
"requestId": "b7e3a2f1...",
"verifiedAt": "2026-05-28T10:30:00.000Z"
}
- Fire-and-forget — the confirmation page does not wait for your server to respond
- 5-second timeout. Slow or unreachable URLs are aborted and logged server-side
- Only
http://andhttps://URLs are accepted - No automatic retries — use
GET /api/otp/status/:requestIdas a reliable fallback
Environment Variables
All variables live in sample.env. Copy it to .env for local development. For Vercel, add them under Settings → Environment Variables.
| Variable | Required | Default | Description |
|---|---|---|---|
| MONGODB_URI | required | — | MongoDB connection string |
| GMAIL_USER | required | — | Sender Gmail address. Must have an App Password enabled |
| GMAIL_PASS | required | — | 16-character Gmail App Password |
| OTP_VALIDITY_PERIOD_MINUTES | required | 5 | How long a code or link token stays valid |
| OTP_SIZE | required | 6 | Number of characters in the OTP |
| APP_BASE_URL | optional | auto | Public URL for magic links. Falls back to $VERCEL_URL then request host |
| DOCS_API_URL | optional | auto | Base URL used by live Try It panels on the docs page. Defaults to same origin. |
| RATE_LIMIT_MAX_REQUESTS | optional | 5 | Max requests per window per IP or email |
| RATE_LIMIT_WINDOW_MINUTES | optional | 15 | Length of the rate-limit rolling window in minutes |
| BLOCK_KEYWORDS_RULES | optional | — | Comma-separated keywords that auto-reject requests |
| ALLOWED_DOMAINS | optional | — | Comma-separated domain allow-list. Empty = all accepted |
| PORT | local only | 5001 | Dev server port. Vercel ignores this |
Error Reference
All errors return { "error": "message" }.
| Status | Error | Cause |
|---|---|---|
| 400 | Invalid email | Missing or malformed email field |
| 400 | Spam detected | IP or email is blocklisted, or body contains a blocked keyword |
| 400 | Maximum attempts reached | Same email hit the 3-attempt limit within the validity window |
| 400 | Invalid OTP | Code is wrong, expired, or already used |
| 400 | Verification link is invalid or has expired | No matching token found, or token has expired |
| 429 | Too many requests | Rate limit exceeded. Check retryAfterSeconds in the response |
| 500 | Internal server error | Unexpected database or runtime error |
Optora
MIT License · GitHub