# Errors

> Every error is an RFC 9457 Problem Details object whose code alone is a machine contract. The format, the complete catalogue of codes by status, and what to do for each.

Every Noryo Merchant API error is a **Problem Details** document (RFC 9457), served as `application/problem+json`.

```json
{
  "type": "https://developer.noryo.app/errors/idempotency_key_reuse",
  "title": "Idempotency key reused",
  "status": 409,
  "code": "idempotency_key_reuse",
  "detail": "this Idempotency-Key is bound to a different request for the remainder of its TTL",
  "request_id": "req_01a07784-d343-75f8-b0ae-b0629b305cd4"
}
```

## The fields

| Field | Content | Stable? |
|---|---|---|
| `type` | The address of the code's page in the [error catalogue](https://developer.noryo.app/en-GB/errors) | The link may change domain; the code does not |
| `title` | A short title, in English, for a human | No: may be reworded |
| `status` | The HTTP status of the response, for every code | |
| `code` | The machine code, in `snake_case` | **Yes: it is the contract** |
| `detail` | A sentence in English, sometimes more specific than the title | No: may be reworded |
| `request_id` | The identifier of the attempt, identical to the `Noryo-Request-Id` header | |

A `410` adds `enrichment_id`, and so does a `503 engine_error`: there it designates the failed attempt, not a result. No other member is added.

> **Important** Build your logic on `code`, and on the HTTP status. `title` and `detail` are written for humans and may be reworded at any time without a version change: a client that parses them will break at the first editorial pass.

Error bodies never carry a SQLSTATE, a table name, a stack trace, an internal hostname, or the value of your key or your `Idempotency-Key`. A `500` carries only `code` and `request_id` as useful information.

## The catalogue

Each code has its own page in the [error catalogue](https://developer.noryo.app/en-GB/errors).

| Status | Code | When | What to do |
|---|---|---|---|
| 400 | [`invalid_request`](https://developer.noryo.app/en-GB/errors/invalid_request) | Body outside the schema, unknown field, `external_transaction_id` on a refresh, unknown host | Fix the request |
| 400 | [`invalid_json`](https://developer.noryo.app/en-GB/errors/invalid_json) | Body that is not valid JSON | Fix the request |
| 400 | [`identifier_required`](https://developer.noryo.app/en-GB/errors/identifier_required) | Create call with neither `Idempotency-Key` nor `external_transaction_id`; refresh without `Idempotency-Key` | Add the identifier |
| 400 | [`invalid_idempotency_key`](https://developer.noryo.app/en-GB/errors/invalid_idempotency_key) | `Idempotency-Key` header empty, too long, repeated or outside visible ASCII | Fix the key |
| 400 | [`invalid_input`](https://developer.noryo.app/en-GB/errors/invalid_input) | Not returned by the current routes | None |
| 400 | [`refresh_requires_transaction_id`](https://developer.noryo.app/en-GB/errors/refresh_requires_transaction_id) | Not returned by the current routes: a refresh of an enrichment without `external_transaction_id` returns `409 refresh_not_available` | See [Refresh](https://developer.noryo.app/en-GB/merchant/refresh) |
| 400 | [`target_enrichment_required`](https://developer.noryo.app/en-GB/errors/target_enrichment_required) | Not returned by the current routes: the path always names the target | None |
| 400 | [`unsupported_canonicalization`](https://developer.noryo.app/en-GB/errors/unsupported_canonicalization) | Not returned by the current routes | Contact support with the `request_id` |
| 401 | [`invalid_api_key`](https://developer.noryo.app/en-GB/errors/invalid_api_key) | Key missing, malformed, unknown, revoked or with the wrong prefix | Fix the key |
| 402 | [`sandbox_quota_exhausted`](https://developer.noryo.app/en-GB/errors/sandbox_quota_exhausted) | Sandbox allowance exhausted | Move to Production |
| 402 | [`no_active_subscription`](https://developer.noryo.app/en-GB/errors/no_active_subscription) | No active subscription covers the operation | Put the account in order |
| 403 | [`insufficient_scope`](https://developer.noryo.app/en-GB/errors/insufficient_scope) | The key lacks the route's scope | Use a key with the right scope |
| 403 | [`account_inactive`](https://developer.noryo.app/en-GB/errors/account_inactive) | Organisation or workspace inactive | Contact Noryo |
| 403 | [`production_not_enabled`](https://developer.noryo.app/en-GB/errors/production_not_enabled) | The key is not allowed in Production | See [Going live](https://developer.noryo.app/en-GB/merchant/going-live) |
| 404 | [`enrichment_not_found`](https://developer.noryo.app/en-GB/errors/enrichment_not_found) | `enrichment_id` unknown, malformed, from another workspace or environment | Check the identifier, the key, the environment |
| 404 | [`transaction_not_found`](https://developer.noryo.app/en-GB/errors/transaction_not_found) | Refresh of a transaction with no recorded result | Create an enrichment first |
| 409 | [`idempotency_key_reuse`](https://developer.noryo.app/en-GB/errors/idempotency_key_reuse) | Same `Idempotency-Key`, different request, within 24 hours | A new key for a new request |
| 409 | [`transaction_id_conflict`](https://developer.noryo.app/en-GB/errors/transaction_id_conflict) | Same `external_transaction_id`, different economic input | Fix the input or the identifier |
| 409 | [`input_mismatch`](https://developer.noryo.app/en-GB/errors/input_mismatch) | Refresh whose input differs from the transaction's | Send the original input |
| 409 | [`refresh_not_available`](https://developer.noryo.app/en-GB/errors/refresh_not_available) | Original enrichment without `external_transaction_id` | Read with `GET`, or create with an identifier |
| 410 | [`result_content_expired`](https://developer.noryo.app/en-GB/errors/result_content_expired) | Content purged after 90 days | See [Retention and 410](https://developer.noryo.app/en-GB/merchant/retention) |
| 413 | [`request_entity_too_large`](https://developer.noryo.app/en-GB/errors/request_entity_too_large) | Body over 16 KiB | Shrink the body |
| 415 | [`unsupported_media_type`](https://developer.noryo.app/en-GB/errors/unsupported_media_type) | `Content-Type` other than `application/json` | Send JSON |
| 429 | [`rate_limited`](https://developer.noryo.app/en-GB/errors/rate_limited) | Rate-limit bucket empty | Retry after `Retry-After` |
| 500 | [`internal_error`](https://developer.noryo.app/en-GB/errors/internal_error) | Unexpected error | Retry with the same key; quote the `request_id` |
| 503 | [`engine_error`](https://developer.noryo.app/en-GB/errors/engine_error) | The resolution engine did not answer; nothing billed or memorised | Retry with the same key |
| 503 | [`busy`](https://developer.noryo.app/en-GB/errors/busy) | Transient contention, or an identical request in progress | Retry after `Retry-After` |
| 503 | [`pricing_not_activated`](https://developer.noryo.app/en-GB/errors/pricing_not_activated) | Billing not yet activated for the operation | Retry after `Retry-After` |

## Retry policy

| Status | Retry? |
|---|---|
| `429` | Yes, after `Retry-After` |
| `503` | Yes, **with the same `Idempotency-Key`**: nothing was billed or memorised |
| `500` | Yes, with the same `Idempotency-Key` |
| `409` | No: fix the payload or the key |
| Other `4xx` | No |

Details are in [Retries](https://developer.noryo.app/en-GB/merchant/operations/retries).

## The attempt identifier

**Every response carries `Noryo-Request-Id`**, and every error body repeats it in `request_id`. One attempt has exactly one identifier: the same one in the header, the body and Noryo's logs, including on a `500`. Quote it to support and we can find the exact attempt. See [Request IDs](https://developer.noryo.app/en-GB/merchant/operations/request-ids).

---

Canonical page: https://developer.noryo.app/en-GB/merchant/error-handling
