Skip to main content
Every non-2xx response carries the same envelope. error.code is stable and machine-readable; error.message is human-readable and may change. docs_url links back to the code’s section on this page, and every section ends with a Fix line: the action that resolves the error. This page documents the codes reachable through the public API. Cleff Ops, backoffice, dashboard-session and beneficiary-link surfaces emit codes of their own; those are not part of the integration surface and are not listed here.
Agents: fetch this page as plain markdown at https://docs.usecleff.com/errors.md.
actor is null on unauthenticated requests. details appears only on field-level failures (validation_failed, BANK_ACCOUNT_VALIDATION_FAILED).

Transport codes

Lowercase codes mirror the HTTP status when no more specific domain code applies.

bad_request

400: Malformed request. Fix: correct the request shape indicated by the message and resend.

validation_failed

400: Request body failed input validation; details[] lists each rejected field. Fix: correct each field named in details[] and resend.

unauthorized

401: Missing or invalid credentials. Fix: send a valid API key / session; re-authenticate if the session expired.

forbidden

403: Authenticated, but not allowed to perform this action. Fix: retry with an API key that has permission for this action; do not retry with the same credentials.

not_found

404: Resource does not exist or is outside your environment. Fix: check the ID, and that your API key’s environment (sandbox vs production) matches where the resource was created.

conflict

409: Request conflicts with the current state of the resource. Fix: GET the resource, re-evaluate against its current state, then retry if still applicable.

unprocessable_entity

422: Well-formed request that cannot be processed. Fix: see the message; usually a domain precondition; resolve it and retry.

too_many_requests

429: Rate limited; retry later. Fix: back off (exponentially) and retry; do not tighten the loop.

internal_error

500: Unexpected server error. Fix: retry once; if it persists, contact support quoting request_id.

Authentication & registration

RATE_LIMITED

429: Too many requests for this business in the current window. The cap is tiered: a sandbox key on an incomplete profile is throttled to the thin cap; a submitted profile (and any production key) gets the generous cap. Fix: back off and retry after the rate-limit window; to lift a sandbox key off the thin cap, complete your company profile in the dashboard to unlock the generous sandbox tier.

REGISTRATION_RATE_LIMITED

429: Too many self-serve registrations from your network in the current window (the cap is per client IP, not per email or per company). Fix: wait for the window to pass and retry POST /v1/registration; if colleagues are signing up from the same office network, have them register later or ask an existing admin to invite them instead.

EMAIL_ALREADY_REGISTERED

409: An account already exists for this email. Fix: sign in with that email instead, or use password reset.

COMPANY_DOMAIN_ALREADY_CLAIMED

409: Another registrant already claimed this corporate domain. Fix: ask your company’s admin for an invite instead of registering a new company.

Beneficiaries

BENEFICIARY_NOT_COMPLIANT

422: Beneficiary has not passed the compliance gate required for payouts. Fix: complete the identity, then POST /v1/beneficiaries/:id/compliance-acknowledgment.

BENEFICIARY_REACKNOWLEDGMENT_REQUIRED

422: Identity PII changed since acknowledgment; new payouts are blocked. Fix: re-attest via POST /v1/beneficiaries/:id/compliance-acknowledgment, then retry the payout.

BENEFICIARY_IDENTITY_INVALID

422: Identity fields don’t match the chosen beneficiary_type. Fix: send only the fields belonging to the chosen type (individual vs business) and resend.

BENEFICIARY_IDENTITY_INCOMPLETE

422: Identity is well-formed but missing per-type required details. Fix: supply the per-type detail fields (PUT /v1/beneficiaries/:id) before acknowledging compliance.

BENEFICIARY_ARCHIVED

422: The Beneficiary was archived. It accepts no new payouts, no new collection invites, and no new bank accounts. Payouts already in flight are unaffected. Fix: register the payee again with POST /v1/beneficiaries (archiving frees the email address) and address the payout, invite or bank account to the new Beneficiary ID. There is no un-archive.

BENEFICIARY_EMAIL_ALREADY_EXISTS

409: A beneficiary with this email already exists under your account for this environment. Archived beneficiaries do not count: archiving frees the address. Fix: find the existing beneficiary with GET /v1/beneficiaries and update it via PUT /v1/beneficiaries/:id instead of creating a duplicate; if this is a different recipient, create them with a distinct email.

Bank accounts

BANK_ACCOUNT_MISSING_ROUTING

422: At least one routing identifier is required. Fix: include one of iban, account_number, swift_bic, clabe, or pix_key and resend.

BANK_ACCOUNT_VALIDATION_FAILED

422: Routing details failed format/checksum validation; nothing was persisted. Fix: correct the field named in details[] (e.g. iban_checksum_failed) and resubmit.

BANK_ACCOUNT_NOT_PENDING

409: Verification is only possible from pending_verification. Fix: GET the account; if already verified treat as done, otherwise re-register it.

BANK_ACCOUNT_NOT_FOUND

404: Bank account does not exist. Fix: list the Beneficiary’s accounts (GET /v1/beneficiaries/:id/bank-accounts) and use a returned ID.

BANK_ACCOUNT_BENEFICIARY_MISMATCH

422: bank_account_id does not belong to the targeted Beneficiary. Fix: use an account ID from that Beneficiary’s own bank-account list.

BANK_ACCOUNT_NOT_VERIFIED

422: Bank account must be verified to be a payout destination. Fix: verification follows beneficiary compliance: acknowledge compliance for the owning Beneficiary, then retry.

BANK_ACCOUNT_NOT_VALIDATED

422: Bank account is verified but its routing details are not validated, so it cannot be a payout destination. A corridor Cleff has no format/checksum rule for stays unvalidated and is refused for the same reason. Fix: the resolving action depends on the account’s country, so read it first with GET /v1/beneficiaries/:id/bank-accounts (which also reports validation_status and folds it into payout_usable). Cleff has a checker for US (ABA mod-10 on the routing code) and for the SEPA/UK IBAN corridors (IBAN mod-97); the exact list is IBAN_COUNTRIES in src/onboarding/bank-accounts/bank-account-corridor.ts. For those countries the details are checkable, so resubmit corrected routing details with PUT /v1/beneficiaries/:id/bank-accounts/:accountId: a clean run flips validation_status to validated and the payout can be retried, a bad one returns BANK_ACCOUNT_VALIDATION_FAILED naming the offending field. Every other country has no Cleff-side checker today, so the account records unvalidated no matter what is submitted. Do not resubmit in that case, it cannot terminate. The account is not usable as a payout destination until the corridor ships a checker; contact support to request it, and pay through an account in a supported corridor meanwhile.

BANK_ACCOUNT_RAIL_REJECTED

422: The payout rail refused to register this bank account, so it cannot be a payout destination. The rail’s own reason is quoted in the message. This is the third axis of payout_usable, and it is per bank account rather than per Beneficiary: naming a different account on the same Beneficiary is not refused by this code. It is checked before the Beneficiary-wide enrollment state, so an enrollment that failed because of this account answers this code; naming another of that Beneficiary’s accounts while the enrollment is still failed answers BENEFICIARY_ENROLLMENT_FAILED instead. Fix: read GET /v1/beneficiaries/:id/bank-accounts; the offending account carries the same reason in rail_rejection_reason and payout_usable: false. If the reason names a correctable detail (routing number, account number, account type), resubmit it with PUT /v1/beneficiaries/:id/bank-accounts/:accountId, then re-enroll the Beneficiary against that account (POST /v1/beneficiaries/:id/enroll with bank_account_id) to clear the rejection; otherwise register a different account and enroll against it. Retry the payout naming an account whose payout_usable is true.

Beneficiary enrollment

NO_PAYOUT_USABLE_BANK_ACCOUNT

422: Beneficiary has no verified + validated bank account to enroll. Fix: register a bank account (POST /v1/beneficiaries/:id/bank-accounts), get it verified + validated, then enroll.

BANK_ACCOUNT_AMBIGUOUS

422: Multiple payout-usable bank accounts; none was named. Fix: pass bank_account_id explicitly in the enroll call.

BANK_ACCOUNT_NOT_PAYOUT_USABLE

422: The specified bank account is not verified + validated. Fix: pick a payout-usable account from the Beneficiary’s list, or resolve this account’s verification/validation first.

BENEFICIARY_ENROLLMENT_FAILED

422: Provider rail enrollment failed; the destination bank is not registered with the provider. Fix: correct the bank account details, re-enroll via POST /v1/beneficiaries/:id/enroll, then create the payout.

Payouts

PAYOUT_NOT_FOUND

404: Payout does not exist. Fix: check the ID and your key’s environment; list payouts to find the right one.

INVALID_STATE_TRANSITION

409: The payout’s current state does not permit this transition. Fix: GET the payout and act on its actual state; if another actor already transitioned it, treat as resolved.

REJECTION_REASON_REQUIRED

400: Rejecting a payout requires at least one reason. Fix: include a non-empty reasons[] in the reject call.

BENEFICIARY_MISMATCH

422: Input beneficiary does not match the verified beneficiary. Fix: GET the Beneficiary and echo its verified identity fields exactly in the payout request.

INVALID_PURPOSE_OF_PAYMENT

422: purpose_of_payment is missing or not a recognised value. Fix: send one of the documented purpose values (see the payout create reference).

PREDECESSOR_NOT_REPLACEABLE

422: A replacement payout requires its predecessor to be in failed or returned state. Fix: GET the predecessor; only replace payouts that have failed or been returned.

PREDECESSOR_ALREADY_REPLACED

422: The predecessor payout was already replaced. Fix: GET the predecessor to find the existing replacement and use it; do not create another.

PAYOUT_CONCURRENCY_CONFLICT

409: The payout was modified concurrently. Fix: re-GET the payout and retry the transition against its fresh state; the retry is safe.

SANDBOX_WALLET_UNFUNDED

422: Sandbox only. The Business’s sandbox wallet has no test money (it is empty, or no wallet has been provisioned yet), so the payout was refused before anything was written. This is not the production insufficient-funds rejection: no money needs to be wired, and the balance is never drawn down by sandbox payouts. Fix: complete the business profile at /dashboard/settings/company/profile and submit it; a complete dossier funds the sandbox wallet with $1,000,000 of test money. Funding is applied just after the submit response returns, not within it, so poll GET /v1/funding/account until available_balance_minor is above zero rather than retrying the payout immediately. Then replay the identical create (same idempotency_key), since the refused attempt wrote nothing. If the balance never appears, the Business predates self-serve sandbox enrollment: its wallet cannot provision itself, and no action on your side changes that. Contact support and ask for the sandbox-enrollment backfill; it is a one-off Cleff-side repair that is safe to run at any time, after which the balance appears without you resubmitting anything.

Funding

FUNDING_DEDICATED_ACCOUNT_NOT_PROVISIONED

404: The Business has no provisioned dedicated account. Fix: finish Business onboarding (KYB approval provisions the account); do not retry until it is provisioned.

FUNDING_CURRENCY_MISMATCH

422: Balance report currency does not match the dedicated account currency. Fix: report the balance in the account’s own currency.

SANDBOX_ONLY

409: Setting the funding-account balance directly (POST /v1/business/funding-account/sandbox-balance) is only allowed on a sandbox account; the session is production. Fix: call it from a sandbox session; a production account’s balance moves only through real funding deposits, never this endpoint.

Money

INVALID_MONEY

400: A money amount is malformed (unknown currency, non-integer minor units, or out of range). Fix: send amounts as integer minor units with an ISO-4217 currency code.