Skip to main content

Error Codes

All API errors return a consistent JSON format:

{
"error": {
"code": "error_code",
"message": "Human-readable description",
"details": {}
},
"meta": {
"request_id": "req_xyz789"
}
}

Use the request_id when contacting support.

Authentication Errors

CodeHTTP StatusDescriptionResolution
unauthorized401Missing or invalid API keyCheck your X-API-Key header
forbidden403API key lacks required permissionsUse a key with appropriate scopes
ip_not_allowed403Request from non-allowlisted IPAdd the IP to your key's allowlist in Settings
account_locked403Account is lockedContact support@zyphr.dev
account_suspended403Account suspended for policy violationContact support@zyphr.dev
account_inactive403The account is not activeContact support@zyphr.dev
invalid_token401The bearer/session token is invalidRe-authenticate to obtain a fresh token
token_expired401The token has expiredRefresh the token or re-authenticate
key_revoked401The API key has been revokedCreate a new API key in Settings → API Keys
email_not_verified403The account/user email must be verified for this operationVerify the email address, then retry
geo_restricted403The request originates from a restricted regionContact support if you believe this is in error

Validation Errors

CodeHTTP StatusDescriptionResolution
validation_error400Request body failed validationCheck details for specific field errors
invalid_url400Malformed URL providedEnsure valid URL format
invalid_body400Request body shape is invalidCheck the request schema
invalid_params400Invalid query or path parametersCheck the parameter values
invalid_provider400The specified provider is not a supported/valid provider for this operationUse a supported provider value
bad_request400The request is malformed or missing required contextCheck the request against the endpoint's documentation
wrong_endpoint400The credential/request was sent to the wrong endpoint (e.g. an end-user token on an account endpoint)Use the endpoint that matches your credential type

Rate Limiting

CodeHTTP StatusDescriptionResolution
rate_limit_exceeded429Too many API requestsBack off and retry after retry_after seconds
otp_send_rate_limit_exceeded429Too many OTP/MFA code requestsWait before requesting another code
otp_verify_rate_limit_exceeded429Too many OTP verification attemptsWait before retrying
sms_send_rate_limit_exceeded429Too many SMS sends for this number (direct SMS-send API)Wait before retrying
rate_limited429Too many login attempts for this identifierWait before retrying
quota_exceeded429Monthly send / MAU quota reachedUpgrade your plan or wait for the quota to reset
batch_exceeds_limit429The batch request exceeds the per-request recipient/item capSplit into smaller batches
daily_limit_exceeded429The channel's daily send limit for your plan is reached (push)Wait for the daily reset or upgrade your plan
sms_daily_limit_reached429The daily SMS send limit for your plan is reachedWait for the daily reset or upgrade your plan
sms_limit_reached429The SMS send limit for your plan is reachedUpgrade your plan or wait for the reset
session_limit_exceeded403The end user has reached the max concurrent sessions configured for the applicationRevoke an existing session or raise the app's session limit
free_account_limit_reached403The per-user free-account cap has been reachedUpgrade a plan, or use an existing account

Account-level send throttle/freeze (reputation) has its own codes — see Sending Status (Reputation) Errors below.

Rate limit headers are included in every response:

X-RateLimit-Limit: 100
X-RateLimit-Remaining: 95
X-RateLimit-Reset: 1705312800

Sending Status (Reputation) Errors

When your account's sending reputation degrades (high bounce or complaint rate, or a sudden volume spike), Zyphr throttles or freezes sending to protect the shared IP pool. These states apply only to send-capable endpoints — read, billing, and deliverability endpoints stay available so you can diagnose and recover.

CodeHTTP StatusDescriptionResolution
account_frozen403Sending is frozen for the account (reputation). The error message carries the specific reason.Stop sending to invalid addresses, clean your recipient list, then contact support@zyphr.dev to unfreeze.
account_throttled429A percentage of sends are rejected while reputation recovers. The error message carries the reason.Back off and retry; reduce bounce/complaint rates. Throttling lifts automatically once rates return to acceptable levels.

Every authenticated response includes the current sending state as headers, so you can surface it without a separate call:

X-Account-Sending-Status: frozen
X-Account-Sending-Status-Reason: Bounce rate (12.50%) exceeded freeze threshold (10%)

X-Account-Sending-Status is one of active, throttled, or frozen. X-Account-Sending-Status-Reason is present only when the status is not active. See API Limits for details.

Not Found Errors

CodeHTTP StatusDescriptionResolution
not_found404Requested resource doesn't existCheck the resource ID
user_not_found404User doesn't exist (e.g. on MFA endpoints; also returned as 401 if the user disappears mid-MFA-challenge)Verify the user ID
template_not_found404Template ID doesn't existVerify the template_id
subscriber_not_found404Subscriber doesn't existCreate the subscriber first
device_not_found404Device ID doesn't existCheck device registration
topic_not_found404Topic doesn't existCreate the topic first
account_not_found404Account doesn't existVerify the account context
organization_not_found404End-user organization doesn't existVerify the organization ID
environment_not_found404Environment (test/live) doesn't existVerify the environment

Webhook 404s return the generic not_found code (with a "Webhook not found" message), not a webhook_not_found code.

Conflict Errors

CodeHTTP StatusDescriptionResolution
conflict409Resource already existsUse update instead of create
domain_registered_elsewhere409Domain is claimed by another accountUse a different domain or contact support
already_verified400Domain/email is already verifiedNo action needed
duplicate_provider409An identity/OAuth provider of this type is already configured on the applicationUpdate the existing provider instead of adding a new one
duplicate_connection409The end user already has a connection for this providerNo action needed — the connection exists
subscription_exists400A subscription already exists for this accountManage the existing subscription instead
step_key_exists409A workflow step with this step_key already existsUse a unique step_key

Configuration Errors

CodeHTTP StatusDescriptionResolution
sms_not_configured400 / 503Twilio credentials not set up. 400 on the direct SMS-send API; 503 on the auth phone-OTP endpoints (see Auth OTP Errors).Configure Twilio in Settings > SMS
domain_not_verified400 / 422Sending domain not verified (status is route-dependent)Verify domain in Settings > Domains
no_verified_domain422No verified sending domain on the projectAdd and verify a domain in Settings > Domains
domain_blocked403The sending domain is blocklistedContact support
not_configured501The requested feature/provider is not configuredEnable/configure it in Settings

MFA & Security Errors

CodeHTTP StatusDescriptionResolution
mfa_requiredMFA verification needed. Not an error — the login/verify endpoints return this as a 200 response body flag ({ "data": { "mfa_required": true, "mfa_challenge": { … } } }), not an error status. Branch on the mfa_required field.Complete the MFA challenge with the returned mfa_challenge.token
otp_invalid400OTP code is incorrectEnter the correct code
otp_expired400OTP code has expiredRequest a new code
otp_max_attempts400Too many failed attempts on this code — it is now lockedRequest a new code
invalid_totp401TOTP code is incorrectEnter the current code from the authenticator app
invalid_backup_code401Backup code is invalid or already usedUse an unused backup code, or re-verify with TOTP
invalid_mfa401MFA challenge is invalid or expiredRestart the MFA challenge
invalid_verification401Verification code is incorrect (TOTP re-verification of an active session)Re-enter the current code
invalid_code400Verification code is incorrect (during MFA enrollment)Re-enter the code from the authenticator app
mfa_not_enabled400MFA is not enabled for this accountEnroll in MFA first
mfa_already_enabled409MFA is already enabledNo action needed
no_pending_enrollment400No MFA enrollment is in progress to verifyStart enrollment before verifying
email_required400The account must have an email address to enable MFAAdd an email to the account first
Branch on code, not message

Every error response includes a stable, machine-readable error.code alongside the human-readable error.message. Branch your control flow on error.code (and the HTTP status) — treat message as display-only text that may be reworded or localized at any time.

Auth OTP Errors (Email & Phone)

Returned by the email-OTP (/auth/email-otp/*) and phone-OTP (/auth/phone/*) send/verify endpoints.

CodeHTTP StatusDescriptionResolution
email_not_found400No account exists for this email (on login send/verify)Fall back to a registration flow, or prompt the user to sign up
phone_not_found400No account exists for this phone number (on login send/verify)Fall back to a registration flow
email_already_registered400Email is already registered (on registration send)Switch to the login flow
phone_already_registered400Phone number is already registered (on registration send)Switch to the login flow
otp_invalid400Verification code is incorrectRe-enter the code
otp_expired400No active code found, or it has expiredRequest a new code
otp_max_attempts400Code locked after too many failed attemptsRequest a new code
invalid_email400Email address is malformedCorrect the address
invalid_phone400Phone number is not valid E.164Use E.164 format (e.g. +14155551234)
user_inactive400The account is suspended or deletedContact support
email_not_configured503Email OTP requires a verified sending domain on the projectVerify a domain in Settings → Domains
sms_not_configured503Phone OTP requires configured SMS (Twilio) credentials. On the auth-OTP endpoints this is 503; the same code is returned as 400 by the direct SMS-send API (see Configuration Errors).Configure Twilio in Settings → SMS
sms_send_failed400The SMS provider failed to send the codeRetry; if persistent, check the SMS provider configuration
otp_send_rate_limit_exceeded429Too many code requests for this identifierWait retry_after seconds before requesting another code
otp_verify_rate_limit_exceeded429Too many verification attempts for this identifierWait retry_after seconds before retrying

Password Errors

CodeHTTP StatusDescriptionResolution
password_validation_error400Password doesn't meet the application's requirementsCheck details; see the password-requirements endpoint
invalid_password400Password is incorrect (e.g. wrong current password on change-password)Enter the correct password

Payment & Billing Errors

CodeHTTP StatusDescriptionResolution
plan_upgrade_required402 / 403Feature or limit requires a plan upgrade. Status is route-dependent — mostly 403 (plan-gated features: webhooks, WaaS, organizations, Muse); 402 on a few paywall paths (e.g. custom domains). Branch on the code, not the status.Upgrade your plan
subscriber_limit_exceeded403Subscriber cap for the current plan reachedUpgrade your plan
topic_limit_exceeded403Topic cap for the current plan reachedUpgrade your plan

Request Size

CodeHTTP StatusDescriptionResolution
payload_too_large413Request body exceeds the maximum allowed sizeSee API Limits. The details object includes limit_bytes and received_bytes

Gaming Identity Errors

Native gaming identity endpoints (POST /v1/auth/game-center, POST /v1/auth/google-play-games) return intentionally opaque errors on any verification failure — the specific control that failed (signature, certificate chain, timestamp, bundle id, token exchange) is never disclosed, to avoid giving an attacker a probing oracle.

CodeHTTP StatusDescriptionResolution
invalid_assertion401Apple Game Center assertion failed verification (any of: signature, certificate chain, timestamp freshness/replay, or bundle-id binding). Opaque by design.Most common cause: sending the wrong player id. Send GKLocalPlayer.teamPlayerID (form T:_…) as playerId — this is the id GameKit signs over. gamePlayerID (A:_…) and the deprecated playerID fail even on an otherwise-perfect assertion. Also confirm a fresh assertion (within the ±5-min window) and a bundle id in the app's trusted allow-list.
invalid_auth_code401Google Play Games server auth code failed verification (invalid/expired code, token exchange failure, or player lookup failure). Opaque by design.Send a fresh single-use serverAuthCode from requestServerSideAccess using the game's web/server client id.
provider_not_configured401The gaming provider isn't configured/enabled for this application.Enable and configure the provider under Applications → [App] → OAuth → Native gaming identity.
environment_required400The request lacks the environment context needed to resolve provider config.Ensure the application key resolves to a valid environment.

Resource Conflict Errors

CodeHTTP StatusDescriptionResolution
slug_already_exists409A resource with this slug already exists in the projectChoose a different slug
reserved_slug400The slug is reserved and can't be usedChoose a different slug
workflow_key_exists409A workflow with this key already existsUse a unique workflow key
topic_key_exists409A topic with this key already existsUse a unique topic key
feature_not_available403The feature isn't available on your current planUpgrade your plan
email_unverified403The account's email isn't verified yetVerify your account email before retrying

Operation Failures

CodeHTTP StatusDescriptionResolution
send_failed500Message failed to sendRetry the request
provider_error502External provider (SES, Twilio, APNs) errorRetry; check provider status if persistent
rendering_error400Template rendering failedCheck template variables match the data provided
recipient_suppressed400 / 422Recipient is on the suppression listRemove from the suppression list or skip

Workflow Errors

CodeHTTP StatusDescriptionResolution
invalid_status_transition400The requested workflow status change isn't allowed from the current statusCheck the current status; valid values are draft, active, paused, archived
workflow_not_active400The workflow can't be triggered because it isn't in the active statusActivate the workflow (PATCH /v1/workflows/{key}/status with {"status":"active"}) before triggering
step_keys_mismatch400The step_keys in a reorder request don't match the workflow's current stepsSend the exact set of existing step_keys in the desired order
execution_not_cancellable400The workflow execution is already in a terminal state and can't be cancelledNo action needed — the execution has finished

Internal Errors

CodeHTTP StatusDescriptionResolution
internal_error500Unexpected server errorRetry; contact support if persistent
service_unavailable503Service temporarily unavailableRetry with exponential backoff
timeout504Request timed outRetry the request

Handling Errors

Retry Strategy

For transient errors (429, 500, 502, 503, 504), implement exponential backoff:

# Example: retry with backoff
for i in 1 2 4 8 16; do
response=$(curl -s -w "\n%{http_code}" -X POST https://api.zyphr.dev/v1/emails \
-H "X-API-Key: zy_live_your_key" \
-H "Content-Type: application/json" \
-d '{"to": "user@example.com", "subject": "Hello", "html": "<p>Hi!</p>"}')

status=$(echo "$response" | tail -1)
if [ "$status" -lt 400 ]; then
echo "Success"
break
fi
echo "Retrying in ${i}s..."
sleep $i
done

Idempotency

Use the Idempotency-Key header to safely retry requests without creating duplicates:

curl -X POST https://api.zyphr.dev/v1/emails \
-H "X-API-Key: zy_live_your_key" \
-H "Idempotency-Key: unique-request-id-123" \
-H "Content-Type: application/json" \
-d '{"to": "user@example.com", "subject": "Hello", "html": "<p>Hi!</p>"}'