Every failure has the same shape, whatever went wrong.
{
"error": {
"type": "card_error",
"code": "card_declined",
"message": "Insufficient funds.",
"decline_code": "insufficient_funds",
"payment": "pay_7f3a2c…",
"doc_url": "https://ronda.am/docs/errors#card_declined"
}
}type tells you how to react; code is the stable machine-readable identifier; message is safe to show a human. param appears when a specific field was at fault, and a decline carries payment — the id of the failed payment, which you can fetch like any other. Internal detail — bank payloads, SQL, stack traces — never crosses this boundary.
Every response, success or failure, carries a ronda-request-id header. Quote it when you ask us about a request — it finds the exact line in our logs.
| Type | Status | What it means |
|---|---|---|
invalid_request_error | 400 / 404 | Something about the request was wrong: a missing parameter, a bad value, an unknown object. |
authentication_error | 401 | The API key was missing, malformed, unknown or revoked. |
permission_error | 403 | Authenticated, but not allowed: a missing scope, a blocked IP, or a suspended account. |
card_error | 402 | The bank declined. Look at decline_code. |
rate_limit_error | 429 | Too many requests. Retry-After tells you when. |
idempotency_error | 409 | An Idempotency-Key was re-used with a different body, or is still in flight. |
api_error | 500 | Something failed on our side. Retry; the payment intent is already recorded. |
| Code | What to do |
|---|---|
parameter_invalid | A value was the wrong type, out of range or malformed. `param` names the field. |
parameter_missing | A required field was not sent. `param` names it. |
parameter_unknown | A field this endpoint does not take — usually a typo; the message suggests the name you probably meant. Unknown fields are refused rather than ignored, so a request never quietly does less than you wrote. |
resource_missing | No object with that id exists for this key's merchant and mode. A test-mode id never resolves with a live key, or the other way round. |
api_key_missing | No key was sent. Use `Authorization: Bearer sk_test_…`. |
invalid_api_key | The key is not one Ronda issued, or it has been rolled. |
api_key_revoked | The key was revoked on the Developers page. |
publishable_key_not_allowed | A publishable key (pk_…) can only create checkout sessions. Use a secret key from your server. |
missing_scope | A restricted key without the scope this endpoint needs. |
ip_not_allowed | The key has an IP allowlist and this request came from elsewhere. |
idempotency_key_in_use | The same Idempotency-Key was sent with a different body, or the first request is still running. Use a new key for a new operation. |
amount_too_large | More than one charge or price may be: ֏100 000 000, or 250 000.00 in USD/EUR. Usually a typo with extra zeros. |
refund_exceeds_balance | The refund is more than what is left on the payment after earlier refunds. |
payment_not_refundable | Only a succeeded (or partly refunded) payment can be refunded. |
plan_inactive | The price is archived. Restore it, or use one that is still offered. |
link_inactive | The payment link was switched off. |
link_expired | The payment link's expiry date has passed. |
link_exhausted | The payment link reached its maximum number of uses. |
velocity_ip_blocked | Too many card attempts from one address in a short time — Ronda's card-testing guard. Real customers rarely see it; wait and retry. |
velocity_customer_blocked | Too many card attempts for one customer in a short time. Wait and retry. |
velocity_merchant_blocked | Live mode only: too many card attempts, or too many declined ones, for this business in a short time, whatever address they came from. New card attempts pause for the window and Ronda is alerted. If it is real traffic, contact support to raise the ceiling. |
coupon_currency_mismatch | An amount-off coupon is in one currency and the subscription is billed in another. Use a percentage coupon, or one in the subscription's currency. |
test_mode_only | Only possible with a test key — simulating a renewal, for example. |
refund_failed | The bank refused the refund. The message carries its reason; nothing was refunded. |
dispute_closed | The chargeback is decided or past its evidence deadline, so evidence can no longer be added. |
bank_not_connected | Live payments need a bank connected on the Settings page. Test mode works without one. |
onboarding_incomplete | The account has not finished onboarding, so live payments are not available yet. |
url_invalid | A webhook URL that does not parse as a URL. |
url_insecure | A webhook URL that is not https. |
url_port_blocked | A webhook URL on a port other than 80 or 443. |
url_credentials | A webhook URL with a username or password in it. Authenticate with the signature instead. |
url_blocked | A webhook host that can never be reached: localhost, *.internal, *.local. |
url_unresolvable | The webhook's hostname does not resolve in DNS. |
url_private_address | The webhook's hostname resolves to a private, loopback or link-local address. |
own_vpos_required | Live payments need the business's own vPOS. Test mode works without one. |
account_suspended | Processing is suspended on this account, usually for an unpaid Ronda invoice. The dashboard's Billing page says why. |
rate_limit_exceeded | Too many requests. Wait for the number of seconds in Retry-After. |
request_too_large | The request body is over the size limit. |
internal_error | Something failed on Ronda's side. Retrying with the same Idempotency-Key is safe. |
Normalised across banks, so your handling does not change when Ronda adds a second bank.
| Code | What to do |
|---|---|
card_declined | The issuer refused without a specific reason. Ask the customer to try another card. |
insufficient_funds | Not enough money on the card. The most common real decline — worth retrying later, which is what dunning does. |
expired_card | The card has expired. Only the customer can fix this; send them a portal link. |
incorrect_cvc | The security code did not match. |
authentication_required | 3-D Secure was required and not completed. |
binding_inactive | The saved card is no longer valid at the bank. Ask the customer to add a card again. |
limit_exceeded | The amount is above the card's limit. |
processing_error | The bank could not process the card. Safe to retry. |
A 500 or a network timeout does not mean the payment failed — it means you do not know. Retry the request with the same Idempotency-Key: if the first attempt succeeded you get the original payment back, and if it did not, the charge happens exactly once. Never retry with a fresh key.
300 requests per minute per API key, and 120 per minute per IP on the unauthenticated surfaces. A publishable key also allows each visitor's IP 30 a minute, so one visitor cannot use up the key for everyone else. A 429 carries a Retry-After header in seconds. Card-testing velocity limits apply separately to checkout attempts: 10 per IP and 6 per customer in a ten-minute window, and in live mode 100 per business, or 20 declined ones, across every address.