Documentation
Everything you need to integrate the Nixify OTP API. Public — no sign-in required.
Create a test API key
Go to API Keys in the dashboard, click Create API Key, choose development environment, then copy the generated mg_test_… key. Test keys run in sandbox mode automatically — no real email is sent and the OTP code is returned in the response body.
Make your first request
curl -X POST https://nixify.vercel.app/api/v1/otp/send \
-H "Authorization: Bearer mg_test_xxx" \
-H "Content-Type: application/json" \
-d '{"email":"user@example.com","purpose":"signup"}'Use your mg_test_ key for the Quick Start. The response includes a code field with the plaintext OTP so you can call /verify immediately without checking an inbox.
Verify the code
const res = await fetch('https://nixify.vercel.app/api/v1/otp/send', {
method: 'POST',
headers: {
'Authorization': 'Bearer mg_test_xxx',
'Content-Type': 'application/json',
},
body: JSON.stringify({ email: 'user@example.com', purpose: 'signup' }),
});
const data = await res.json();
console.log(data.code); // sandbox: the plaintext OTP (mg_test_ only)
console.log(data.otp_request_id);
// Then verify:
const verify = await fetch('https://nixify.vercel.app/api/v1/otp/verify', {
method: 'POST',
headers: {
'Authorization': 'Bearer mg_test_xxx',
'Content-Type': 'application/json',
},
body: JSON.stringify({
email: 'user@example.com',
code: data.code,
purpose: 'signup',
}),
});
console.log((await verify.json()).verified); // truemg_test_ keys run in sandbox mode (no real email, code returned in the response, per-email rate limits skipped). mg_live_ keys send real email via Nixify's managed delivery and enforce all rate limits. When you're ready to go live, create a production environment key and swap mg_test_xxx for mg_live_xxx in your code.How this works
- Click Copy Prompt below — the prompt is pre-written and includes all the API details.
- Paste it into any AI model (ChatGPT, Claude, DeepSeek, or Z.ai).
- Replace
[MY PROGRAMMING LANGUAGE]with your language (JavaScript, Python, PHP, Go, etc.). - The AI will generate a complete, beginner-friendly step-by-step guide with full code, error handling, and comments.
You are a helpful coding assistant. I'm a beginner and I want to use the Nixify email OTP verification service in my project.
Please write a complete, step-by-step guide for integrating Nixify into my app using [MY PROGRAMMING LANGUAGE — e.g., JavaScript, Python, PHP, Go, Ruby, Java, C#]. Explain each step simply so a beginner can follow along.
Here's what you need to know about Nixify:
SERVICE OVERVIEW
- Nixify is an email OTP (one-time password) verification API.
- You send a user's email address to Nixify, Nixify emails them a 6-digit code, then you verify the code they entered.
- Three API endpoints: send OTP, verify OTP, resend OTP.
- Base URL: https://nixify.vercel.app/api/v1
AUTHENTICATION
- Create an API key in the Nixify dashboard. Use mg_test_ for development and CI, mg_live_ for production.
- Send the key as a Bearer token in the Authorization header:
Authorization: Bearer mg_test_xxxxxxxxxxxxxxxxxxxxxxxx
SANDBOX MODE (mg_test_ keys only)
- Test keys run in sandbox mode automatically: OTPs are generated and stored but NO real email is sent. The plaintext 6-digit code is returned in the "code" field of the /send and /resend response so you can call /verify immediately without an inbox.
- Test keys skip the per-email rate limit (3/min, 10/hour) so CI can run fast. The per-IP limit still applies. User-owned test keys still consume the plan API_MESSAGES quota.
- Optionally force a simulated error with the X-Sandbox-Simulate header: rate_limited, locked, expired, mismatch, smtp_error.
- Live keys (mg_live_) CANNOT use sandbox mode — they always send real email.
STEP 1 — SEND OTP
POST /api/v1/otp/send
Content-Type: application/json
Authorization: Bearer YOUR_API_KEY
Body:
{
"email": "user@example.com",
"purpose": "signup"
}
Response (200):
{
"otp_request_id": "uuid-here",
"request_id": "trace-uuid",
"expires_at": "2026-07-06T22:50:00.000Z",
"message": "OTP sent",
"code": "123456"
}
With a mg_live_ key, Nixify emails the user a 6-digit code and the "code" field is NOT present. The code expires in 10 minutes. With a mg_test_ key, no email is sent and "code" contains the plaintext OTP (sandbox mode).
STEP 2 — VERIFY OTP
POST /api/v1/otp/verify
Content-Type: application/json
Authorization: Bearer YOUR_API_KEY
Body:
{
"email": "user@example.com",
"code": "123456",
"purpose": "signup"
}
Response (200):
{
"verified": true,
"otp_request_id": "uuid-here",
"request_id": "trace-uuid"
}
If verified is true, the email is confirmed. Each code can only be used once.
STEP 3 — RESEND OTP (optional, if the user didn't get the email)
POST /api/v1/otp/resend
Body: { "email": "user@example.com", "purpose": "signup" }
RATE LIMITS
- Per email — /send: 3 per minute, 10 per hour (mg_live_ keys only; test keys skip these)
- Per IP — /send: 10 per minute, 60 per hour (all keys)
- Per IP — /verify: 30 per minute, 120 per hour (all keys)
- When rate limited, the API returns 429. IP-level and email-level 429s include a Retry-After header (seconds); email-level 429s also include X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset. Plan-rate 429s (rate_limited from the per-minute plan rate) include X-RateLimit-Reset and X-Quota-Remaining instead of Retry-After.
- All responses include X-Request-Id (matches the body request_id) and X-Api-Version: 1. Successful (2xx) responses include X-Quota-Remaining.
ERROR HANDLING
The API returns JSON errors with this shape:
{ "error": { "code": "rate_limited", "message": "Too many OTP sends.", "doc_url": "/docs#error-rate_limited" }, "request_id": "uuid" }
The doc_url field points to a public docs anchor that explains the code.
Common error codes: validation_failed, unauthorized, key_revoked, key_expired, insufficient_scope, rate_limited, code_mismatch, expired, already_used, locked, not_found, ip_blocked, quota_exceeded, feature_not_available, internal_error.
WHAT I NEED FROM YOU
1. Write the complete integration in [MY LANGUAGE] — a single file I can run.
2. Include all three steps: send, verify, resend.
3. Show how to handle errors (try/catch, check response status, display the error message to the user).
4. Note that otp_request_id is for correlation/observability only — verify does NOT require it as input.
5. Add comments explaining each line for a beginner.
6. Show how to test it locally (what to install, how to run it).
7. Keep it simple — no frameworks, just plain [MY LANGUAGE] code using the standard library or a simple HTTP client.Now paste it into an AI model
Hover over the button below, then click any AI model to open it in a new tab. Paste the prompt, replace the language placeholder, and you'll get a complete guide.
Send your API key in the Authorization header as a Bearer token:
Authorization: Bearer mg_test_xxxxxxxxxxxxxxxxxxxxxxxx
Development & CI. Sandbox mode is automatic — OTPs are generated and persisted exactly as in production, but no real email is sent; the plaintext code is returned in the code field of the/send and /resend response. Per-email rate limits are skipped so tests can run fast. Plan API_MESSAGES quota still applies to user-owned test keys.
Optionally force simulated errors with the X-Sandbox-Simulate header (one of rate_limited, locked,expired, mismatch,smtp_error). Live keys cannot use sandbox mode.
Production only. Nixify sends real email through its managed delivery infrastructure (API customers do not provide SMTP credentials). All rate limits and quotas are enforced. Sandbox mode is not available.
/api/v1/otp/sendIssue + deliver a new OTP code to the given email.
Request body
| email* | string | RFC 5322 email address (lowercased, trimmed) |
| purpose | string | signup | login | reset (defaults to signup) |
Response body
| otp_request_id | string | OTP correlation ID (for webhook correlation) |
| request_id | string | API request trace ID (matches X-Request-Id header) |
| expires_at | string (ISO) | 10-minute TTL |
| message | string | "OTP sent" |
| code | string | Sandbox only (mg_test_ keys): the plaintext 6-digit OTP. Never present for mg_live_ keys. |
{
"email": "user@example.com",
"purpose": "signup"
}{
"otp_request_id": "f3a2b1c8-...",
"request_id": "a1b2c3d4-...",
"expires_at": "2026-07-06T22:50:00.000Z",
"message": "OTP sent",
"code": "123456"
}Possible errors
/api/v1/otp/verifyVerify the 6-digit code entered by the user.
Request body
| email* | string | Same email used in /send |
| code* | string | Exactly 6 numeric digits |
| purpose | string | signup | login | reset (defaults to signup) |
Response body
| verified | boolean | true on success |
| otp_request_id | string | OTP correlation ID of the consumed attempt |
| request_id | string | API request trace ID (matches X-Request-Id header) |
{
"email": "user@example.com",
"code": "123456",
"purpose": "signup"
}{
"verified": true,
"otp_request_id": "f3a2b1c8-...",
"request_id": "a1b2c3d4-..."
}Possible errors
/api/v1/otp/resendSend a fresh code if the user didn't receive the first one.
Request body
| email* | string | Target email |
| purpose | string | signup | login | reset (defaults to signup) |
Response body
| otp_request_id | string | OTP correlation ID for the new attempt |
| request_id | string | API request trace ID (matches X-Request-Id header) |
| expires_at | string (ISO) | 10-minute TTL |
| message | string | "OTP resent" |
| code | string | Sandbox only (mg_test_ keys): the plaintext 6-digit OTP. Never present for mg_live_ keys. |
{
"email": "user@example.com",
"purpose": "signup"
}{
"otp_request_id": "9c1d7e44-...",
"request_id": "e5f6g7h8-...",
"expires_at": "2026-07-06T22:55:00.000Z",
"message": "OTP resent",
"code": "654321"
}Possible errors
All endpoints can also return authentication errors (unauthorized, key_revoked,key_expired, insufficient_scope) and plan-entitlement errors (quota_exceeded,feature_not_available). See the Error Codes section below for the full catalog.
const res = await fetch('https://nixify.vercel.app/api/v1/otp/send', { method: 'POST', headers: { 'Authorization': 'Bearer mg_test_xxx', 'Content-Type': 'application/json' }, body: JSON.stringify({ email: 'user@example.com', purpose: 'signup' }) });import requests; res = requests.post('https://nixify.vercel.app/api/v1/otp/send', headers={'Authorization': 'Bearer mg_test_xxx'}, json={'email': 'user@example.com', 'purpose': 'signup'})Register endpoint URLs in the Webhooks dashboard. Each delivery is signed with HMAC-SHA256 and includes the Nixify-Signature and Nixify-Event headers:
Nixify-Signature: t=1720000000000,v1=8c2f1e9a7b3d4f5e6a8b9c0d1e2f3a4b5c6d7e8f9a0b1c2d3e4f5a6b7c8d9e0f Nixify-Event: otp.sent Content-Type: application/json
The t component is a millisecond timestamp; v1 is the HMAC-SHA256 of `${t}.${payload}` using your endpoint secret. Reject any delivery older than 5 minutes to prevent replay attacks.
Verify the signature
import crypto from 'crypto';
function verify(secret, payload, signatureHeader) {
const { t, v1 } = Object.fromEntries(
signatureHeader.split(',').map(p => p.split('='))
);
const signed = `${t}.${payload}`;
const expected = crypto
.createHmac('sha256', secret)
.update(signed)
.digest('hex');
return crypto.timingSafeEqual(
Buffer.from(expected), Buffer.from(v1)
);
}Events
otp.sent— code was generated + deliveredotp.verified— user successfully verifiedotp.failed— verification failed (wrong code)otp.expired— 10-minute TTL elapsed without verification
| Scope | Limit | Window |
|---|---|---|
| Per email — /send | 3 | 1 minute |
| Per email — /send | 10 | 1 hour |
| Per IP — /send | 10 / 60 | 1 min / 1 hr |
| Per IP — /verify | 30 / 120 | 1 min / 1 hr |
Per-email limits apply to mg_live_ keys only; test keys skip them so CI can run fast. Per-IP limits apply to all keys.
Response headers
- All responses include
X-Request-Id(matches the body'srequest_id) andX-Api-Version: 1. - Successful (2xx) responses include
X-Quota-Remainingfor plan quota tracking. - Rate-limited responses (429): IP-level and email-level 429s include a
Retry-Afterheader (seconds); email-level 429s additionally includeX-RateLimit-Limit,X-RateLimit-Remaining, andX-RateLimit-Reset. - Plan-rate 429s (the per-minute plan rate limit, returned as
rate_limitedfrom the entitlement engine) includeX-RateLimit-ResetandX-Quota-Remaining— they do not includeRetry-After.
{
"error": {
"code": "rate_limited",
"message": "Too many OTP sends. Retry in 47s.",
"doc_url": "/docs#error-rate_limited"
},
"request_id": "a1b2c3d4-..."
}The doc_url field always points to a public anchor on this page — every code below has its own #error-<code> jump link.
validation_failedHTTP 400Validation FailedThe request body or parameters failed validation.
- Missing required field (email, code, password)
- Invalid email format
- OTP code is not exactly 6 digits
- Password is shorter than 8 characters
- Check the `message` field for the specific field that failed
- Ensure email is a valid RFC 5322 address
- OTP codes must be exactly 6 numeric digits
key_revokedHTTP 401API Key RevokedThe API key has been revoked and can no longer be used.
- An admin revoked the key
- The key was rotated and the old one revoked
- Generate a new API key
- Update your application's environment variables
key_expiredHTTP 401API Key ExpiredThe API key has passed its expiration date.
- The key was created with an expiry that has now passed
- Generate a new API key
- For long-lived keys, omit the expiration
insufficient_scopeHTTP 403Insufficient ScopeThe API key does not have permission for this action.
- A read-only key was used for a write operation
- The key's scopes don't include the required action
- Use a key with `full` scope or the specific required scope
- Update the key's scopes in the dashboard
rate_limitedHTTP 429Rate LimitedToo many requests in the time window. The rate_limited code covers three independent limiters: per-email (3/min, 10/hour), per-IP (send + verify), and the plan's per-minute API request rate. Not every 429 of this code includes a Retry-After header — see the fixes for the two header patterns.
- Exceeded 3 OTP sends per email per minute
- Exceeded 10 OTP sends per email per hour
- Exceeded an IP-level rate limit (send or verify)
- Exceeded the plan's per-minute API request rate (entitlement engine)
- IP/email 429s: wait for the Retry-After header (seconds) before retrying; email-level 429s additionally include X-RateLimit-Limit/Remaining/Reset
- Plan-rate 429s (no Retry-After): wait for X-RateLimit-Reset and check X-Quota-Remaining; implement exponential backoff
- Reduce request frequency or upgrade to a plan with a higher per-minute rate
lockedHTTP 423LockedToo many failed verification attempts.
- 5 incorrect OTP attempts on a single code
- 10 cumulative failed verifies (brute-force lockout)
- Wait 15 minutes for the per-code lockout to expire
- Wait 30 minutes for the account lockout to expire
- If the lock persists after the cooldown, contact support with the request ID
code_mismatchHTTP 400Code MismatchThe OTP code did not match the stored code.
- User typed the wrong code
- Code was for a different email or purpose
- Ask the user to re-enter the code
- Request a new code via the resend endpoint
expiredHTTP 410OTP ExpiredThe OTP code has expired (10-minute TTL).
- More than 10 minutes passed since the code was issued
- Request a new code via POST /api/v1/otp/resend
already_usedHTTP 409OTP Already UsedThis OTP code has already been consumed (single-use).
- The code was already verified successfully
- A concurrent request consumed it first
- Request a new code if you need to verify again
disposable_emailHTTP 422Disposable Email RejectedThe email domain is on the disposable-email blocklist.
- The domain (e.g. mailinator.com) is blocked
- Use a real email address
ip_blockedHTTP 403IP BlockedThe client IP has been temporarily suspended.
- Too many rate-limit violations from this IP
- Wait for the block to expire
- Contact support if you believe this is an error
not_foundHTTP 404Not FoundThe requested resource was not found.
- No active OTP found for this email
- Account does not exist
- Request a new OTP first
- Check the email address spelling
quota_exceededHTTP 402Monthly API Quota ExceededYour plan's monthly API_MESSAGES quota has been exhausted. This quota is consumed by every authenticated v1 API request — not just the OTP endpoints (broadcasts, suppressions, groups, events, deliveries, and all other v1 routes also consume it). It is separate from the per-email and per-IP rate limits.
- The API key owner's plan has used all of its monthly API_MESSAGES allotment
- Note: mg_test_ (sandbox) keys owned by a user ALSO consume this quota — sandbox mode skips real email delivery and the per-email rate limit, but not the plan quota
- Wait for the quota to reset on the next billing cycle
- Upgrade to a higher plan for a larger monthly API_MESSAGES quota
- Reduce request volume by batching or caching where possible
feature_not_availableHTTP 402Feature Not AvailableYour current plan does not include access to this feature.
- The API key owner's plan does not grant the required feature entitlement
- Upgrade to a plan that includes this feature
- Use a different API key associated with an eligible plan
internal_errorHTTP 500Internal Server ErrorAn unexpected error occurred.
- SMTP connection failure
- Database error
- Unexpected server bug
- Retry with exponential backoff
- Contact support with the request ID from the response body or the X-Request-Id header
- Initial public release.
- Endpoints:
/api/v1/otp/send,/api/v1/otp/verify,/api/v1/otp/resend. - API keys (mg_test_ / mg_live_) with full + read_only scopes.
- Webhooks with HMAC-SHA256 signed deliveries (
Nixify-Signature+Nixify-Eventheaders). - Sandbox mode is automatic for
mg_test_keys: OTPs are persisted but not emailed; the plaintext code is returned in the response. The optionalX-Sandbox-Simulateheader forces simulated errors (rate_limited, locked, expired, mismatch, smtp_error) for testing.