The Payouts API uses the same error envelope as the rest of the Clevis API:
{
"error": {
"code": "PAYOUT_NOT_FOUND",
"message": "No payout 'po_01J...' for this API key.",
"details": {},
"request_id": "01HX9B2KM3..."
}
}
Use the code field for programmatic branching — never the HTTP status,
which can be shared by several codes (e.g. 409 covers two distinct error
codes below).
Error code reference
| Code | HTTP | When it fires | What to do |
|---|
PAYOUTS_DISABLED | 403 | The server has PAYOUTS_ENABLED=false. All /v1/payouts* routes return this. | Check your environment configuration; payouts are gated and may be off in some regions. |
PAYOUT_NOT_FOUND | 404 | No payout with that id is visible to your API key. | The id may be wrong, or it belongs to another API key. Don’t retry with the same id. |
PAYOUT_COUNTRY_NOT_SUPPORTED | 404 | country is not one of MX, CO, AR, BR, CL, PE. | Adding a country is a schema-only change on the server — open a support request. |
PAYOUT_CURRENCY_MISMATCH | 400 | currency does not match the country’s payout currency (e.g. USD for country: "CO"). | Send the country’s native currency. See Country requirements. |
EXTERNAL_ID_CONFLICT | 409 | The external_id is already used by this API key, and no idempotency replay match was found. | Use a unique external_id, or include the original Idempotency-Key header to replay. |
IDEMPOTENCY_KEY_REUSED | 409 | Same Idempotency-Key, but the request body is different from the original. | Either fix the caller to send the original body, or pick a new key. Surface this as a bug — silent retries are not safe here. |
PAYOUT_NOT_CANCELLABLE | 409 | Cancel was called on a payout that’s already terminal (PAID, REJECTED, or CANCELLED). | Read the resource and act on its current status. |
PAYOUT_STORE_FULL | 503 | The mock store hit PAYOUTS_MAX_OPEN (default 10,000). Mock-only failure. | Restart the API to clear the in-memory store, or raise PAYOUTS_MAX_OPEN. Not an issue with the real provider. |
VALIDATION_ERROR | 422 | Request body failed schema validation, including per-country beneficiary checks and bad CLABE/CPF/CBU/RUT/CCI check digits. | Read details for the specific field. A bad check digit is a bad request, not a REJECTED payout — fix the data, don’t retry. |
Don’t confuse these
A few distinctions worth calling out:
REJECTED status vs. 422 VALIDATION_ERROR. A REJECTED payout was
accepted by the API (the request is well-formed) and then turned away by
the provider/bank. Bad input (e.g. malformed CLABE, missing beneficiary
field) is rejected with 422 before a payout is created.
EXTERNAL_ID_CONFLICT vs. IDEMPOTENCY_KEY_REUSED. Both are 409s.
The first means you reused an external_id for a different payout. The
second means you reused an Idempotency-Key for the same logical create,
but the body changed — usually a caller bug.
PAYOUT_NOT_FOUND vs. PAYOUT_COUNTRY_NOT_SUPPORTED. Both are 404s.
The first means “this id doesn’t exist for you”; the second means “this
country isn’t a payout destination yet”.
Handling errors in code
import httpx
resp = httpx.post(
"https://api.clevis.dev/v1/payouts",
headers={
"Authorization": "Bearer YOUR_API_KEY",
"Idempotency-Key": idempotency_key,
},
json=payload,
)
if resp.status_code in (200, 201):
payout = resp.json()
else:
err = resp.json()["error"]
match err["code"]:
case "VALIDATION_ERROR":
print(f"Bad request: {err['details']}")
case "PAYOUT_CURRENCY_MISMATCH":
print("Wrong currency for country — check requirements/{country}")
case "PAYOUT_COUNTRY_NOT_SUPPORTED":
print("Country not in the supported set")
case "EXTERNAL_ID_CONFLICT":
print("external_id already used — fetch the existing payout")
case "IDEMPOTENCY_KEY_REUSED":
raise RuntimeError("Idempotency-Key reused with a different body")
case "PAYOUT_STORE_FULL":
# mock-only — retry after the operator restarts the API
...
case _:
print(f"{err['code']}: {err['message']} (request_id={err['request_id']})")
See also