> ## Documentation Index
> Fetch the complete documentation index at: https://docs.usecleff.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Errors

> The error envelope and every error code the Cleff API can return to an integration

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`.

```json theme={null}
{
  "error": {
    "code": "BENEFICIARY_NOT_COMPLIANT",
    "message": "Beneficiary is not compliant",
    "request_id": "req_abc123",
    "occurred_at": "2026-07-07T12:00:00.000Z",
    "actor": {
      "user_id": "usr_...",
      "membership_id": "mem_...",
      "role": "business_admin",
      "environment": "sandbox"
    },
    "details": [{ "field": "iban", "code": "iban_checksum_failed", "message": "..." }],
    "docs_url": "https://docs.usecleff.com/errors#beneficiary_not_compliant"
  }
}
```

`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.
