OTP
Send OTP
Send or resend an idempotent email code for an email and purpose.
Send code
cURLcurl --request POST \
--url https://api.sendotp.email/v1/send \
--header "Authorization: Bearer $SENDOTP_API_KEY" \
--header "Content-Type: application/json" \
--data '{"email":"customer@example.com","purpose":"report-access","language":"en"}'200
The code was accepted for delivery.{
"ok": true,
"id": "otp_01JZ4NQ8F2T7G2A9J6P0G5QX3K",
"resent": false,
"expiresAt": 1700000600
}Overview
Creates a live challenge when none exists, or emails the same live code again for the same normalized email and purpose. The default live window is 10 minutes.
Authorization
AuthorizationrequiredSend a server API key in the Authorization header. Browser-origin requests are rejected so keys stay on your backend.
Request
emailstringrequiredRecipient email address. It is trimmed, lowercased, and validated before lookup.
purposestringrequiredStable flow key such as login, report-access, signup, or account-delete.
languagestringOptional email template language. Supported: en (English), es (Spanish), fr (French), de (German), pt (Portuguese), it (Italian), and nl (Dutch). Defaults to en. Accepts base codes like es or locale tags like es-MX; unsupported languages fall back to en.
Responses
The code was accepted for delivery.
{
"ok": true,
"id": "otp_01JZ4NQ8F2T7G2A9J6P0G5QX3K",
"resent": false,
"expiresAt": 1700000600
}The same live challenge was re-sent too recently.
{
"error": "resend_cooldown",
"id": "otp_01JZ4NQ8F2T7G2A9J6P0G5QX3K",
"expiresAt": 1700000600,
"retryAfter": 30
}The email and purpose exceeded the account rate limit.
{
"error": "rate_limited"
}The email address uses a known temporary or disposable domain.
{
"error": "temporary_email_not_allowed",
"message": "Please use a permanent email address to continue."
}