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.
| Exit | Meaning | Codes |
|---|---|---|
0 | Success. | — |
64 | Usage error: a wrong flag, value or argument. | USAGE |
65 | Confirmation required: the command needs --yes. | CONFIRMATION_REQUIRED |
66 | Login required: no CLI Login, or it ended or expired. Run akedly login. | LOGIN_REQUIRED |
67 | Permission denied. | PERMISSION_DENIED, ORG_ADMIN_REQUIRED, OWNER_REQUIRED, CLI_READ_ONLY, IMPERSONATION_BLOCKED, SELF_IDENTITY_REQUIRED, CLI_LOGIN_OWNER_PROTECTED |
68 | Browser required: the step can only be done in a browser. | NEEDS_BROWSER, SETUP_REQUIRED |
69 | Rate limited. | CLI_RATE_LIMITED, and any HTTP 429 whatever its code |
70 | Server 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 |
1 | Any 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
{
"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 asAPI_ERROR.
- Name
- message
- Type
- string
- Description
What went wrong, for people.
- Name
- httpStatus
- Type
- number | null
- Description
The server's HTTP status, or
nullfor a failure inside the CLI.
- Name
- details
- Type
- object | array
- Description
Present only when there is something to add, such as the missing
permissionor, on a 429,retryAfterandcooldownSeconds.
- 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.
details.testPhone on BYO SMS tests is a sentence
When akedly byo-sms test fails, details.testPhone describes where the test message may have gone, for example <saved testPhone or managed Vodafone handset>, or a number followed by (or Akedly's managed handset for Vodafone). It is not a phone number. Do not parse it.
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.
| Event | When | Fields |
|---|---|---|
approval_required | akedly login has a code for you to approve. | userCode, verificationUrl, expiresAt |
env_login_active | akedly login saved a new login while AKEDLY_CLI_LOGIN is set, so commands keep using the environment login until you unset it. | — |
attempt_created | akedly otp v2 attempt --wait created the attempt and is waiting for its result. Emitted only with --wait. | attemptId, iframeUrl, expiresAt |
rate_limited | The server answered CLI_RATE_LIMITED and the CLI is waiting to try again. | retryInSeconds, attempt, maxAttempts |
handoff | A 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_skipped | widgets studio --open had no dashboard address to open. | action, message |
open_failed | widgets 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,retryAfteror theRetry-Afterheader, otherwise 1 second. AretryAfterthat 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.retryAfterordetails.cooldownSecondssay 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 untildetails.retryAfteror fordetails.cooldownSeconds, then run it again.
- Name
- Login polling
- Description
The exception. While
akedly loginwaits for your approval, aCLI_RATE_LIMITEDanswer 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, withLOGIN_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_LIMITEDis retried, at most 3 tries, because that refusal sent nothing. A 429 with no server code is also shown asCLI_RATE_LIMITED, but that label alone does not make a write retry.
- Name
- API key creation
- Description
api-keys createre-sends its issue request once after a network error or HTTP 5xx, using the same idempotency key. If the response has no Secret, it callsrecover-secretonce.
- Name
- Sends
- Description
The final send request of a send command is never retried, apart from that same
CLI_RATE_LIMITEDexception.
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
| Status | Code | Cause and Solution |
|---|---|---|
| 410 | CLI_LOGIN_REQUEST_EXPIRED | The 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. |
| 403 | CLI_LOGIN_DENIED | Someone denied the request on the approval page. Run akedly login again and approve the new code. Exits 1. |
| 410 | CLI_SECRET_ALREADY_DELIVERED | The 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. |
| 403 | IMPERSONATION_BLOCKED | A 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. |
| 429 | CLI_RATE_LIMITED | A 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.retryAfteris 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
Check before you repeat
Commands that delete, spend money or send a real message need --yes, and so do the setup changes below. --dry-run previews without writes or messages; some commands still read resources to build the plan. After an unclear failure, find out what happened before you run the command again: a repeat can send, charge or file a second time.
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.
| Command | Why it needs --yes |
|---|---|
team members set-role | Changes what a teammate can do. |
team roles create, team roles update | Changes the permissions a role grants to everyone who has it. |
whatsapp set-primary | Changes the number your OTPs are sent from. |
pipelines update with --webhook-url or --redirect-url | Changes where Akedly sends your users and your verification results. |
api-keys create | Needs --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. |
quickstart | Activates 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-name | Changes 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. |
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 createanswers HTTP 409 when a request is already open: checkakedly sso-request status.akedly demo-extension requestanswers HTTP 409 when a request is already pending, andDEMO_EXTENSION_NOT_ELIGIBLE(409) when the account cannot get demo credits: checkakedly demo-extension status.akedly quotation-requests createhas 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 regenerateandutilities pipelines signing-secret regeneratestop 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, useakedly api-keys recover-secret. Prefer--secret-fileor--saveto 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 underutilities sandbox send.
- The CLI does not retry it after a timeout or server error. Only a refusal coded
- Test-send limit.
akedly utilities templates test-sendcan be refused with429 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
| Code | Exit | Cause and solution |
|---|---|---|
USAGE | 64 | Correct 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_TRAFFIC | 1 | No 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_UPDATE | 1 | Inspect 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
| Status | Code | Cause and Solution |
|---|---|---|
| 400 | SETTING_NOT_FOR_THIS_PIPELINE | Read 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. |
| 400 | CLUSTER_GATE_NOT_AVAILABLE | Read 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. |
| 400 | CHANNEL_CHOICE_NOT_AVAILABLE | details.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. |
| 400 | VERIFICATION_METHOD_UNAVAILABLE, VERIFICATION_METHODS_REQUIRED | Keep 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. |
| 400 | DESTINATION_COUNTRIES_MODE_REQUIRED | A changed country list under all would be discarded. Supply only-selected or all-except-selected with the list; use all alone to clear it. |
| 400 | RATE_LIMIT_INVALID | Correct 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. |
| 400 | PIPELINE_POW_INVALID, WIDGET_POW_INVALID | V1.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. |
| 400 | PASSKEY_BRANDING_INVALID | Correct the named field: HTTPS logo URL, full six-digit hex color, or light, dark, system theme. Use none only for a clearable override. |
| 400 | APPEARANCE_FIELD_UNKNOWN | Remove the unknown appearance field named in details.field. Pass the appearance object itself, without a branding or appearance wrapper. |
| 400 | API_VERSION_CHANGE_NOT_ALLOWED | Only V1.2-to-V2 is supported. Set both callbacks for V2. Create a separate pipeline if you need another version. |
| 400 | TEMPLATE_NOT_APPROVED | Check the selected template's status for the resulting OTP language. Wait for approval, choose an approved template, or restore --otp-template default. |
| 400 | TURNSTILE_DOMAINS_NOT_EDITABLE | This control edits only enabled existing legacy Web integrations. Manage a custom integration's Hostnames in Cloudflare. |
| 400 | EXPECTED_UPDATED_AT_INVALID | The CLI reads revisions automatically. Retry with current CLI/server versions; contact support if it persists. Do not invent a revision or remove the guard. |
| 409 | PIPELINE_CHANGED, WIDGET_CHANGED | The 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.
Root arrays are projected element by element
When the result is a bare array, as akedly commands --json returns, each path is applied to every element, so 0.name selects nothing. When the result is an envelope, paths start at its root: use data.0.name to reach the first item's name.
akedly commands --json --fields name,auth
akedly whoami --fields user,cliLogin.expiresAt
Where to look next
Codes and hints
—every shared error code, its exit status and its fallback hint
Retry rules
—the full retry legend used in each command's details
Server guards
—where each command's server-side checks are described
Use the CLI with AI agents
—rules for scripts and agents that run the CLI
