Skip to main content
Ringup fails in two different places, and telling them apart is the whole game. A transport error means the request never ran: bad key, malformed body, unknown route. It arrives as a 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.
A declined card is not an HTTP error. charge returns 200 with "status": "failed". If you only check the HTTP status, you will read a decline as a success. Always branch on the status field, never on 200 alone.

The error envelope

Every transport error has the same shape, so you write one handler:
{
  "error": {
    "type": "invalid_request",
    "code": "missing_caller_number",
    "message": "This endpoint needs the X-Caller-Number header."
  }
}
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

StatustypeWhen it happensWhat to do
400invalid_requestA required header or field is missing or malformed.Fix the request. The message names the field.
401authentication_errorMissing, malformed, or unknown API key.Check the Authorization: Bearer header and that the key matches the mode (rk_test_ vs rk_live_). See Authentication.
403permission_errorThe key is valid but not allowed to act for this merchant.Use a key that owns the merchant.
404not_foundUnknown route, or an id that does not exist.Check the path and the id.
409conflictA concurrent write raced this one.Retry once; the idempotency guarantee makes a retry safe.
429rate_limitToo many requests in a short window.Back off and retry with the Retry-After header.
5xxapi_errorA 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.
OutcomeWhere it showsThe signal
Card declinedcharge, and the checkout.failed webhook"status": "failed" with a reason
Caller has no card on fileidentify"known": false (offer a pay link instead)
Merchant takes no payment for this ordercreate_checkout"payment_required": "none" (proceed as the normal call)
First-time caller has not paid yetpayment_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

Every charge 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.