Optora API
Lightweight OTP email verification with code-based and one-click magic-link flows. Built on Node.js, Express, and MongoDB. Vercel-ready.
Overview
Optora provides two verification flows. Choose the one that fits your product:
/api/otp/generate with email/api/otp/verify with email + code/api/otp/send-link with emailAuthentication
Optora uses no API keys by default — it is designed to run as your own private backend. Protect your endpoints with your own authentication layer (middleware, API gateway, etc.) before deploying publicly.
Quick Start
Base URL — detected automatically from the request host. Override with DOCS_API_URL:
https://your-app.vercel.app
Send a verification code:
curl -X POST https://your-app.vercel.app/api/otp/generate \ -H "Content-Type: application/json" \ -d '{"email": "user@example.com"}'
Generate OTP
Generates a one-time code and emails it to the user. If an unexpired, unverified OTP exists for the email, it is reused (up to 3 attempts).
Request body
| Field | Type | Required | Description |
|---|---|---|---|
| string | required | Recipient email address | |
| type | string | optional | numeric, alphanumeric, or alphabet. Default: "numeric" |
| organization | string | optional | Sender name in the email. Default: "Verification" |
| subject | string | optional | Email subject line. Default: "Your verification code" |
Example request
curl -X POST https://your-app.vercel.app/api/otp/generate \ -H "Content-Type: application/json" \ -d '{ "email": "user@example.com", "type": "numeric", "organization": "Acme Inc" }'
Response
{
"message": "Verification code sent to your email",
"mode": "code",
"requestId": "a1b2c3d4e5f6a7b8c9d0e1f2",
"validityMinutes": 5
}
Send Magic Link
Emails a one-click verification link. When the user clicks the link, their browser opens a confirmation page and — if configured — your server receives a webhook.
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
Called automatically when the user clicks the link in their email. Returns an HTML confirmation page — you do not need to call this 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 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-31T10:30:00.000Z",
"expiresAt": "2026-05-31T10: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 notifies 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-31T10: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
Copy sample.env 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 | optional | 5 | How long a code or link token stays valid |
| OTP_SIZE | optional | 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 |
| 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 | 5000 | 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 server error. Check logs |