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 quotingrequest_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 retryPOST /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, thenPOST /v1/beneficiaries/:id/compliance-acknowledgment.
BENEFICIARY_REACKNOWLEDGMENT_REQUIRED
422: Identity PII changed since acknowledgment; new payouts are blocked. Fix: re-attest viaPOST /v1/beneficiaries/:id/compliance-acknowledgment, then retry the payout.
BENEFICIARY_IDENTITY_INVALID
422: Identity fields don’t match the chosenbeneficiary_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 withPOST /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 withGET /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 ofiban, 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 indetails[] (e.g. iban_checksum_failed) and resubmit.
BANK_ACCOUNT_NOT_PENDING
409: Verification is only possible frompending_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 beverified 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 isverified 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 ofpayout_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: passbank_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 viaPOST /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-emptyreasons[] 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 infailed 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.