4xx or 5xx with an error envelope. A payment outcome means the request ran fine and the
answer is “no”: a declined card, a caller with no card on file, a merchant who takes no payment.
That arrives as a 200 with a status in the body.
The error envelope
Every transport error has the same shape, so you write one handler:type is the broad class (branch on it), code is the specific reason (log it, show it), and
message is a human sentence (safe to surface to yourself, never to a caller).
Transport errors
| Status | type | When it happens | What to do |
|---|---|---|---|
400 | invalid_request | A required header or field is missing or malformed. | Fix the request. The message names the field. |
401 | authentication_error | Missing, malformed, or unknown API key. | Check the Authorization: Bearer header and that the key matches the mode (rk_test_ vs rk_live_). See Authentication. |
403 | permission_error | The key is valid but not allowed to act for this merchant. | Use a key that owns the merchant. |
404 | not_found | Unknown route, or an id that does not exist. | Check the path and the id. |
409 | conflict | A concurrent write raced this one. | Retry once; the idempotency guarantee makes a retry safe. |
429 | rate_limit | Too many requests in a short window. | Back off and retry with the Retry-After header. |
5xx | api_error | A fault on Ringup’s side. | Retry with backoff. If it persists, the status page and support can help. |
Payment outcomes (all 200)
These are not errors. The request ran; the answer is simply not “charged.” Each has a first-class
field, so you never parse a message to find out what happened.
| Outcome | Where it shows | The signal |
|---|---|---|
| Card declined | charge, and the checkout.failed webhook | "status": "failed" with a reason |
| Caller has no card on file | identify | "known": false (offer a pay link instead) |
| Merchant takes no payment for this order | create_checkout | "payment_required": "none" (proceed as the normal call) |
| First-time caller has not paid yet | payment_status | "status": "pending" (keep waiting, or end and reconcile on the webhook) |
reason on a decline is written for your logs, not for the caller. Your agent should say
something graceful (“that card did not go through, want to try another?”), which your platform
handles, not read the raw reason aloud.Idempotency
Everycharge is deduplicated server-side on the call id and amount. A network retry, a model
retry, or a duplicated tool call returns the original payment result instead of charging
again. This is a server guarantee, so a 409 or a timeout is always safe to retry: you cannot
double-charge a caller by retrying. See the idempotency note in the
API introduction.
Next steps
Webhooks
Reconcile the real outcome out of band, independent of what the caller heard.
Testing
The decline card and fixtures to exercise every path above.