Skip to content
Akedly

CLI errors and exit codes

Every CLI failure has a code, an exit status and a hint. This page explains them for people and for scripts.


Exit codes

Branch on the exit status first, then on the error code.

ExitMeaningCodes
0Success.—
64Usage error: a wrong flag, value or argument.USAGE
65Confirmation required: the command needs --yes.CONFIRMATION_REQUIRED
66Login required: no CLI Login, or it ended or expired. Run akedly login.LOGIN_REQUIRED
67Permission denied.PERMISSION_DENIED, ORG_ADMIN_REQUIRED, OWNER_REQUIRED, CLI_READ_ONLY, IMPERSONATION_BLOCKED, SELF_IDENTITY_REQUIRED, CLI_LOGIN_OWNER_PROTECTED
68Browser required: the step can only be done in a browser.NEEDS_BROWSER, SETUP_REQUIRED
69Rate limited.CLI_RATE_LIMITED, and any HTTP 429 whatever its code
70Server unavailable for the CLI, or the CLI needs an upgrade. CLI_DISABLED means customer CLI access is turned off on the server; akedly login is refused with it before any code is shown.NOT_AVAILABLE_ON_SERVER, CLI_UPGRADE_REQUIRED, CLI_DISABLED
1Any other failure, including LOGIN_TIMEOUT, the WAIT_* and WEBHOOK_* codes, and other API, network and local errors.everything else

The two exit-68 codes:

  • NEEDS_BROWSER — the pipeline requires Turnstile, which only a browser can solve. Use a fresh end-user token with the sending command's --turnstile-token, or intentionally enable dev-mode bypasses: akedly pipelines dev-mode set <pipelineId> --dev-mode on --bypass-turnstile on --bypass-pow on --yes. Dev mode loosens live verification on the pipeline; it is not a production workaround.
  • SETUP_REQUIRED — finish phone setup on the Personal Notifications page of the dashboard, then retry.

Commands that end in the browser, such as akedly whatsapp connect, are not failures: they print the dashboard link (see Progress events), and with --wait they then check for the result.


Error shape

An error

{
  "status": "error",
  "code": "PERMISSION_DENIED",
  "message": "<message from the server>",
  "httpStatus": 403,
  "details": { "permission": "api_keys:create" },
  "hint": "<the next step to take>"
}
  • Name
    status
    Type
    string
    Description

    Always "error".

  • Name
    code
    Type
    string
    Description

    The error code. Branch on this, never on message. Most server errors that carry no code of their own show as API_ERROR.

  • Name
    message
    Type
    string
    Description

    What went wrong, for people.

  • Name
    httpStatus
    Type
    number | null
    Description

    The server's HTTP status, or null for a failure inside the CLI.

  • Name
    details
    Type
    object | array
    Description

    Present only when there is something to add, such as the missing permission or, on a 429, retryAfter and cooldownSeconds.

  • Name
    hint
    Type
    string
    Description

    The next step to take, always present. Some commands give their own hint for a code. Otherwise the CLI uses the shared hint in Codes and hints, or a general next step.

In JSON mode the error is the one document on stdout. In human mode it goes to stderr.


Progress events

Some commands report progress on stderr while they work. In JSON mode each event is one JSON line; in human mode it is a sentence. stdout still carries only the final result. stderr can also contain plain-text warnings and notices in JSON mode. Do not parse the whole stderr stream as JSON lines.

EventWhenFields
approval_requiredakedly login has a code for you to approve.userCode, verificationUrl, expiresAt
env_login_activeakedly login saved a new login while AKEDLY_CLI_LOGIN is set, so commands keep using the environment login until you unset it.—
attempt_createdakedly otp v2 attempt --wait created the attempt and is waiting for its result. Emitted only with --wait.attemptId, iframeUrl, expiresAt
rate_limitedThe server answered CLI_RATE_LIMITED and the CLI is waiting to try again.retryInSeconds, attempt, maxAttempts
handoffA command that ends in the dashboard printed its link (whatsapp connect, widgets studio, billing top-up, seats buy, seats add, seats renew).action, path, url (when the dashboard address is known), requested
open_skippedwidgets studio --open had no dashboard address to open.action, message
open_failedwidgets studio --open or akedly login --open could not open the browser.action, message (widgets studio only)

Opening a browser is always best effort; the link is printed either way, and akedly login keeps waiting for your approval. In human mode akedly login --open prints Could not open the browser. Open the approval URL yourself. instead of the event.


Retries

  • Name
    Requests that are safe to repeat
    Description

    Reads, and the writes the reference marks idempotent. Retried on network errors and HTTP 429, 502, 503 and 504, at most 3 tries in total. The CLI waits for cooldownSeconds, retryAfter or the Retry-After header, otherwise 1 second. A retryAfter that is a whole number counts seconds.

  • Name
    Longest wait
    Description

    30 seconds for these automatic retries. When the server asks for a longer wait, the CLI does not wait or retry: it stops with that error straight away. On a 429 it exits 69, and details.retryAfter or details.cooldownSeconds say when to try again. A request cap reached early in its minute therefore ends the command at once instead of waiting. When a command exits 69, wait until details.retryAfter or for details.cooldownSeconds, then run it again.

  • Name
    Login polling
    Description

    The exception. While akedly login waits for your approval, a CLI_RATE_LIMITED answer to a check slows the checks to the pace the server asks for, however long, and the CLI keeps checking. It stops only at the login deadline or when the request expires, with LOGIN_TIMEOUT (exit 1). The first request that creates the login follows the 30-second rule above.

  • Name
    Writes
    Description

    Non-idempotent writes are normally not retried. For commands that use a CLI Login, a refusal explicitly coded CLI_RATE_LIMITED is retried, at most 3 tries, because that refusal sent nothing. A 429 with no server code is also shown as CLI_RATE_LIMITED, but that label alone does not make a write retry.

  • Name
    API key creation
    Description

    api-keys create re-sends its issue request once after a network error or HTTP 5xx, using the same idempotency key. If the response has no Secret, it calls recover-secret once.

  • Name
    Sends
    Description

    The final send request of a send command is never retried, apart from that same CLI_RATE_LIMITED exception.

A timeout reads The request observation timed out; the server may still have processed it. The request may have worked. Check the state, for example with the matching list or get command, before you repeat anything that changes or sends.


Login and rate-limit errors

StatusCodeCause and Solution
410CLI_LOGIN_REQUEST_EXPIREDThe login request expired before it was approved, or it had less than 10 seconds left. Run akedly login again for a new code. Exits 1.
403CLI_LOGIN_DENIEDSomeone denied the request on the approval page. Run akedly login again and approve the new code. Exits 1.
410CLI_SECRET_ALREADY_DELIVEREDThe request was approved, but its credential was already handed out to an earlier check, usually because the terminal lost that response. The credential is never sent twice, so this login is stranded. Start a new akedly login and approve the new code. The stranded login counts toward your active logins until it expires; end it with akedly cli-logins end <id>. Exits 1.
403IMPERSONATION_BLOCKEDA restricted Akedly staff session tried to approve or deny a login code. Only a full-access staff session can do that. The same code also refuses a staff-approved login that tries something staff cannot do; run akedly login and approve it in your own dashboard session. Exits 67.
429CLI_RATE_LIMITEDA request cap was reached. Nothing ran. Wait until details.retryAfter, or for details.cooldownSeconds, then try again. See the caps below. Exits 69.

A CLI Login can end while you use it: when you or a team admin end it, when you leave the team, when the account is deleted, or, for a login that Akedly staff approved, when their staff session ends. The ending takes effect on the credential's next use; leaving the team can take up to about 60 seconds to reach it. The next command then fails with LOGIN_REQUIRED (exit 66). Run akedly login again.

  • Name
    Per CLI Login
    Description

    Every request made with a CLI Login counts toward a per-minute cap for that login, reads included. The minute starts with the first request. Over the cap, the request is refused before it runs, and details.retryAfter is the time the minute ends. Sending Commands use an API key, so this cap does not count them.

  • Name
    Starting a login
    Description

    Requests that start akedly login, and the checks for your decision, are capped per minute per network address.

  • Name
    The approval page
    Description

    Opening, approving and denying login codes in the dashboard is capped per minute per person.

How the CLI retries a CLI_RATE_LIMITED refusal is under Retries: it waits and tries again, at most 3 tries, only when the wait is 30 seconds or less. While akedly login waits for your approval, it slows its checks instead of stopping.


Commands that send or change things

Changes that need --yes

Some setup changes are not deletes, sends or payments, but they still change who can do what or where messages and results go. They need --yes too. Without it they stop with CONFIRMATION_REQUIRED (exit 65) and change nothing; --dry-run previews them.

CommandWhy it needs --yes
team members set-roleChanges what a teammate can do.
team roles create, team roles updateChanges the permissions a role grants to everyone who has it.
whatsapp set-primaryChanges the number your OTPs are sent from.
pipelines update with --webhook-url or --redirect-urlChanges where Akedly sends your users and your verification results.
api-keys createNeeds --yes unless the key is limited with --scope selected, because a key with the default scope can use every pipeline. It always needs --yes with --save, which changes the default key the CLI sends with, and with --replaces-api-key-id, which stops the old Secret at once.
quickstartActivates a pipeline, turns on dev mode for it for one hour, and can save an API key. While dev mode is on, that pipeline skips its rate limits and circuit breaker.
byo-sms update with --sender-nameChanges the Vodafone sender name, which deactivates an active integration until you test it again with akedly byo-sms test. Changed steps, secrets or coverage (--steps-file, --secrets-file, --countries) can do the same.

Examples

akedly team members set-role ana@example.com --role support --yes
akedly team roles create --key support --name Support --permissions pipelines:read --yes
akedly team roles update <id> --permissions pipelines:read --yes
akedly whatsapp set-primary <phoneNumberId> --yes
akedly pipelines update <id> --webhook-url https://example.com/webhooks/akedly --yes
akedly api-keys create --label <name> --save --yes
akedly quickstart --yes
akedly byo-sms update <id> --sender-name <name> --yes

Teammate commands such as team members set-role take <member>: the teammate's email or their member ID from akedly team members list. If the CLI cannot find exactly one teammate, it stops with one of these codes, exit 1, before it changes anything.

  • Name
    MEMBER_NOT_FOUND
    Description

    No teammate matches the email or member ID you gave. Check it with akedly team members list, then run the command again. A pending invitation is not a member yet. Exits 1.

  • Name
    MEMBER_AMBIGUOUS
    Description

    More than one teammate matches the email. Run the command again with the member ID from akedly team members list. Exits 1.

Other commands to check before you repeat

  • Requests to Akedly staff. akedly sso-request create answers HTTP 409 when a request is already open: check akedly sso-request status. akedly demo-extension request answers HTTP 409 when a request is already pending, and DEMO_EXTENSION_NOT_ELIGIBLE (409) when the account cannot get demo credits: check akedly demo-extension status. akedly quotation-requests create has no such check: each run files a new request and notifies staff.
  • Secrets that stop at once. api-keys create --replaces-api-key-id, pipelines webhook-secret regenerate and utilities pipelines signing-secret regenerate stop the old secret immediately. If the response is lost, the old secret may already be dead. Reveal the current secret to a new file before anything else, or for an API key, use akedly api-keys recover-secret. Prefer --secret-file or --save to stdout: stdout can end up in agent transcripts and logs.
  • Utilities sandbox sends (akedly utilities sandbox send):
    • The CLI does not retry it after a timeout or server error. Only a refusal coded CLI_RATE_LIMITED, which sends nothing, is retried.
    • A rejected WhatsApp sandbox send still counts against the rolling 60-minute allowance.
    • Each idempotency key belongs to one attempt. The same key replays the saved attempt, even a failed one. A new key starts a new, separately billed send.
    • After a refusal such as INSUFFICIENT_BALANCE, whether to reuse the key depends on whether an attempt was saved for it. Follow that code's hint under utilities sandbox send.
  • Test-send limit. akedly utilities templates test-send can be refused with 429 TEST_SEND_LIMIT (Daily test-send limit of N reached.), which exits 69. The response does not say when the limit resets.

TOTP enrollment errors

Authenticator enrollment is unavailable through the CLI. totp users create is not a command; a direct enrollment request authenticated with a CLI Login is refused with CLI_TOTP_ENROLLMENT_UNAVAILABLE before provisioning. Use the supported TOTP setup flow instead.

Existing TOTP users, pipelines, history and authentication-request commands remain available in the TOTP reference. This restriction does not remove WhatsApp OTP-template creation.


Settings errors

The settings refusals below exit 1, except malformed flags or conflicting inputs (USAGE, exit 64) and missing --yes (CONFIRMATION_REQUIRED, exit 65). A local refusal has no HTTP status. In a multi-resource command, a refusal can be wrapped in PARTIAL_UPDATE; check its saved resources before retrying.

Local planning and partial saves

CodeExitCause and solution
USAGE64Correct the named flag or JSON file. Do not set one appearance leaf twice, combine recommendation and explicit limits, or supply Turnstile keys without --turnstile on. Method words must be unique members of whatsapp, telegram, sms, email.
NOT_ENOUGH_TRAFFIC1No update was sent. Recommendations need at least 100 analyzed requests and better than low confidence. Keep current limits, choose a longer --period, or set an explicit limits file.
PARTIAL_UPDATE1Inspect applied, notApplied and error in the details. Read unconfirmed resources; a lost response can follow a save. Rerun an update to finish it. After a create, keep secretFile and widgetId, then run the exact pipelines update command supplied in details.recovery instead of creating again.

SETTING_NOT_FOR_THIS_PIPELINE and VERIFICATION_METHOD_UNAVAILABLE can also be raised during CLI planning, with no HTTP status; their remedies are the same as below.

Server settings refusals

StatusCodeCause and Solution
400SETTING_NOT_FOR_THIS_PIPELINERead details.setting, reason and useInstead. V1.2-only settings cannot be applied to V1/V2. Shared widget settings need an attachment; Channel Choice mode needs V2 with a widget. Inspect active and inactive widgets before deciding none exists.
400CLUSTER_GATE_NOT_AVAILABLERead details.reason: not-offered means drop these flags; no-widget means attach a ready eligible pipeline to a widget; check-failed means retry later. Raw API callers can also receive on-create or intent-missing; create the pipeline first and use the supported CLI update.
400CHANNEL_CHOICE_NOT_AVAILABLEdetails.blockers names missing entitlement, granular pricing or pay-per-message billing, an excluded account, the global switch, or a utility pipeline. Contact Akedly for account eligibility; resend other changes without the Channel Choice flags.
400VERIFICATION_METHOD_UNAVAILABLE, VERIFICATION_METHODS_REQUIREDKeep at least one available method. details.method names the unavailable word or catalog id. Recheck the catalog and submit the full list of methods to keep.
400DESTINATION_COUNTRIES_MODE_REQUIREDA changed country list under all would be discarded. Supply only-selected or all-except-selected with the list; use all alone to clear it.
400RATE_LIMIT_INVALIDCorrect details.field and any details.errors. Use whole numbers above zero in the pipeline or widget file shape, and respect widget caps and hour/day ratios. Breaker suspension values are seconds.
400PIPELINE_POW_INVALID, WIDGET_POW_INVALIDV1.2 starting difficulty is adaptive or 2–7. Widgets take a 25–95% trigger and difficulties 3–6, with start no higher than maximum. Compare a changed difficulty with the other saved value.
400PASSKEY_BRANDING_INVALIDCorrect the named field: HTTPS logo URL, full six-digit hex color, or light, dark, system theme. Use none only for a clearable override.
400APPEARANCE_FIELD_UNKNOWNRemove the unknown appearance field named in details.field. Pass the appearance object itself, without a branding or appearance wrapper.
400API_VERSION_CHANGE_NOT_ALLOWEDOnly V1.2-to-V2 is supported. Set both callbacks for V2. Create a separate pipeline if you need another version.
400TEMPLATE_NOT_APPROVEDCheck the selected template's status for the resulting OTP language. Wait for approval, choose an approved template, or restore --otp-template default.
400TURNSTILE_DOMAINS_NOT_EDITABLEThis control edits only enabled existing legacy Web integrations. Manage a custom integration's Hostnames in Cloudflare.
400EXPECTED_UPDATED_AT_INVALIDThe CLI reads revisions automatically. Retry with current CLI/server versions; contact support if it persists. Do not invent a revision or remove the guard.
409PIPELINE_CHANGED, WIDGET_CHANGEDThe resource changed after it was read. Read it again, then rerun the command so the update is planned from the current revision.

For TURNSTILE_SETUP_TOKEN_MISSING or TURNSTILE_SETUP_VERIFICATION_FAILED, follow the CLI's setup-token link, check the Cloudflare Hostnames and key pair, and use a fresh token. Never reuse one after a refused or uncertain save.


Fields and JSON paths

--fields keeps only the paths you name, separated by commas. Missing paths are left out.

A bare array

akedly commands --json --fields name,auth

An envelope

akedly whoami --fields user,cliLogin.expiresAt

Where to look next