Errors and troubleshooting
Look up V1.2 error codes by category, and the expiry limits that apply.
Error Reference
V1.2 errors use status, code, and message. Middleware-generated errors may also include retryable, retryAfter, cooldownSeconds, and details; the response fields depend on the endpoint and error path.
{
"status": "error",
"code": "ERROR_CODE",
"message": "Human-readable description",
"retryable": true,
"retryAfter": "2026-03-25T12:01:00.000Z",
"cooldownSeconds": 3600,
"details": {}
}
Expiration fields
| Limit | Value |
|---|---|
| Challenge expiry | Scales with difficulty — 90s at difficulty 2–3, 120s at 4, 240s at 5, 300s at 6. The default difficulty is 3, so most pipelines get 90 seconds. Read expiresAt from the challenge response; see Challenge expiry scales with difficulty. |
| Captcha token expiry | 2 minutes |
| Transaction expiry | 3 minutes. Use data.expiresAt from the send response when deciding whether to request a new OTP. |
V1.2 has no resend and no request signing
The core OTP flow is three endpoints — /challenge, /send and /verify. There is no resend endpoint: to give a user another code, run the flow again from /challenge. (V1.2 also mounts a separate passkey ceremony under /api/v1.2/transactions/passkey, gated independently of the OTP flow — see Passkeys.) V1.2 also has no request-signing scheme — your requests are authenticated by APIKey plus the PoW and Turnstile proofs, not by a signature you compute. (Request signing on outbound webhooks is separate and covered in Webhook Callbacks; the usk_ signing scheme belongs to Utilities, and widget-level signing to V2.)
Authentication Errors
| Status | Code | Cause and Solution |
|---|---|---|
| 400 | MISSING_REQUIRED_FIELDS | APIKey and pipelineID are required for the challenge request; transactionReqID and otp are required for verification. |
| 401 | INVALID_API_KEY | API key is invalid or does not match any account. Verify in your dashboard. |
| 403 | API_KEY_SCOPE_DENIED | The key is limited to some pipelines and this pipeline is not one of them. Use a key whose scope covers it. Returned by /challenge, /send and the passkey auth-options call. |
| 503 | API_KEY_INDEX_UNAVAILABLE | The key could not be checked right now. The response carries retryable: true: try again shortly. Returned by /challenge, /send and the passkey auth-options call. |
Pipeline and Configuration Errors
| Status | Code | Cause and Solution |
|---|---|---|
| 403 | PIPELINE_OWNERSHIP_MISMATCH | The pipeline belongs to a different account. Use a pipeline owned by the account identified by APIKey. |
| 403 | INVALID_API_VERSION | The pipeline is not configured for V1.2. |
| 403 | PIPELINE_INACTIVE | The pipeline is inactive. Activate it in the pipeline editor. |
| 403 | UTILITY_PIPELINE_ON_OTP_ENDPOINT | The selected pipeline is a Utilities pipeline. Use the Utilities API instead. |
| 404 | PIPELINE_NOT_FOUND | The pipelineID does not identify an existing pipeline. |
| 500 | PIPELINE_NOT_LOADED | The server could not load pipeline data for a security check. Retry the request. |
Channel Choice Errors
Returned only when you pass channel. The three validation codes are lower-case, unlike the rest
of this reference — that is what the API actually returns. Validation runs before the
availability check, so an unrecognised channel value is reported as invalid_channel even on a
pipeline where channel choice is switched off.
| Status | Code | Cause and Solution |
|---|---|---|
| 400 | invalid_channel | channel was not one of whatsapp, telegram, sms — or you passed it without a verificationAddress.phoneNumber. Sending channel: null counts as passing it: omit the field entirely to use the pipeline's normal routing. |
| 400 | channel_choice_not_enabled | Not available for this account and pipeline. Covers the account entitlement, the pipeline switch, the platform-wide switch, legacy (non-granular) pricing and accounts not billed per message. Check the pipeline switch in Dashboard > Pipelines first; if that is on, ask Akedly. |
| 400 | channel_not_eligible | A valid channel, but not one this pipeline sends on. The response carries eligibleChannels, in the pipeline's own configured order — offer the user those. The order reflects your pipeline configuration, not any ranking by us. |
| 422 | pre_send_failure | The channel was refused before anything was sent, so nothing was spent — today this means a telegram number that is not reachable on Telegram. Offer the user another channel. |
| 500 | ACTIVATION_FAILED | The send failed for any other reason. Because you named a channel explicitly, no fallback was attempted. Retry, or send again without channel to use the pipeline's normal routing. |
OTP Template Errors
Returned with HTTP 400 when the request or the pipeline default selects an OTP template that cannot be used. Nothing is sent or billed. Remedies are in the OTP templates guide.
| Status | Code | Cause |
|---|---|---|
| 400 | TEMPLATE_UNKNOWN | The template id is malformed, not found, or not owned by the account. |
| 400 | TEMPLATE_ARCHIVED | The template was archived. |
| 400 | TEMPLATE_WABA_REQUIRED | The account has no active own WABA, or its token cannot be read. |
| 400 | TEMPLATE_WABA_CHANGED | The template belongs to a previous WABA. |
| 400 | TEMPLATE_NOT_APPROVED | The OTP language has no approved variant. |
| 400 | TEMPLATE_REJECTED | Meta rejected the OTP language variant. |
| 400 | TEMPLATE_VARIABLE_MISSING | A required variable has no request value or stored default. |
| 400 | TEMPLATE_VARIABLE_INVALID | A variable name or value is invalid, or the template object contains an unsupported key. |
Challenge and Captcha Errors
| Status | Code | Cause and Solution |
|---|---|---|
| 400 | POW_SOLUTION_MISSING | The request omitted the PoW solution while PoW was required. Solve the current challenge and send its challengeToken and nonce. |
| 400 | POW_INVALID_TOKEN, POW_INVALID_SIGNATURE, POW_PIPELINE_MISMATCH, POW_INVALID_SOLUTION | The submitted PoW proof is invalid or bound to another pipeline. Fetch a new challenge and solve it for this pipeline. |
| 409 | POW_CHALLENGE_REPLAYED, POW_DIFFICULTY_STALE | The proof was already consumed or was minted below the current difficulty. Fetch a new challenge and retry. |
| 410 | POW_CHALLENGE_EXPIRED | The PoW challenge has expired. The TTL scales with difficulty (90s at the default difficulty 3, up to 300s at difficulty 6) — do not assume 5 minutes. Request a new challenge from Step 1. |
| 500 | POW_VERIFICATION_FAILED, POW_DIFFICULTY_CALCULATION_FAILED | The server could not complete a PoW operation. Retry the request. |
| 400 | TURNSTILE_TOKEN_MISSING | Turnstile is enabled but no token was provided. Obtain a token before sending. |
| 403 | TURNSTILE_VERIFICATION_FAILED | Cloudflare rejected the Turnstile token. Obtain a fresh token and retry. |
| 500 | TURNSTILE_SECRET_KEY_MISSING | The pipeline's Turnstile secret is not configured. Contact the pipeline owner. |
| 502 | TURNSTILE_API_ERROR | Turnstile's API returned an error. Retry the request. |
| 504 | TURNSTILE_VALIDATION_TIMEOUT | Turnstile validation timed out. Retry the request. |
Rate Limiting Errors
Each 429 response includes retryAfter (ISO timestamp) and cooldownSeconds (number) so you can back off precisely. V1.2 emits these explicit codes: RATE_LIMIT_PHONENUMBER_PERMINUTE, RATE_LIMIT_PHONENUMBER_PERHOUR, RATE_LIMIT_PHONENUMBER_PERDAY, RATE_LIMIT_ENDUSERIP_PERMINUTE, RATE_LIMIT_ENDUSERIP_PERHOUR, RATE_LIMIT_ENDUSERIP_PERDAY, RATE_LIMIT_PIPELINE_PERMINUTE, RATE_LIMIT_PIPELINE_PERHOUR, RATE_LIMIT_PIPELINE_PERDAY, and, on /send, IDENTITY_TEMPORARILY_BLOCKED.
| Status | Code | Cause and Solution |
|---|---|---|
| 429 | RATE_LIMIT_PHONENUMBER_PERMINUTE, RATE_LIMIT_PHONENUMBER_PERHOUR, RATE_LIMIT_PHONENUMBER_PERDAY | The same phone number has requested too many OTPs in this window. Wait for retryAfter before retrying. |
| 429 | RATE_LIMIT_ENDUSERIP_PERMINUTE, RATE_LIMIT_ENDUSERIP_PERHOUR, RATE_LIMIT_ENDUSERIP_PERDAY | Too many requests from the same end-user IP. Only fires when x-end-user-ip is provided or the connection originates from a public IP — private and loopback IPs are excluded from this check. |
| 429 | RATE_LIMIT_PIPELINE_PERMINUTE, RATE_LIMIT_PIPELINE_PERHOUR, RATE_LIMIT_PIPELINE_PERDAY | Pipeline-wide cap reached across all end users. Consider raising the pipeline rate limits in the dashboard. |
| 429 | IDENTITY_TEMPORARILY_BLOCKED | Returned by /send. The identity is under a zero-verify ban. retryable is false even though a schedule is supplied — the ban is terminal until it expires, so honour retryAfter (the ban expiry) and cooldownSeconds (the remaining wait) rather than branching on retryable. |
Circuit Breaker Errors
The circuit breaker is a pipeline-level flood guard: when a pipeline's traffic looks like an attack it trips, and every send is refused for a cooldown rather than burning your OTP budget. Its thresholds and cooldown are configured on the pipeline.
| Status | Code | Cause and Solution |
|---|---|---|
| 503 | CIRCUIT_BREAKER_OPEN | Pipeline circuit breaker is open due to flood detection. Wait for cooldownSeconds. |
| 503 | CIRCUIT_BREAKER_TRIGGERED | Pipeline suspended due to sustained high traffic. Automatic recovery after cooldown. |
Transaction and Verification Errors
| Status | Code | Cause and Solution |
|---|---|---|
| 400 | MISSING_REQUIRED_FIELDS | Required fields are missing from the request. For send, provide APIKey and pipelineID; for verify, provide transactionReqID and otp. |
| 400 | MISSING_VERIFICATION_ADDRESS | The send request did not include a verificationAddress with a phoneNumber or email. |
| 500 | TRANSACTION_NOT_FOUND | The transaction created for the send request could not be retrieved. Retry the request. |
| 410 | TRANSACTION_EXPIRED | Transaction expired. Use the expiresAt value returned by send and create a new transaction when it has passed. |
| 403 | INVALID_OTP | OTP entered by user does not match. Prompt them to re-enter. |
| 409 | ALREADY_VERIFIED | The transaction request was already verified. Do not submit the same request again. |
| 429 | MAX_ATTEMPTS_EXCEEDED | 10th wrong OTP on this transaction. The transaction is force-expired — create a new one; further attempts return TRANSACTION_EXPIRED. |
| 500 | SEND_OTP_FAILED | The V1.2 send operation failed outside the handled transaction paths. Retry the request. |
| Propagated status (default 500) | ACTIVATION_FAILED | The transaction was created but could not be activated. Retry according to the returned status. |
| 402 | BILLING_FAILED | Billing failed while verifying the OTP. Resolve the billing issue before retrying. |
| 500 | VERIFY_OTP_FAILED | The verify operation failed outside the handled verification outcomes. Retry the request. |
| Propagated from transaction creation (default 400) | TRANSACTION_CREATION_FAILED | The underlying transaction could not be created. The response preserves its status and, when the creator supplies one, its own code, so read code before this one. For example DESTINATION_COUNTRY_BLOCKED (403) when the destination is not allowed or SYSTEM_MANAGED_PIPELINE_FORBIDDEN (404) for a pipeline reserved for platform operations. |
Challenge-generation errors
The challenge endpoint returns 500 for an unhandled challenge-generation failure. The PoW service
may identify the failure as POW_CHALLENGE_GENERATION_FAILED; the controller fallback is
CHALLENGE_GENERATION_FAILED.
| Status | Code | Cause and Solution |
|---|---|---|
| 500 | CHALLENGE_GENERATION_FAILED, POW_CHALLENGE_GENERATION_FAILED | The server could not create a PoW challenge. Retry the challenge request. |
Validation Errors
| Status | Code | Cause and Solution |
|---|---|---|
| 400 | INVALID_OTP_LANGUAGE | Invalid otpLanguage; see accepted values. Before retrying, fetch and solve a new challenge when proof-of-work is enabled and obtain a fresh Turnstile token when required. |
| 400 | VALIDATION_ERROR | Request body failed validation. Check the details field for specific issues. |
| 413 | PAYLOAD_TOO_LARGE | Request body exceeds size limit. |
Server Errors
| Status | Code | Cause and Solution |
|---|---|---|
| 500 | INTERNAL_SERVER_ERROR | Unexpected server error. Retry the request. If persistent, contact support at support@akedly.io. |
