Errors
Every failure — validation, auth, a missing record, a provider outage, a path that does not exist — answers JSON in one shape:
Code
Branch on code. It is a stable token and does not change once published. error is prose
for a human reading a log, and we reword it freely.
Each operation in the reference lists the codes it can carry. We may add codes as new conditions become distinguishable — we never remove one or repurpose its meaning — so keep a default branch for a value you do not recognise.
Codes any operation can answer
| Code | Status | Meaning |
|---|---|---|
invalid_request | 400 | the body or path does not match the published schema |
unreadable_request | 400 | the body is not the media type it claims |
unauthorized | 401 | missing, malformed or revoked key |
payload_too_large | 413 | over the body limit |
upstream_failed | 502 | us, or a provider we depend on |
unknown_endpoint | 404 | this API serves no such path |
unknown_endpoint belongs to no operation, so it appears under none of them in the reference —
it is how a mistyped path answers, in the same shape as everything else, rather than as plain
text your client cannot parse.
Codes worth handling by name
Not found — business_not_found, invoice_not_found, import_not_found,
subscription_not_found. Usually an identifier you have not synced yet rather than one that does
not exist.
Refusals on taking an advance, all 409: invoice_not_financeable, business_not_eligible,
concentration_exceeded. See financing an invoice.
Conflicts on a replay — already_subscribed, already_imported. Both mean the thing you are
creating is already there.
Rejected input we can name — invalid_routing_number, invalid_invitee, self_dealing (a
business cannot finance its own invoice), unsupported_media_type, unreadable_file,
too_many_rows (an import is capped at 1000), destination_refused (a webhook URL we will not
send to).
Retrying
502 is the only one worth retrying blind, with a backoff. Everything in the 4xx range will
answer the same way until the request or the underlying state changes.
A retry after a timeout you never saw the answer to is safe for the writes keyed on your own identifier — syncing a business, syncing an invoice, setting a payout account, taking an advance, an import. Those update rather than duplicate.
Subscribing to a webhook destination is the exception. If the first POST /v1/webhooks
committed and you lost the response, the retry answers 409 already_subscribed — and the signing
secret was returned once and is never returned again, including by
GET /v1/webhooks. The subscription exists and you cannot verify
anything it delivers. Recover by deleting it with
DELETE /v1/webhooks/{reference} — the listing gives
you the reference — and subscribing again, keeping the secret this time.

