# Errors

Every API error uses one JSON envelope. The HTTP status tells you the class of failure; the
`code` tells you what to do. Do not branch on the human-readable message.

```json
{
  "error": {
    "code": "quote_expired",
    "message": "The quote expired.",
    "requestId": "3f4c1a8e-7b2d-4f0e-9a61-2d8c5b7e1f04"
  }
}
```

`requestId` is also returned in `X-Request-Id`. Include it in a support request. Never
include the API secret, LPA text, QR data or customer records.

## Status classes

| Status | Meaning | Retry? |
| ---: | --- | --- |
| 400 | Malformed request or unsupported value | Fix the request first. |
| 401 | Missing, invalid, expired or replayed credential | Fix authentication. Do not retry unchanged. |
| 402 | Wallet cannot cover the order | Fund the wallet or reduce the order. |
| 403 | Scope, allowlist, account or agent restriction | Fix authority. Do not retry unchanged. |
| 404 | Object absent or not owned by this partner | Confirm the id and environment. |
| 405 | Method not allowed on this path | Use the documented method. |
| 409 | Stock, price, quote, redemption or idempotency conflict | Re-read state before retrying. |
| 413 | Request body too large | Reduce the body. |
| 429 | Per-minute limit or daily quota exhausted | Wait `Retry-After`. |
| 500 | Unexpected server failure | Confirm no effect, then retry an idempotent mutation with the same key. |
| 501 | Capability deliberately gated | Do not retry. Use a supported path. |
| 503 | Dependency unavailable (FX, payment provider) | Retry later with backoff. |

## Error codes

| HTTP | Code | Meaning | Safe action |
| ---: | --- | --- | --- |
| 400 | `validation_error` | Missing or malformed input | Correct the body, query or header. |
| 401 | `missing_signature` | One or more signing headers absent | Send all four signing headers. |
| 401 | `unauthorized` | Unknown, revoked or wrong-environment key | Check the API ID and environment. |
| 401 | `signature_invalid` | HMAC mismatch | Recompute over the exact path and raw body. |
| 401 | `request_expired` | Timestamp outside five minutes | Sync the clock and use a fresh timestamp. |
| 401 | `replayed` | Nonce already used | Generate a fresh nonce. |
| 401 | `key_expired` | Key passed its expiry | Rotate or issue a new key. |
| 401 | `agent_inactive` | Agent suspended, revoked or expired | Ask the owner to reactivate or reissue. |
| 402 | `insufficient_funds` | Available wallet credit is below the total | Fund the wallet or reduce the order. |
| 402 | `payment_required` | Target payment envelope with a funding URL | Surface the funding URL to the principal. Planned. |
| 403 | `forbidden` | Key lacks the required scope, or the agent tried a principal-only action | Request the scope or use a principal key. |
| 403 | `ip_not_allowed` | Source IP is outside the key allowlist | Use an allowed egress address. |
| 403 | `account_not_active` | Account review is incomplete | Finish onboarding before ordering. |
| 404 | `order_not_found` | Purchase absent or not owned | Check the purchase id and environment. |
| 404 | `quote_not_found` | Quote absent, closed or not owned | Create a new quote. |
| 404 | `agent_not_found` | Agent absent or not owned | Check the agent id. |
| 404 | `key_not_found` | Key absent or not owned | Check the key id. |
| 404 | `error_not_found` | Error group absent | Check the error group id. |
| 405 | `method_not_allowed` | Wrong method | Use the documented method. |
| 409 | `out_of_stock` | No sellable stock for at least one offer | Refresh the catalogue or reduce quantity. |
| 409 | `price_changed` | Quote no longer matches the current price | Re-quote or retry the one-call order with a fresh expected total. |
| 409 | `quote_expired` | Quote lifetime ended | Create a new quote. |
| 409 | `not_redeemable` | Purchase not fulfilled or no credentials | Poll the purchase before redeeming. |
| 409 | `idempotency_conflict` | Key already used for a different or paid operation | Reuse the original request or use a new key for a new operation. |
| 409 | `approval_required` | Order exceeds an approval threshold | Stop and request human approval. No charge is made. |
| 413 | `validation_error` | Stripe receiver payload over 256 KB | Reduce the payload. |
| 429 | `rate_limited` | Per-minute or burst limit exceeded | Wait `Retry-After`, then retry with jitter. |
| 429 | `quota_exceeded` | Daily quota exhausted | Wait for the UTC reset or use another call class. |
| 500 | `internal_error` | Unexpected failure | Confirm no effect, then retry with the same idempotency key. |
| 501 | `capability_unavailable` | Feature is gated | Do not retry. Use a supported path. |
| 503 | `fx_unavailable` | EUR or GBP rate missing or stale | Retry later or use USD. |
| 503 | `payment_pending` | Stripe is not configured | Funding is unavailable. Do not retry. |

`approval_required` and `payment_required` are part of the target agent contract and are
not returned by every deployment yet. Handle them if you receive them, and handle the current
`402 insufficient_funds` shape as well.

## Retry rules

### Reads

Retry `429`, `500` and `503` with exponential backoff and jitter. Use a fresh nonce
for every attempt. A read is safe to repeat.

### Mutations

Order, redeem and funding calls are idempotent only when you reuse the same
`Idempotency-Key` and the same body. A new key creates a new logical operation.

| Situation | Action |
| --- | --- |
| `402 insufficient_funds` | Do not retry. Fund the wallet. |
| `402 payment_required` | Do not retry. Surface `funding_url`. |
| `409 price_changed` | Re-quote or retry one-call with the returned total and a new decision. |
| `409 quote_expired` | Create a new quote. |
| `409 idempotency_conflict` | Resolve the original operation. Do not generate a new key to bypass it. |
| `429` | Wait `Retry-After`, then retry with the same key. |
| `500` | Poll the purchase or wallet first, then retry with the same key. |
| `503 payment_pending` | Do not retry. Funding is not configured. |

### Unknown outcomes

If a timeout or connection reset leaves the outcome unknown, do not create a second order. Call
`GET /v1/orders/{purchaseId}` if you have the id, or `GET /v1/orders` to find the
reference. Only create a new attempt after confirming the original did not exist.

## What never appears in an error

- The API secret or any part of it.
- LPA strings or QR payloads.
- Upstream store, product or cost identifiers.
- Customer personal data.
- Stack traces or internal SQL.

## Support

Send the `X-Request-Id`, UTC time, method, path, status and error code to
[partners@roamworks.io](mailto:partners@roamworks.io). For a suspected credential leak, use
[security@roamworks.io](mailto:security@roamworks.io) and revoke the key first.
