Error handling
Every error response — both expected validation failures and unexpected server faults — comes back in the same envelope:
{
"code": "RESOURCE_NOT_FOUND",
"status": 404,
"message": "Inventory line not found",
"data": { "id": "65f3a2b1c8d4e9f7a6b5c4d3" }
}
Four fields, always:
| Field | Type | Meaning |
|---|---|---|
code |
string | Stable machine-readable code. Match on this. |
status |
integer | HTTP status (also on the response status line). |
message |
string | Human-readable default. Useful for logs; do not parse for client logic. |
data |
object | Per-error structured detail. Schema documented per error code. |
Common error codes
These can be returned by any endpoint:
| Code | Status | When |
|---|---|---|
BAD_REQUEST |
400 | Input validation failed, malformed Idempotency-Key header, malformed body. See Validation failures. |
UNAUTHORIZED |
401 | Missing, malformed, invalid, or revoked API key. data is empty by design. |
FORBIDDEN |
403 | API key lacks the scopes the endpoint requires. data lists required and granted. |
TOO_MANY_REQUESTS |
429 | Rate limit exceeded. data carries the limit value and retryAfter (seconds). See Rate limits. |
INTERNAL_SERVER_ERROR |
500 | Unexpected server fault. Quote the response's X-Request-Id header to support. |
Endpoint-specific errors (e.g. INVENTORY_LINE_NOT_FOUND on a line lookup, INSUFFICIENT_FUNDS on a payout, LISTING_ALREADY_SOLD on a purchase) are documented on the operation's reference page along with the shape of their data.
Validation failures
When a request fails validation, data.issues names every field that was rejected:
{
"code": "BAD_REQUEST",
"status": 400,
"message": "Input validation failed",
"data": {
"issues": [
{
"field": "productIds",
"message": "Too big: expected array to have <=200 items"
},
{
"field": "lines.3.quantity",
"message": "Too small: expected number to be >=1"
}
]
}
}
field is a dotted path into the body you sent — listing.price.amount for a nested field, lines.3.quantity for the fourth entry of an array. It is empty when the problem is with the body as a whole.
At most 100 issues come back in one response. Fix those and retry to see any that remain.
issues is absent when a 400 was raised for something other than validation — a malformed Idempotency-Key header, or a body that could not be read at all. message says what happened.
Retry semantics
| Status | Retry? |
|---|---|
| 4xx (other than 429) | No. The request is malformed or unauthorised. Fix the client; retrying doesn't help. |
| 429 | Yes, after Retry-After seconds. |
| 5xx | Yes, with exponential backoff. Cap at ~3 retries; quote X-Request-Id to support if the failure persists. |
Network errors (connection refused, TLS failure, timeout) are equivalent to a 5xx for retry purposes — same backoff, same cap.
Idempotency on writes
Write endpoints that opt in to idempotency accept an Idempotency-Key header. The header value is a free-form string (1–255 chars) that you generate per logical operation; the server caches the response for 24 hours and replays it on subsequent calls with the same key.
Use this for any operation where a network blip-then-retry could otherwise create duplicate state — orders, payouts, listing creations. The operation's reference page tells you whether it's idempotency-aware.
A typical client-side helper:
import { randomUUID } from "node:crypto"
async function createOrder(payload) {
return fetch("https://public-api.cardnexus.com/v1/orders", {
method: "POST",
headers: {
"Authorization": `Bearer ${apiKey}`,
"Content-Type": "application/json",
"Idempotency-Key": randomUUID(),
},
body: JSON.stringify(payload),
})
}
randomUUID() per logical order — not per process, not per session.
Logging + support
Every response carries an X-Request-Id header. Capture this on every error you log; if you contact support, the request id is what makes a problem locatable on our side. Without it, "the API returned 500 last Tuesday" is unactionable.