# Errors

Every failure returns the same flat shape, so you can handle them in one place.

## The error shape

```bash
curl -X POST https://sandbox-api.baxpay.com/v1/payment-links \
  -H "Authorization: Bearer $BAXPAY_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{ "description": "Refund", "amount": -5, "businessWide": true }'
```

```json
{
  "statusCode": 400,
  "message": ["amount must not be less than 0.01"],
  "error": "Bad Request",
  "timestamp": "2026-07-16T14:02:10.000Z",
  "path": "/v1/payment-links"
}
```

| Field | Always there | Meaning |
| --- | --- | --- |
| `statusCode` | yes | The HTTP status, repeated in the body |
| `message` | yes | A string, or **an array of strings** when several fields failed validation. Written for a human reading a log: show it, don't parse it |
| `error` | yes | The status name (`Bad Request`, `Not Found`, …) |
| `timestamp`, `path` | yes | When and where it failed |
| `code` | sometimes | A stable, machine-readable reason (e.g. `EREPORTING_PERIOD_INVALID`). Branch on it when present |
| `params`, `violations`, `reason`, `retryAfterSeconds` | sometimes | Details for that `code`, such as each failed EN 16931 rule in `violations` |
| `requestId` | with an API key, always | The request id, also in the `X-Request-Id` header; yours if you sent `X-Request-ID`. Find the call in the [request log](https://developers.baxpay.com/conventions#request-log) |

Handle `message` as either type:

```js
const reason = Array.isArray(err.message) ? err.message.join('; ') : err.message;
```

## Status codes

| Status | Meaning | What to do |
| --- | --- | --- |
| `200` `201` `204` | Success | Nothing |
| `400` | Invalid request: validation, a business rule, or a missing/reused `Idempotency-Key` | Fix the request. Retrying won't help |
| `401` | Missing, unknown or revoked credential | Check the key. API keys don't expire; a revoked one must be replaced |
| `403` | Authenticated but not allowed, e.g. no BaxPay plan chosen yet, or a feature your plan doesn't include | Read `message`; don't retry |
| `404` | No such resource **in this key's mode** | Check the id, and that a test key isn't looking for a live record |
| `409` | The same `Idempotency-Key` is still being processed | Wait briefly and retry with the same key |
| `429` | Rate limited (see [limits](https://developers.baxpay.com/conventions#rate-limits)) | Wait for `X-RateLimit-Reset`, then retry |
| `500` `502` `503` | Our side, or a payment partner | Retry with backoff **and the same idempotency key** |

A record from the other mode answers exactly like one that doesn't exist:

```bash
curl https://sandbox-api.baxpay.com/v1/payment-links/00000000-0000-4000-8000-000000000000 \
  -H "Authorization: Bearer $BAXPAY_API_KEY"
```

```json
{ "statusCode": 404, "message": "Payment link not found", "error": "Not Found" }
```

```bash
curl https://sandbox-api.baxpay.com/v1/payment-links \
  -H "Authorization: Bearer bx_test_0000"
```

```json
{ "statusCode": 401, "message": "Invalid or revoked API key", "error": "Unauthorized" }
```

## Retrying safely

A network timeout tells you nothing about whether the call went through. That's what
[idempotency keys](https://developers.baxpay.com/conventions#idempotency-keys) are for.

> **Retry with the same key, then reconcile**
> Send an `Idempotency-Key` on every call that creates something or moves money. If a request fails
> ambiguously (timeout, `409`, `5xx`), retry with **the same key**: BaxPay returns the original result
> instead of acting twice. Still unsure? Read the resource back, for example
> `GET /payment-links/{id}`, before doing anything else.

> **Never retry a 4xx in a loop**
> A `400`, `403` or `404` means the request itself is wrong. Retrying it in a loop turns a bug into
> an outage and gets you rate-limited. Fix the request.
