API reference
Errors
The error envelope, every error type with its status, the codes you will actually see, and which decline codes are safe to retry.
Updated
Every failure — validation, authentication, a declined card, a rate limit, our own fault — comes back as the same object, with the same shape, at the HTTP status its type implies.
{
"error": {
"type": "card_error",
"code": "card_declined",
"decline_code": "insufficient_funds",
"message": "The payment was declined (insufficient_funds, soft — the customer may retry). Payment pay_01JBXQ2M4K7T9V3ZC8HR5N6PWD is recorded as declined.",
"param": "payment_method",
"request_id": "req_01JBXQ2M4K7T9V3ZC8HR5N6PWD"
}
}type is the stable thing to branch on. code is stable too but the set grows; treat an unknown code as its type. message is written for a human reading a log and may change without a version bump — never match on it. param names the offending field, dotted for nested ones (payment_method.token). request_id is also on every response as the X-Request-Id header; quote it when you ask about a request.
Types
| HTTP | type | When |
|---|---|---|
| 400 | validation_error | Malformed or missing parameters, or a body that is not valid JSON. |
| 401 | authentication_error | Missing, malformed, unknown, revoked or expired key. |
| 403 | authorization_error | The key is valid but may not do this — wrong key type, wrong environment. |
| 402 | card_error | The issuer declined. decline_code is populated. |
| 404 | not_found_error | The resource does not exist, or belongs to another merchant. We never say which. |
| 409 | idempotency_error | An Idempotency-Key reused with different parameters, or still in flight. |
| 422 | processing_error | The request is valid and cannot be processed — unroutable method, limit exceeded, wrong state. |
| 429 | rate_limit_error | Too many requests. Retry-After says when. |
| 5xx | api_error | Our side. Retry with the same Idempotency-Key. |
404 is deliberately indistinguishable from "belongs to someone else": disclosing that an id exists elsewhere would leak another merchant's activity.
Codes you will see
Validation (400):
code | Meaning |
|---|---|
invalid_parameter | The default for a schema failure. param and message name the field. |
invalid_json | The body did not parse. |
raw_card_data_not_allowed | A card number, expiry or security code appeared in a body. See below. |
token_invalid | The tok_ is unknown, already used, or expired. |
invalid_card_number | Tokenizer only: not a valid test number. |
card_expired | Tokenizer only: the expiry is in the past. |
amount_exceeds_refundable | The refund is larger than what is left to refund. |
invalid_url, https_required, private_address, unresolvable_host | A webhook URL that failed the SSRF guard. |
Processing (422):
code | Meaning |
|---|---|
payment_failed | The processor could not process the authorisation. The payment is failed. |
capture_failed | The capture was refused. The payment stays authorized. |
void_failed | The void was refused. The payment keeps its status. |
payment_not_capturable | The payment is not in a state that can be captured. |
payment_not_refundable | The payment has not been captured, or is fully refunded. |
payment_not_cancelable | The payment is already captured or terminal. |
payment_not_confirmable | The payment is not pending. |
payment_not_advanceable | Sandbox only: the payment is not processing. |
no_route | No connector is configured for that method and currency on this account. |
amount_above_limit | Above the per-transaction limit configured for the account. |
endpoint_limit_reached | Already 16 enabled webhook endpoints in this environment. |
no_matching_endpoint | Nothing is subscribed to the event you asked to resend. |
Idempotency (409): idempotency_key_conflict and idempotency_key_in_use. See idempotency.
Rate limits (429): rate_limit_exceeded, with Retry-After.
Ours (5xx): internal_error, and connector_http_error when the processor itself answered with a server error. Both are safe to retry with the same Idempotency-Key.
Never send raw card data
Any body containing something that looks like a card number, an expiry pair or a security code is rejected before the schema runs, on every endpoint except the sandbox tokenizer:
{
"error": {
"type": "validation_error",
"code": "raw_card_data_not_allowed",
"message": "Raw card data is not accepted here. Tokenise the card with hosted fields (or POST /v1/sandbox/tokens with a test key) and send payment_method.token. Accepting card numbers on this endpoint would put your integration in PCI DSS scope.",
"param": "payment_method.number",
"request_id": "req_01JBXQ2M4K7T9V3ZC8HR5N6PWD"
}
}This is not a formality. It is what keeps a card number off your servers and your SAQ A scope intact, and the guard runs on the raw body so a schema that would have stripped the field cannot let it through.
Declines
A decline is a 402 and the payment still exists. Read it back with GET /v1/payments/:id whenever you want the full outcome: outcome.decline_code, outcome.decline_is_soft, outcome.response_code, plus the AVS, CVV, 3-D Secure and risk fields.
decline_is_soft is the only field you should base a retry on.
decline_code | Soft? | What it means, and what to do |
|---|---|---|
insufficient_funds | soft | No funds right now. A later retry can succeed. |
issuer_unavailable | soft | The issuer did not answer. Retry with backoff. |
try_again_later | soft | A transient issuer refusal. Retry with backoff. |
sca_required | soft | Authentication is needed. Retry with a fresh challenge, not with the same attempt. |
do_not_honor | hard | The issuer refused without a reason. Ask for another method. |
generic_decline | hard | As above, with even less information. |
expired_card | hard | Collect a new expiry. |
incorrect_cvc | hard | Collect the security code again. One retry, not a loop. |
invalid_account | hard | The account does not exist. Ask for another method. |
lost_card | hard | Do not retry and do not tell the cardholder why. |
stolen_card | hard | As above. |
restricted_card | hard | The card may not be used for this. Ask for another method. |
transaction_not_allowed | hard | The issuer does not permit this transaction type. |
fraud_suspected | hard | Do not retry. |
Retrying a hard decline does not help and does count: repeated attempts on the same card are what issuers and schemes read as card testing, and the cost lands on your approval rate.
Show a payer "that card was declined — try another card or another method". Never show them the decline code. lost_card and stolen_card in particular exist for your logs, not for the person at the checkout.