# Errors & idempotency

Errors are JSON with a human-readable reason and the HTTP status carries the class:

```json
{ "error": "total mismatch: server-computed 9700" }
```

| Status | Meaning | Examples |
| --- | --- | --- |
| 400 | Validation or state error | `total mismatch`, `bad cashback_rate`, `auth not capturable`, `auth expired`, `return qty exceeds captured` |
| 401 | Missing or invalid credentials | `missing x-api-key`, `invalid or expired session` |
| 403 | Permission, environment or gate | `owner/admin required`, `key env mismatch`, `merchant KYB not approved`, `blocked by monitoring` |
| 404 | Unknown resource | `charge not found`, `no active checkout on this terminal` |
| 500 | Unexpected server error | Retry later; capture retries are safe |

## Idempotency

- **Capture is idempotent** on the charge id. Retrying `POST /charges/{id}/capture` returns the same transaction reference and cashback — it never charges twice. Safe to retry on network failure.
- **Void is state-guarded**: only `AUTHORIZED` charges can be voided; a repeat is a `400`.
- **Returns are not idempotent.** Each call records a new return. Retry only when the previous attempt produced no response, and check the charge's captured/returned quantities before retrying.
- **Checkout creation is not idempotent.** A retry creates a new checkout and supersedes the previous `PENDING` checkout on the same reusable terminal. Carry your own `order_id` for reconciliation.
- **Webhooks**: dedupe on the `x-fluense-delivery` header (unique per delivery, stable across retries).

## Conventions

- Money is always integer **minor units** (cents) with an ISO-4217 `currency` field.
- Amounts never cross currencies in one operation.
- Timestamps are RFC 3339 UTC strings.
- Pagination uses `limit` plus a `cursor`; responses carry `next_cursor` (null when done).
