Skip to main content
Look up an error code and find the request that resolves it. For developers, and agents, handling Cleff API errors.
Agents: fetch this page as plain markdown at https://docs.usecleff.com/errors.md.
Every error response has the same shape. error.code is stable, so branch on it. error.message is for people and can change. docs_url links to the code’s entry on this page, and each entry ends with a Fix line telling you what to do next.
actor is null when the request wasn’t authenticated. details only appears when specific fields were rejected (validation_failed, BANK_ACCOUNT_VALIDATION_FAILED).

Transport codes

Lowercase codes match the HTTP status and are used when no more specific code applies.

bad_request

400: The request is malformed. Fix: correct the request as described in the message and send it again.

validation_failed

400: One or more fields are invalid. details[] lists each one. On POST /v1/payouts, this also covers an Idempotency-Key header that doesn’t match the idempotency_key body field (detail code conflicts_with_header). Fix: correct each field listed in details[] and send the request again.

unauthorized

401: Credentials are missing or invalid. Fix: send a valid API key as Authorization: Bearer <key>. If the key was rotated, send the new one.

forbidden

403: You’re authenticated but not allowed to do this. Fix: check actor.role in the error. A key acts with that role, and a business_read_only key can only read. Send the request with a key whose role allows this action. Retrying with the same key won’t work.

not_found

404: The resource doesn’t exist, or it belongs to the other environment. Fix: check the ID, and check that your API key is for the environment (sandbox or production) where the resource was created.

conflict

409: The request conflicts with the resource’s current state. Fix: fetch the resource, check its current state, and retry only if the request still makes sense.

payload_too_large

413: The request body is over 1 MiB. Nothing in it was processed. Fix: split it into smaller requests. For POST /v1/beneficiaries/import and POST /v1/payouts/bulk, send fewer rows per call.

unsupported_media_type

415: The Content-Type isn’t supported. Nothing in the request was processed. Fix: send Content-Type: application/json. Every endpoint takes JSON. None accepts file uploads or form data.

unprocessable_entity

422: The request is valid but can’t be processed. Fix: read the message. It usually names a condition you need to meet first. Meet it, then retry.

too_many_requests

429: Too many requests. Fix: wait and retry, backing off exponentially.

internal_error

500: Something went wrong on our side. Fix: retry once. If it keeps happening, contact support with the request_id.

Authentication

RATE_LIMITED

429: Your business sent too many requests in the current window. Sandbox keys have a lower limit until your company profile is complete. Production keys and sandbox keys with a complete profile get the higher limit. Fix: wait and retry. To raise a sandbox key’s limit, complete your company profile in the dashboard.

Beneficiaries

BENEFICIARY_NOT_COMPLIANT

422: The beneficiary hasn’t been approved for payouts yet. Fix: add any missing identity details with PUT /v1/beneficiaries/{id}, then call POST /v1/beneficiaries/{id}/compliance-acknowledgment. If GET /v1/beneficiaries/{id} returns 404, the beneficiary_id is wrong or belongs to the other environment.

BENEFICIARY_REACKNOWLEDGMENT_REQUIRED

422: The beneficiary’s identity details changed after you acknowledged them. New payouts are blocked until you acknowledge them again. Fix: call POST /v1/beneficiaries/{id}/compliance-acknowledgment, then retry the payout.

BENEFICIARY_SUSPENDED

422: A payout to this beneficiary failed because of the beneficiary themselves, not their account (for example, the account holder is deceased or blocked). New payouts to them are refused. A payout that was approved but not yet sent is refused too. Acknowledging the beneficiary again doesn’t lift the suspension, and POST /v1/beneficiaries/{id}/compliance-acknowledgment returns this code. Fix: there’s no API action for this. Contact Cleff support.

BENEFICIARY_IDENTITY_INVALID

422: The identity fields don’t match beneficiary_type. Fix: send only the fields for that type (person or business) and try again.

BENEFICIARY_IDENTITY_INCOMPLETE

422: Identity details required for this beneficiary_type are missing. Fix: add the missing fields with PUT /v1/beneficiaries/{id}, then call POST /v1/beneficiaries/{id}/compliance-acknowledgment.

BENEFICIARY_ARCHIVED

422: The beneficiary is archived. You can’t create new payouts, send invites, add destinations for them, delete their destinations, or set their declared destination. Payouts already in progress aren’t affected. Fix: create the beneficiary again with POST /v1/beneficiaries (archiving frees their email) and use the new ID. Archiving can’t be undone.

BENEFICIARY_EMAIL_ALREADY_EXISTS

409: A beneficiary with this email already exists in this environment. Letter case is ignored, so A@x.com and a@x.com are the same address. Archived beneficiaries don’t count. On POST /v1/beneficiaries/import, this code is returned per row inside a 200 response instead, and means the email was already in use before the request. Fix: find the existing beneficiary with GET /v1/beneficiaries and update it with PUT /v1/beneficiaries/{id}. If it’s a different person, use a different email.

BENEFICIARY_EMAIL_LOCKED

422: The beneficiary has a destination on file, so their email can’t be changed. Their collection link goes to that address, and moving it would let you act as them. Resending the current email isn’t a change, whatever its letter case. A deleted destination doesn’t count. Fix: delete each of their destinations with DELETE /v1/beneficiaries/{id}/bank-accounts/{accountId}; GET /v1/beneficiaries/{id}/bank-accounts lists them, and a destination that a payout awaiting approval is headed to is deleted only after you reject that payout with POST /v1/payouts/{payoutId}/reject. Then update the email with PUT /v1/beneficiaries/{id} and register the bank accounts again with POST /v1/beneficiaries/{id}/bank-accounts. A card can’t be registered by you: invite them with POST /v1/beneficiaries/collection-invites and it returns through their own link, which goes to the new address.

BENEFICIARY_ACKNOWLEDGMENT_REQUIRED

Returned per row inside a 200 response, never as an HTTP status. An imported row had acknowledged: false. Import only accepts rows you’ve already verified. If the field is missing entirely, you get validation_failed instead. Fix: set acknowledged: true on the row and send it again. If you haven’t verified the person and don’t have their bank details, invite them with POST /v1/beneficiaries/bulk-invites. If you have their bank details but haven’t verified them, create them with POST /v1/beneficiaries and acknowledge them later with POST /v1/beneficiaries/{id}/compliance-acknowledgment.

BENEFICIARY_EMAIL_DUPLICATED_IN_BATCH

Returned per row inside a 200 response, never as an HTTP status. An earlier row in the same import request used this email. The earlier row was created and this one wasn’t. Fix: remove the duplicate row. The earlier row in results[] has the beneficiary ID. If the two rows are different people, give one of them a different email and send only that row.

Destinations

BANK_ACCOUNT_MISSING_ROUTING

422: No account identifier was sent. Fix: include account_number and routing_code and try again.

BANK_ACCOUNT_VALIDATION_FAILED

422: The bank details were rejected and nothing was saved. Either a field failed its format check (for example aba_checksum_failed), or the account’s country or currency isn’t supported yet (country_not_payable, currency_not_payable). Only US bank accounts in USD are supported today. Fix: correct the field named in details[] and resend. For country_not_payable or currency_not_payable, use a US bank account with currency: "USD".

BANK_ACCOUNT_NOT_FOUND

404: The destination, a bank account or a card, doesn’t exist. Fix: list the beneficiary’s destinations with GET /v1/beneficiaries/{id}/bank-accounts and use one of those IDs. The list includes cards as well as bank accounts, and each item’s kind says which.

BANK_ACCOUNT_BENEFICIARY_MISMATCH

422: The payout’s destination_id (or bank_account_id) is a destination of a different beneficiary, whether a bank account or a card. Nothing was created. Fix: check that beneficiary_id is the beneficiary you mean to pay. Then send the payout again naming one of their own destinations: GET /v1/beneficiaries/{id}/bank-accounts lists every destination they hold, cards included. Leave destination_id out only if they have a declared destination or exactly one of their destinations has payout_usable: true.

BANK_ACCOUNT_NOT_VERIFIED

422: The destination isn’t verified, so it can’t receive payouts. Fix: call POST /v1/beneficiaries/{id}/compliance-acknowledgment. It verifies the beneficiary’s pending destinations. Then retry the payout. If the destination’s status is suspended, acknowledging doesn’t change it: register a new bank account with POST /v1/beneficiaries/{id}/bank-accounts and pay to that one. If it’s the beneficiary’s chosen destination (their declared_destination_id), a payout that names none is refused rather than sent to another of theirs: name another destination with destination_id on the payout, set another as the declared one with PUT /v1/beneficiaries/{id}/declared-destination, or send them an invite with POST /v1/beneficiaries/collection-invites to choose again.

BANK_ACCOUNT_NOT_VALIDATED

422: The bank account is verified, but its routing details haven’t passed validation, so it can’t receive payouts. Fix: check validation_status with GET /v1/beneficiaries/{id}/bank-accounts. A bank account’s details can’t be changed: register the correct details as a new US bank account in USD with POST /v1/beneficiaries/{id}/bank-accounts, delete this one with DELETE /v1/beneficiaries/{id}/bank-accounts/{accountId}, and pay to the new one. If it’s the beneficiary’s chosen destination (their declared_destination_id), a payout that names none is refused rather than sent to another of theirs: name another destination with destination_id on the payout, set another as the declared one with PUT /v1/beneficiaries/{id}/declared-destination, or send them an invite with POST /v1/beneficiaries/collection-invites to choose again.

BANK_ACCOUNT_RAIL_REJECTED

422: The payment network refused to register this bank account, so it can’t receive payouts. The reason is in the message and in the account’s rail_rejection_reason. The beneficiary’s other accounts aren’t affected. Fix: register the correct details as a new bank account with POST /v1/beneficiaries/{id}/bank-accounts and pay to that one. If it’s the beneficiary’s chosen destination (their declared_destination_id), a payout that names none is refused rather than sent to another of theirs: name another destination with destination_id on the payout, set another as the declared one with PUT /v1/beneficiaries/{id}/declared-destination, or send them an invite with POST /v1/beneficiaries/collection-invites to choose again.

DESTINATION_REPLACEMENT_REQUIRED

422: This payout destination can’t be paid any more. Either an earlier payout to it failed because the account is closed, invalid or can’t receive funds, or it’s a card that has expired. For a failed payout, the reason is in the destination’s rail_rejection_reason. Expiry sets no reason: an expired card shows as kind: "card" with payout_usable: false on GET /v1/beneficiaries/{id}/bank-accounts, and its last valid day is in card_valid_through. The destination can’t be used again, and Cleff won’t send the payout to another of the beneficiary’s destinations instead. Fix: ask the beneficiary for a new destination with POST /v1/beneficiaries/collection-invites. The destination they submit is used for their next payout. Or register a new bank account with POST /v1/beneficiaries/{id}/bank-accounts and name it with destination_id on the payout, or make it the declared one with PUT /v1/beneficiaries/{id}/declared-destination.

DESTINATION_HAS_PENDING_PAYOUT

409: A payout awaiting approval is headed to this destination, so it can’t be deleted yet. The message names one of them. Payouts that are already approved or on compliance hold don’t block a delete: they’re still sent to the destination they were approved to. Fix: reject each payout awaiting approval to this destination with POST /v1/payouts/{payoutId}/reject, then delete the destination again with DELETE /v1/beneficiaries/{id}/bank-accounts/{accountId}. GET /v1/payouts?status=pending_approval lists the payouts awaiting approval.

Beneficiary enrollment

BANK_ACCOUNT_AMBIGUOUS

422: The beneficiary has several usable destinations and none is declared, and the payout didn’t name one. Cleff never picks one for them. Fix: name the destination with destination_id on POST /v1/payouts, using an ID from GET /v1/beneficiaries/{id}/bank-accounts where payout_usable is true. To make one the default instead, set it with PUT /v1/beneficiaries/{id}/declared-destination, or send the beneficiary an invite with POST /v1/beneficiaries/collection-invites to choose. Either way it appears as declared_destination_id on GET /v1/beneficiaries/{id}, and payouts sent without destination_id go to it.

BANK_ACCOUNT_NOT_PAYOUT_USABLE

422: The bank account isn’t both verified and validated, so it can’t be used. Fix: check payout_usable with GET /v1/beneficiaries/{id}/bank-accounts and use an account where it’s true. To make this one usable, fix what the BANK_ACCOUNT_NOT_VERIFIED or BANK_ACCOUNT_NOT_VALIDATED Fix describes.

BENEFICIARY_ENROLLMENT_FAILED

422: The payment network refused to register this beneficiary, so none of their destinations can be paid yet. This clears as soon as the network accepts one of their bank accounts. Fix: register a bank account with POST /v1/beneficiaries/{id}/bank-accounts, then check GET /v1/beneficiaries/{id} until enrollment_status is no longer rejected before creating the payout. If it’s refused again, enrollment_failure_detail says why.

Payouts

PAYOUT_NOT_FOUND

404: The payout doesn’t exist. Fix: check the ID, and check that your API key is for the environment where the payout was created. GET /v1/payouts lists the payouts your key can see.

NO_PAYOUT_DESTINATION

422: The payout didn’t name a destination, and the beneficiary has no usable one. Nothing was created. Fix: register a bank account with POST /v1/beneficiaries/{id}/bank-accounts, or send the beneficiary an invite with POST /v1/beneficiaries/collection-invites. GET /v1/beneficiaries/{id}/bank-accounts shows which accounts are payout_usable.

INVALID_STATE_TRANSITION

409: The payout’s current state doesn’t allow this action. Fix: fetch the payout with GET /v1/payouts/{payoutId} and act on its current state. If someone else already changed it, there’s nothing more to do.

REJECTION_REASON_REQUIRED

400: Rejecting a payout needs at least one reason. Fix: include at least one entry in reasons[] and send the rejection again.

BENEFICIARY_EMAIL_NOT_FOUND

Returned per row inside a 200 response, never as an HTTP status. The row’s beneficiary_email matched none of your live beneficiaries in this environment, in any letter case. Archived beneficiaries don’t match. Nothing was created for that row. Fix: find the beneficiary with GET /v1/beneficiaries and correct the email, or send beneficiary_id instead. If the payee isn’t registered yet, create them with POST /v1/beneficiaries. Then send only that row in a new upload, with a new uploadId.

BENEFICIARY_MISMATCH

422: beneficiary_id names a beneficiary, but not in the letter case Cleff returns it. IDs are lowercase. Fix: fetch the beneficiary with GET /v1/beneficiaries/{id} and send its id as beneficiary_id exactly as returned.

INVALID_PURPOSE_OF_PAYMENT

422: purpose_of_payment is missing or not an accepted value. Fix: send one of the purpose_of_payment values listed on the POST /v1/payouts reference, such as payroll or other.

PREDECESSOR_NOT_REPLACEABLE

422: Only a payout that is failed or returned can be replaced. Fix: fetch the original payout with GET /v1/payouts/{payoutId}. Send replaces_payout_id only once its state is failed or returned.

PREDECESSOR_ALREADY_REPLACED

422: The original payout has already been replaced. Fix: don’t create another one. To find the replacement, list payouts with GET /v1/payouts and look for the one whose replaces_payout_id is the original payout’s ID.

IDEMPOTENCY_CONFLICT

409: You reused an idempotency key for a different request, so it was refused instead of replayed. Nothing was created or changed. IDs are compared without regard to case, and objects without regard to key order. On POST /v1/payouts, a key (the Idempotency-Key header or idempotency_key field) replays the original payout for 24 hours, within one environment, only if these fields are unchanged: beneficiary_id, the destination, amount_minor, currency, purpose_of_payment, external_ref, replaces_payout_id and validator_strategy. A field you left out the first time must stay out. For the destination, a retry that names none always matches, and a retry that names one must name the destination the payout uses. On POST /v1/payouts/{payoutId}/decisions (and /approve and /reject), decision_id replays only if status and reasons are unchanged. evaluated_by, policy_version, agent_version and confidence aren’t compared. Fix: decide which request you meant. To send the new details, use a new idempotency key (or decision_id). To find what the original request created, list payouts with GET /v1/payouts and find the one with that idempotency_key, or list GET /v1/payouts/{payoutId}/decisions and find the one with that decision_id. Sending the original request again unchanged returns it with HTTP 200.

PAYOUT_CONCURRENCY_CONFLICT

409: The payout was changed by another request at the same time. Fix: fetch the payout again with GET /v1/payouts/{payoutId} and retry. Retrying is safe.

SANDBOX_WALLET_UNFUNDED

422: Sandbox only. Your sandbox balance is zero, so the payout was refused and nothing was created. No real money is needed, and sandbox payouts never reduce the balance. Fix: if you have not yet submitted your company profile, submit it at /dashboard/settings/company/profile; that gives your sandbox its test money shortly after. If you set the balance to 0 with POST /v1/business/funding-account/sandbox-balance, call it again with a balance above zero. Check GET /v1/funding/account until available_balance_minor is above zero, then resend the same request with the same idempotency key. If the balance never appears, contact support.

Business standing

BUSINESS_BLOCKED

422: Cleff has blocked your business from starting new payouts. This is a decision made by a person at Cleff, not an automatic one. In production, it refuses POST /v1/payouts, POST /v1/payouts/bulk, and approving a payout with POST /v1/payouts/{payoutId}/decisions (or /approve). Payouts already approved still go out, and everything else in the API and dashboard keeps working. Fix: contact Cleff. No API call lifts the block, and you’ll get an email when it’s lifted. You can still reject pending payouts with POST /v1/payouts/{payoutId}/decisions (or /reject) while blocked.

Funding

FUNDING_DEDICATED_ACCOUNT_NOT_PROVISIONED

404: Your business doesn’t have a funding account yet. Fix: complete and submit your company profile at /dashboard/settings/company/profile. A sandbox account is created shortly after you submit, and a production account once your Business is approved. GET /v1/funding/account returns it from then on.

FUNDING_CURRENCY_MISMATCH

422: The currency sent to POST /v1/business/funding-account/sandbox-balance isn’t the funding account’s currency. Fix: leave currency out, or send the currency that GET /v1/funding/account returns.

SANDBOX_ONLY

409: POST /v1/business/funding-account/sandbox-balance only works in sandbox, and this request is in production. Fix: call it with a sandbox API key. In production, the balance only changes through real deposits.

Money

INVALID_MONEY

400: An amount is malformed: an unknown currency, a non-integer amount, or a value out of range. Fix: send amounts as whole numbers in the currency’s smallest unit, with an ISO 4217 currency code.

Next steps

Webhooks

Receive signed payout and beneficiary events instead of polling.