Skip to content
Akedly
V2.0 Widgets

Errors and troubleshooting

Look up widget error codes and dispatch responses, use your own OTP, and fix common problems.

Error Reference

OTP dispatch responses

These dispatch responses apply to request-otp and resend-otp; OTP_NOT_REQUESTED also applies to verify-otp. The two 409 codes are returned when a send for that attempt is already in flight, or when the outcome of an earlier send for it is unknown. For resend-otp, the one resend is claimed just before dispatch, after the endpoint's own checks: your one resend is already consumed when a dispatch conflict is returned, so a retry returns 429 RESEND_LIMIT_EXCEEDED. Earlier endpoint checks can also return a different error. Dev-mode test pairs never return these 409s; they send no real code.

StatusErrorCause & Solution
409OTP_DISPATCH_IN_PROGRESSAnother call holds this send: it is still in flight, or it finished but its result is not readable yet. For request-otp, retryable is true: read retryAfterSeconds (3) from the body (the response also sets Retry-After: 3, but browsers may not expose that header cross-origin) and retry the same call after that many seconds; expect 409 while in flight; the original request once the first send was accepted. If that result is not readable yet, the response remains 409. For resend-otp, retryable is false and no retry delay is sent: its one resend is already consumed, so a retry returns 429 RESEND_LIMIT_EXCEEDED.
409OTP_DISPATCH_OUTCOME_UNKNOWNThe first send's outcome could not be confirmed. retryable is false; do not retry the same verification attempt. Start a new verification attempt. The code may have been delivered. For request-otp only, an optional redirectUrl points to the pipeline's frontendCallbackURL with status=failed, error=OTP_DISPATCH_OUTCOME_UNKNOWN, transactionId, attemptId, timestamp, and meta_<key> for each public metadata entry. No field is returned without a callback URL; older origins also omit it during rolling deploys, so treat it as optional. The hosted widget shows a return link to this URL on the terminal error and follows it automatically, except for a native app-scheme URL inside an iframe, which is shown as a link only. This is a user-experience hint; this path sends no failed webhook. resend-otp does not carry redirectUrl.
500OTP_SEND_FAILEDThe original request-otp caller's provider timeout remains a generic send failure, with retryable: true. It is not the unknown-outcome 409; a later call can return OTP_DISPATCH_OUTCOME_UNKNOWN.
429RESEND_LIMIT_EXCEEDEDYour one resend is reserved before dispatch and stays consumed once a send was attempted or its outcome is unknown. Do not retry resend-otp automatically, including after a 500 OTP_RESEND_FAILED timeout or a 409 dispatch conflict: a retry returns 429 RESEND_LIMIT_EXCEEDED. A 500 body with OTP_RESEND_FAILED or RESEND_OTP_FAILED says retryable: true; for resend-otp ignore it and do not retry automatically: the one resend is normally already spent, although a failure that sent nothing may release it. Treat this response as final for that attempt and start a new verification attempt.
403OTP_NOT_REQUESTEDVerification requires a prior successful OTP request. Request an OTP before calling verify; resend-otp also returns this code when no OTP was requested.

OTP dispatch responses

JSON409 Status
{
  "status": "error",
  "code": "OTP_DISPATCH_IN_PROGRESS",
  "message": "Your verification code is still being sent. Please wait a few seconds and try again.",
  "retryable": true,
  "retryAfterSeconds": 3,
  "reason": "dispatch_in_progress"
}

V2 Widget errors reach you by four different routes, and the route decides the shape of the response body — so check which table a code is in before writing a handler for it. Shared-handler responses carry retryable and action. Endpoint-handler responses list their endpoint-specific body fields. Middleware responses carry the fields shown in their row. Dual-path codes arrive both ways depending on the endpoint.

Shared error-handler responses

Returned by the passkey SDK endpoints and by middleware that hands the error to the shared handler. These bodies carry retryable and action alongside the code.

On create-attempt, authenticateCreateAttempt runs before the controller. Its MISSING_REQUIRED_FIELDS, INVALID_SIGNATURE, SIGNATURE_EXPIRED responses use status, code, message, retryable, action.

CodeHTTPRetryableAction
INVALID_SIGNATURE401falseCheck signature generation logic
SIGNATURE_EXPIRED401trueGenerate new signature with current timestamp
ATTEMPT_NOT_FOUND404falseCreate a new attempt
ATTEMPT_EXPIRED410falseCreate a new attempt
CHALLENGE_EXPIRED410trueRequest a new device challenge
CAPTCHA_NOT_VERIFIED403trueComplete captcha verification first
WIDGET_NOT_FOUND404falseVerify widget exists
MISSING_REQUIRED_FIELDS400falseCheck request parameters
PASSKEY_DISABLED403falsePasskeys are not enabled for this account/pipeline; fall back to OTP
PASSKEY_NOT_VERIFIED403falseComplete OTP verification before enrolling a passkey
PASSKEY_AUTH_IN_PROGRESS409trueFinish the current passkey prompt or retry shortly. Raised by auth-options and auth-abandon.
PASSKEY_REGISTRATION_FAILED400trueRetry passkey enrollment
PASSKEY_AUTH_FAILED403trueRetry, or use a one-time code instead
NO_PASSKEY404falseNo passkey on this device; use a one-time code
TEST_PAIR_RETIRED403falseDev-mode test pair changed; re-open dev mode or use a real one-time code
INSUFFICIENT_QUOTA402falseTop up quota or upgrade the plan
BILLING_FAILED500trueRetry, or use a one-time code instead

Error-handler fallbacks

CodeHTTPRetryableAction
VALIDATION_ERROR400falseCheck request parameters
INVALID_ID_FORMAT400falseProvide valid ID format
INTERNAL_SERVER_ERROR500falseContact support if issue persists

Endpoint-handler responses

The widget endpoints choose the status in the handler itself. These rows list the fields emitted by each endpoint. Some handlers add retryable or conditional redirect/quota fields.

The request-otp, verify-otp and resend-otp responses are answered to the hosted widget, which handles most of them itself, such as the duplicate-send and channel-choice rows. They are listed here so the contract is complete.

CodeHTTPBody fieldsMeaning
INVALID_OTP_LANGUAGE400status, code, messagecreate-attempt: otpLanguage must be en or ar in any case, optionally followed by - or _ and two letters or three digits, without trimming; null or omission uses the pipeline default.
INVALID_API_KEY401status, code, message; conditional details, suspendedUntil, retryable, requiredQuota, remainingQuotacreate-attempt: The API key is invalid.
TEMPLATE_UNKNOWN400status, code, message; conditional details, suspendedUntil, retryable, requiredQuota, remainingQuotacreate-attempt: The OTP template id is malformed, missing, or not owned by the account.
TEMPLATE_ARCHIVED400status, code, message; conditional details, suspendedUntil, retryable, requiredQuota, remainingQuotacreate-attempt: The OTP template is archived.
TEMPLATE_WABA_REQUIRED400status, code, message; conditional details, suspendedUntil, retryable, requiredQuota, remainingQuotacreate-attempt: An active own WhatsApp Business Account or its readable token is required.
TEMPLATE_WABA_CHANGED400status, code, message; conditional details, suspendedUntil, retryable, requiredQuota, remainingQuotacreate-attempt: The OTP template belongs to a previous WhatsApp Business Account.
TEMPLATE_NOT_APPROVED400status, code, message; conditional details, suspendedUntil, retryable, requiredQuota, remainingQuotacreate-attempt: The OTP template is not approved for the OTP language.
TEMPLATE_REJECTED400status, code, message; conditional details, suspendedUntil, retryable, requiredQuota, remainingQuotacreate-attempt: Meta rejected the OTP template for the OTP language.
TEMPLATE_VARIABLE_MISSING400status, code, message; conditional details, suspendedUntil, retryable, requiredQuota, remainingQuotacreate-attempt: A required OTP template variable has no request value or stored default.
TEMPLATE_VARIABLE_INVALID400status, code, message; conditional details, suspendedUntil, retryable, requiredQuota, remainingQuotacreate-attempt: An OTP template variable or selection key is invalid; inspect details when present.
API_KEY_SCOPE_DENIED403status, code, message; conditional details, suspendedUntil, retryable, requiredQuota, remainingQuotacreate-attempt: The API key is limited to some pipelines and this widget’s pipeline is not one of them.
WIDGET_USER_MISMATCH403status, code, message; conditional details, suspendedUntil, retryable, requiredQuota, remainingQuotacreate-attempt: The widget does not belong to the API-key owner.
PIPELINE_OWNERSHIP_MISMATCH403status, code, message; conditional details, suspendedUntil, retryable, requiredQuota, remainingQuotacreate-attempt: The widget’s pipeline does not belong to the API-key owner.
WIDGET_INACTIVE400status, code, message; conditional details, suspendedUntil, retryable, requiredQuota, remainingQuotacreate-attempt: The widget is inactive.
PIPELINE_NOT_CONFIGURED400status, code, message; conditional details, suspendedUntil, retryable, requiredQuota, remainingQuotacreate-attempt: The widget has no pipeline configured.
PIPELINE_NOT_FOUND404status, code, message; conditional details, suspendedUntil, retryable, requiredQuota, remainingQuotacreate-attempt: The configured pipeline could not be found.
CIRCUIT_BREAKER_OPEN503status, code, message; conditional details, suspendedUntil, retryable, requiredQuota, remainingQuotacreate-attempt: The widget is temporarily suspended by its circuit breaker.
INSUFFICIENT_QUOTA402status, code, message; conditional details, suspendedUntil, retryable, requiredQuota, remainingQuotacreate-attempt: The account does not have enough quota for the widget attempt.
METADATA_SIZE_EXCEEDED400status, code, message; conditional details, suspendedUntil, retryable, requiredQuota, remainingQuotacreate-attempt: Combined public and private metadata exceeds 10 KB.
CUSTOM_HEADERS_SIZE_EXCEEDED400status, code, message; conditional details, suspendedUntil, retryable, requiredQuota, remainingQuotacreate-attempt: Custom headers exceed 4 KB.
NATIONAL_PIPELINE_RESTRICTION403status, code, message; conditional details, suspendedUntil, retryable, requiredQuota, remainingQuotacreate-attempt: The destination is restricted by the pipeline’s national policy.
DESTINATION_COUNTRY_BLOCKED403status, code, message; conditional details, suspendedUntil, retryable, requiredQuota, remainingQuotacreate-attempt: The destination country is blocked by pipeline policy.
INVALID_OTP_DIGITS400status, code, message; conditional details, suspendedUntil, retryable, requiredQuota, remainingQuotacreate-attempt: digits must be 4, 5, or 6.
OTP_DIGITS_MISMATCH400status, code, message; conditional details, suspendedUntil, retryable, requiredQuota, remainingQuotacreate-attempt: The custom OTP length does not match digits.
INVALID_TEST_PAIR400status, code, message; conditional details, suspendedUntil, retryable, requiredQuota, remainingQuotacreate-attempt: The stored dev-mode test pair has an unsupported OTP length.
CREATE_ATTEMPT_FAILED400status, code, message; conditional details, suspendedUntil, retryable, requiredQuota, remainingQuotacreate-attempt: The widget attempt could not be created.
API_KEY_INDEX_UNAVAILABLE503status, code, message, retryablecreate-attempt: The API key could not be checked right now. retryable is true: try again shortly.
MISSING_REQUIRED_FIELDS400status, code, messageregister-device: The attempt ID and fingerprint are required.
ATTEMPT_NOT_FOUND404status, code, messageregister-device: The attempt could not be found.
ATTEMPT_EXPIRED410status, code, messageregister-device: The attempt has expired.
FINGERPRINT_INCONSISTENT400status, code, messageregister-device: The fingerprint is missing required fields or contains invalid screen data.
DEVICE_REGISTRATION_FAILED500status, code, messageregister-device: The device could not be registered.
MISSING_REQUIRED_FIELDS400status, code, messagevalidate-attempt: The attempt ID, stable device ID, and fingerprint are required.
ATTEMPT_NOT_FOUND404status, code, messagevalidate-attempt: The attempt could not be found.
ATTEMPT_EXPIRED410status, code, messagevalidate-attempt: The attempt has expired.
INVALID_DEVICE_ID400 or 403status, code, messagevalidate-attempt: 400 when deviceId or fingerprint is absent from the request; 403 when the device ID does not match the fingerprint — either the device was never registered, or the recomputed ID differs.
FINGERPRINT_INCONSISTENT400status, code, messagevalidate-attempt: The submitted fingerprint is missing required fields or contains invalid screen data. Despite the name, this is a malformed-payload error, not a spoofing rejection.
VALIDATE_ATTEMPT_FAILED400status, code, messagevalidate-attempt: The attempt could not be validated.
MISSING_REQUIRED_FIELDS400status, code, message, retryableverify-captcha: The attempt ID, device ID, and captcha token are required.
ATTEMPT_NOT_FOUND404status, code, message, retryableverify-captcha: The attempt could not be found.
ATTEMPT_EXPIRED410status, code, message, retryableverify-captcha: The attempt has expired.
POW_SOLUTION_MISSING400status, code, message, retryableverify-captcha: A proof-of-work solution is required.
POW_INVALID_TOKEN400status, code, message, retryableverify-captcha: The proof-of-work token is invalid.
POW_INVALID_SIGNATURE400status, code, message, retryableverify-captcha: The proof-of-work signature is invalid.
POW_CHALLENGE_EXPIRED410status, code, message, retryableverify-captcha: The proof-of-work challenge has expired.
POW_ATTEMPT_MISMATCH400status, code, message, retryableverify-captcha: The proof-of-work challenge belongs to another attempt.
POW_CHALLENGE_REPLAYED409status, code, message, retryableverify-captcha: The proof-of-work challenge was already used.
POW_INVALID_SOLUTION400status, code, message, retryableverify-captcha: The proof-of-work solution is invalid.
POW_VERIFICATION_FAILED500status, code, message, retryableverify-captcha: Proof-of-work verification failed unexpectedly.
CAPTCHA_ALREADY_USED409status, code, message, retryableverify-captcha: The captcha token was already consumed.
CAPTCHA_VALIDATION_FAILED400status, code, message, retryableverify-captcha: Cloudflare rejected the submitted Turnstile token.
CAPTCHA_API_ERRORCloudflare’s own statusstatus, code, message, retryableverify-captcha: Cloudflare answered the Turnstile check with an HTTP error. The response carries that same HTTP status, so it is not a fixed 502. retryable is true.
CAPTCHA_VALIDATION_TIMEOUT400status, code, message, retryableverify-captcha: Captcha validation timed out. retryable is true.
CAPTCHA_TOKEN_REUSED400status, code, message, retryableverify-captcha: The same captcha token was submitted twice concurrently.
CAPTCHA_NETWORK_ERROR400status, code, message, retryableverify-captcha: Captcha validation could not reach the provider.
CAPTCHA_VERIFICATION_FAILED400status, code, message, retryableverify-captcha: Captcha verification failed unexpectedly.
MISSING_REQUIRED_FIELDS500status, code, message, retryablerequest-otp: The attempt ID or device ID is missing.
ATTEMPT_NOT_FOUND404status, code, message, retryablerequest-otp: The attempt could not be found.
ATTEMPT_EXPIRED410status, code, message, retryablerequest-otp: The attempt has expired.
CAPTCHA_NOT_VERIFIED403status, code, message, retryablerequest-otp: Captcha verification must be completed first.
TEST_PAIR_RETIRED403status, code, message, retryablerequest-otp: The dev-mode test pair is no longer active.
DESTINATION_COUNTRY_BLOCKED403status, code, message, retryablerequest-otp: The destination country is blocked by pipeline policy.
NATIONAL_PIPELINE_RESTRICTION403status, code, message, retryablerequest-otp: The destination is restricted by the pipeline’s national policy.
TRANSACTION_NOT_FOUND500status, code, message, retryablerequest-otp: The transaction could not be found while requesting an OTP.
TRANSACTION_CREATION_FAILED500status, code, message, retryablerequest-otp: The underlying transaction could not be created.
API_KEY_SCOPE_DENIED403status, code, message, retryablerequest-otp: The API key is limited to some pipelines and this widget’s pipeline is not one of them.
OTP_TEMPLATE_SELECTION_UNAVAILABLE500status, code, message, retryablerequest-otp: The OTP template chosen for this transaction could not be loaded before sending. Nothing was sent. retryable is true.
OTP_SEND_FAILED500status, code, message, retryablerequest-otp: OTP delivery failed while requesting an OTP.
REQUEST_OTP_FAILED500status, code, message, retryablerequest-otp: The OTP request failed outside the named terminal cases.
CHANNEL_NOT_OFFERED400status, code, message, retryable; conditional channelChoice (the channels still on offer)request-otp · resend-otp (calls the hosted widget makes): The channel sent is not on this attempt’s offer list, or it was already refused. channelChoice lists the channels still on offer. retryable is false.
CHANNEL_PROBE_UNAVAILABLE503status, code, message, retryable; conditional channelChoice (the channels still on offer)request-otp · resend-otp (calls the hosted widget makes): Telegram was chosen and its reachability check could not answer. Nothing was sent. retryable is false.
CHANNEL_PRICING_UNAVAILABLE503status, code, message, retryable; conditional channelChoice (the channels still on offer)request-otp · resend-otp (calls the hosted widget makes): The chosen channel could not be priced for the quota check. retryable is false.
CHANNEL_QUOTA_EXCEEDED402status, code, message, retryable; conditional channelChoice (the channels still on offer)request-otp · resend-otp (calls the hosted widget makes): The account does not have enough quota to send on the chosen channel. retryable is false.
PRE_SEND_FAILURE422status, code, message, retryable; conditional channelChoice (the channels still on offer)request-otp · resend-otp (calls the hosted widget makes): The chosen channel was refused before anything was sent, so nothing was spent. retryable is false.
OTP_DISPATCH_IN_PROGRESS409status, code, message, retryable, reason; conditional retryAfterSeconds (request-otp in-progress only), certainty (unknown outcome)request-otp: Another call holds this send: it is still in flight, or it finished but its result is not readable yet. Retry after retryAfterSeconds (3): 409 while in flight; the original request once the first send was accepted. If that result is not readable yet, the response remains 409.
OTP_DISPATCH_OUTCOME_UNKNOWN409status, code, message, retryable, reason, certainty; conditional redirectUrl (when the pipeline has a frontendCallbackURL)request-otp: The first send’s outcome could not be confirmed. Not retryable; start a new verification attempt.
MISSING_REQUIRED_FIELDS400status, code, message, retryable; conditional redirectUrlverify-otp: Required verification fields are missing.
ATTEMPT_NOT_FOUND404status, code, message, retryable; conditional redirectUrlverify-otp: The attempt could not be found.
OTP_NOT_REQUESTED403status, code, message, retryable; conditional redirectUrlverify-otp: An OTP must be requested before it can be verified.
TRANSACTION_NOT_FOUND404status, code, message, retryable; conditional redirectUrlverify-otp: The transaction could not be found.
TRANSACTION_EXPIRED410status, code, message, retryable; conditional redirectUrlverify-otp: The transaction has expired.
INVALID_OTP403status, code, message, retryable; conditional redirectUrlverify-otp: The submitted OTP is invalid.
MAX_ATTEMPTS_EXCEEDED429status, code, message, retryable; conditional redirectUrlverify-otp: The transaction reached its failed-OTP limit; start a new attempt.
TEST_PAIR_RETIRED403status, code, message, retryable; conditional redirectUrlverify-otp: The dev-mode test pair is no longer active.
BILLING_FAILED400status, code, message, retryable; conditional redirectUrlverify-otp: Billing failed during widget OTP verification.
VERIFY_FAILED400status, code, message, retryable; conditional redirectUrlverify-otp: The shared verification service returned an unhandled failure.
VERIFY_OTP_FAILED400status, code, message, retryable; conditional redirectUrlverify-otp: OTP verification failed unexpectedly.
MISSING_REQUIRED_FIELDS500status, code, message, retryableresend-otp: Required resend fields are missing.
ATTEMPT_NOT_FOUND404status, code, message, retryableresend-otp: The attempt could not be found.
OTP_NOT_REQUESTED403status, code, message, retryableresend-otp: An OTP must be requested before it can be resent.
RESEND_LIMIT_EXCEEDED429status, code, message, retryableresend-otp: The attempt has reached its resend limit.
TRANSACTION_NOT_FOUND404status, code, message, retryableresend-otp: The transaction could not be found.
TRANSACTION_EXPIRED410status, code, message, retryableresend-otp: The transaction has expired.
TEST_PAIR_RETIRED403status, code, message, retryableresend-otp: The dev-mode test pair is no longer active.
OTP_RESEND_FAILED500status, code, message, retryableresend-otp: The OTP resend failed outside the named terminal cases.
OTP_TEMPLATE_SELECTION_UNAVAILABLE500status, code, message, retryableresend-otp: The OTP template chosen for this transaction could not be loaded before sending. Nothing was sent. retryable is true.
RESEND_OTP_FAILED500status, code, message, retryableresend-otp: The resend operation failed unexpectedly.
OTP_DISPATCH_IN_PROGRESS409status, code, message, retryable, reason; conditional retryAfterSeconds (request-otp in-progress only), certainty (unknown outcome)resend-otp: Another call holds this send: it is still in flight, or it finished but its result is not readable yet. Not retryable; the one resend is spent. A retry returns 429 RESEND_LIMIT_EXCEEDED after earlier endpoint checks.
OTP_DISPATCH_OUTCOME_UNKNOWN409status, code, message, retryable, reason; conditional retryAfterSeconds (request-otp in-progress only), certainty (unknown outcome)resend-otp: The first send’s outcome could not be confirmed. Not retryable; start a new verification attempt.
MISSING_PUBLIC_KEY400status, code, messagewidget-branding: The widget branding request did not include a public key.
WIDGET_NOT_FOUND404status, code, messagewidget-branding: The active widget could not be found for the public key.

Direct error responses (rate limits, circuit breaker, bans)

Rate-limit and circuit-breaker rows are answered by middleware, before the endpoint handler runs at all — so they pre-empt that endpoint's own rows. The zero-verify ban is different: IDENTITY_TEMPORARILY_BLOCKED is returned by the request-otp and resend-otp handlers themselves, after most of their other checks — so an earlier error always wins over the ban. The exact order is below.

request-otp checks, in order: required fields, attempt lookup, attempt expiry, the stored channel (CHANNEL_NOT_OFFERED, only when a channel is sent), the live test-pair/widget load, the idempotent already-requested return, captcha state, the account lookup, then the ban. After the ban come the channel quota check (only when a channel is sent, and not for test pairs), and then every transaction and activation error. The idempotent step is the one worth planning for: a repeat of a request that already succeeded returns that success, so a caller retrying after a dropped response gets the original result rather than CAPTCHA_NOT_VERIFIED.

resend-otp has no captcha step. It checks required fields, attempt lookup, whether an OTP was requested at all, the live test pair, the stored channel, the transaction lookup and its ownership, transaction expiry, the ban, channel quota (when a channel is chosen, except for test pairs), then the resend claim just before dispatch.

The rows list the fields available on each response. Where present, retryAfter is an ISO timestamp and cooldownSeconds the equivalent in seconds. Wait for that schedule rather than retrying on a fixed interval.

CodeHTTPExtra body fieldsMeaning
WIDGET_IDENTITY_INDEX_UNAVAILABLE503retryableevery widget and widget-passkey route except telemetry: Widget identity operations are temporarily unavailable. retryable is true: try again shortly.
RATE_LIMIT_WIDGET_ATTEMPTS429retryable, retryAfter, cooldownSeconds, detailscreate-attempt · validate-attempt: Too many attempts for this widget — the whole widget is over budget, not one end user.
RATE_LIMIT_PHONENUMBER_ATTEMPTS429retryable, retryAfter, cooldownSeconds, detailscreate-attempt · validate-attempt: Too many attempts for this phone number.
RATE_LIMIT_DEVICEID_ATTEMPTS429retryable, retryAfter, cooldownSeconds, detailscreate-attempt · validate-attempt: Too many attempts from this device.
CIRCUIT_BREAKER_OPEN503retryable, retryAfter, cooldownSeconds, detailscreate-attempt · register-device · validate-attempt · verify-captcha: The widget is already suspended by its circuit breaker. The suspension end is details.suspendedUntil, not a top-level field.
CIRCUIT_BREAKER_TRIGGERED503retryable, retryAfter, cooldownSeconds, detailscreate-attempt · register-device · validate-attempt · verify-captcha: The widget was flood-suspended by this request. retryAfter carries the suspension end.
MISSING_WIDGET_ID400messagecreate-attempt · register-device · validate-attempt · verify-captcha · request-otp · resend-otp: The request reached the rate-limit or circuit-breaker layer without a resolvable widget ID and is refused before the endpoint handler. On create-attempt this is what an unknown publicKey returns. On register-device, validate-attempt and verify-captcha it means an absent or unresolvable attemptId. On request-otp and resend-otp it means an absent or non-string attemptId; an unknown string attemptId reaches the handler and returns ATTEMPT_NOT_FOUND.
RATE_LIMIT_WIDGET_OTP429retryable, retryAfter, cooldownSeconds, detailsrequest-otp · resend-otp: Too many OTP requests for this widget. Like its ATTEMPTS twin this is a widget-wide limit, so it fires for an end user who has done nothing wrong — surface a "try again shortly" state rather than blaming their number.
RATE_LIMIT_PHONENUMBER_OTP429retryable, retryAfter, cooldownSeconds, detailsrequest-otp · resend-otp: Too many OTP requests for this phone number.
RATE_LIMIT_DEVICEID_OTP429retryable, retryAfter, cooldownSeconds, detailsrequest-otp · resend-otp: Too many OTP requests from this device.
IDENTITY_TEMPORARILY_BLOCKED429retryable, retryAfter, cooldownSecondsrequest-otp · resend-otp: 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.

Dual-path errors

CodeShared formatter pathDirect response path
WIDGET_NOT_FOUNDWhen rate-limit lookup cannot find the widget, the error reaches the shared formatter: HTTP 404 with status, code, message, retryable, and action.When the circuit breaker cannot find the widget, it responds directly with HTTP 404 and only status, code, and message.
BILLING_FAILEDPOST /api/v1/widget-sdk/passkey/auth-verify: HTTP 500; the formatError body includes retryable and action.POST /api/v1/widget-sdk/verify-otp: HTTP 400; the controller body carries status, code, message and retryable — but no action. The difference from the passkey path is the missing action, not the retryability signal.
INSUFFICIENT_QUOTAPOST /api/v1/widget-sdk/passkey/auth-verify: HTTP 402; the formatError body includes retryable and action.POST /api/v1/widget-sdk/create-attempt: HTTP 402; the controller body uses quota fields instead of the map action.
TEST_PAIR_RETIREDPOST /api/v1/widget-sdk/passkey/{register-options, register-verify, auth-options, auth-abandon, auth-verify}: HTTP 403; the formatError body includes retryable and action.POST /api/v1/widget-sdk/{request-otp, verify-otp, resend-otp}: HTTP 403; the controller body carries status, code, message and retryable (false at this status on all three endpoints) — but no action.

Advanced Features

Bring Your Own OTP

If you want to generate your own OTP codes, pass a custom otp parameter:

javascript
const response = await axios.post(
  'https://api.akedly.io/api/v1/widget-sdk/create-attempt',
  {
    apiKey: AKEDLY_API_KEY,
    publicKey: WIDGET_PUBLIC_KEY,
    signature,
    timestamp,
    verificationAddress: { phoneNumber: '+201234567890' },
    otp: '123456', // Your custom OTP (4, 5 or 6 digits)
  },
)

Troubleshooting

INVALID_SIGNATURE Error

Common causes:

  • Incorrect HMAC algorithm (must be SHA256)
  • Wrong JSON key order in signature message
  • Extra spaces in JSON string
  • Widget secret is incorrect
  • Timestamp is outside the 60-second window (stale or ahead of server time)

Solutions:

  • Verify you're using HMAC-SHA256
  • Ensure the message format matches the attempt. A phone attempt signs JSON.stringify({ apiKey, publicKey, timestamp, phoneNumber }). An email-only attempt signs JSON.stringify({ apiKey, publicKey, timestamp }) — phoneNumber is omitted entirely, not sent as null or "", because the server builds the same string from an absent value and JSON.stringify drops an undefined property. Adding the key back is the usual cause of an email-only signature mismatch.
  • Check your widget secret matches the dashboard
  • Synchronize server clock with NTP — a clock 60+ seconds fast fails as hard as one that is slow
  • Generate a fresh timestamp and signature per request; never queue or replay a signed payload

Iframe Shows Blank Screen

Common causes:

  • Invalid or malformed attemptId in URL
  • Attempt has expired (5-minute lifetime)
  • Parent page not served over HTTPS
  • Browser blocking mixed content
  • sandbox attribute without allow-same-origin

Solutions:

  • Check browser console for errors
  • Verify iframeUrl contains a valid attemptId
  • Ensure parent page uses HTTPS in production
  • Confirm attempt hasn't expired
  • If the iframe has a sandbox attribute, include allow-scripts allow-same-origin allow-forms allow-popups allow-popups-to-escape-sandbox

Widget Shows "Turn on cookies to continue"

Common causes:

  • The browser or OS blocks cookies, for example Safari with "Block All Cookies" on
  • Storage is blocked for the embedded frame only
  • Android WebView without DOM storage

Solutions:

  • Have the user follow the on-screen steps, then tap "I’ve turned it on — try again"
  • Use "Open in a new tab", where the browser is more likely to allow storage
  • On native apps, open the widget in a system browser instead of a WebView
  • In an iframe, add clipboard-write to allow so Copy link works, and check the frame settings — see When the browser blocks cookies

PIPELINE_NOT_CONFIGURED Error

Solution:

  • Go to Authentication → Setup → Widgets → [Your Widget]
  • Edit the widget and open the Pipeline section
  • Select a pipeline from the dropdown
  • Save changes

OTP Not Received

Common causes:

  • Phone number format is incorrect
  • Rate limit exceeded for phone number
  • Pipeline verification methods not configured

Solutions:

  • Ensure phone number uses E.164 format with country code (e.g., +201234567890)
  • Check rate limit status in dashboard analytics
  • Verify pipeline has at least one verification method enabled