The account is not active. On end-user sign-in (passkey, OAuth, magic link, gaming, anonymous, MFA completion, password change, account conversion), the end user is suspended, usually by your application
If you suspended them, re-activate the end user (PATCH /v1/auth/users/:id with status: "active"). If you didn't, or for your own Zyphr account, contact support@zyphr.dev
invalid_token
401
The bearer/session token is invalid
Re-authenticate to obtain a fresh token
token_expired
401
The token has expired
Refresh the token or re-authenticate
key_revoked
401
The API key has been revoked
Create a new API key in Settings → API Keys
email_not_verified
403
The account/user email must be verified for this operation
Too many SMS sends for this number (direct SMS-send API)
Wait before retrying
rate_limited
429
Too many login attempts for this identifier
Wait before retrying
quota_exceeded
429
Monthly send / MAU quota reached
Upgrade your plan or wait for the quota to reset
usage_limit_exceeded
429
The monthly Webhooks-as-a-Service event limit for your plan is reached (POST /v1/waas/applications/{appId}/events). A batch publish still returns 201; each event over the limit is listed in results with code: "usage_limit_exceeded"
Upgrade your plan or wait for the monthly reset
mau_quota_exceeded
402
An end-user sign-in would add a new monthly active user to an account already at its MAU limit. Users already counted this month can still sign in. The body includes limit and current
Upgrade your plan, or wait for the monthly reset
batch_exceeds_limit
429
The batch request exceeds the per-request recipient/item cap
Split into smaller batches
daily_limit_exceeded
429
The channel's daily send limit for your plan is reached (push)
Wait for the daily reset or upgrade your plan
sms_daily_limit_reached
429
The daily SMS send limit for your plan is reached
Wait for the daily reset or upgrade your plan
sms_limit_reached
429
The SMS send limit for your plan is reached
Upgrade your plan or wait for the reset
session_limit_exceeded
403
The end user has reached the application's max_concurrent_sessions and the application refuses new sessions (deny_new). Returned by sign-ins that open a session for an existing user (password, phone/email OTP, passkey, OAuth, magic link, gaming, MFA). Anonymous sign-in, registration and account conversion don't apply the limit. For MFA logins it is normally returned before a challenge is issued, so no TOTP or backup code is spent. It can still be returned at MFA completion if the user reached the limit after the challenge was issued. The body includes max_sessions and current_sessions
Revoke an existing session, raise the limit, or switch the app to revoke_oldest
test_environment_limit
403
Creating this end user would exceed the test environment's user limit
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.
Code
HTTP Status
Description
Resolution
account_frozen
403
Sending 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_throttled
429
A 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 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.
MFA 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_invalid
400
OTP code is incorrect
Enter the correct code
otp_expired
400
OTP code has expired
Request a new code
otp_max_attempts
400
Too many failed attempts on this code — it is now locked
Request a new code
invalid_totp
401
TOTP code is incorrect
Enter the current code from the authenticator app
invalid_backup_code
401
Backup code is invalid or already used
Use an unused backup code, or re-verify with TOTP
invalid_mfa
401
MFA challenge is invalid or expired
Restart the MFA challenge
invalid_verification
401
Verification code is incorrect (TOTP re-verification of an active session)
Re-enter the current code
invalid_code
400
Verification code is incorrect (during MFA enrollment)
Re-enter the code from the authenticator app
mfa_not_enabled
400
MFA is not enabled for this account
Enroll in MFA first
mfa_already_enabled
409
MFA is already enabled
No action needed
no_pending_enrollment
400
No MFA enrollment is in progress to verify
Start enrollment before verifying
email_required
400
The account must have an email address to enable MFA
Add 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.
Returned by the email-OTP (/auth/email-otp/*) and phone-OTP (/auth/phone/*) send/verify endpoints.
Code
HTTP Status
Description
Resolution
email_not_found
400
No account exists for this email (on login send/verify)
Fall back to a registration flow, or prompt the user to sign up
phone_not_found
400
No account exists for this phone number (on login send/verify)
Fall back to a registration flow
email_already_registered
400
Email is already registered (on registration send)
Switch to the login flow
phone_already_registered
400
Phone number is already registered (on registration send)
Switch to the login flow
otp_invalid
400
Verification code is incorrect
Re-enter the code
otp_expired
400
No active code found, or it has expired
Request a new code
otp_max_attempts
400
Code locked after too many failed attempts
Request a new code
invalid_email
400
Email address is malformed
Correct the address
invalid_phone
400
Phone number is not valid E.164
Use E.164 format (e.g. +14155551234)
user_inactive
400
The end user is suspended or deleted. Returned both when an OTP is requested and when it is verified
Re-activate the end user in your application, or create a new one
email_not_configured
503
Email OTP requires a verified sending domain on the project
Verify a domain in Settings → Domains
sms_not_configured
503
Phone 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_failed
400
The SMS provider failed to send the code
Retry; if persistent, check the SMS provider configuration
otp_send_rate_limit_exceeded
429
Too many code requests for this identifier
Wait retry_after seconds before requesting another code
otp_verify_rate_limit_exceeded
429
Too many verification attempts for this identifier
Feature 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.
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.
Code
HTTP Status
Description
Resolution
invalid_assertion
401
Apple 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_code
401
Google 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_configured
401
The gaming provider isn't configured/enabled for this application.
Enable and configure the provider under Applications → [App] → OAuth → Native gaming identity.
environment_required
400
The request lacks the environment context needed to resolve provider config.
Ensure the application key resolves to a valid environment.