# The two identifiers

> Idempotency-Key protects the transport, external_transaction_id protects the invoice. What each one does, why at least one is required, and why you should send both.

Noryo Merchant API recognises two identifiers. They are not the same thing, and confusing them is the most expensive mistake available on this API.

| | `Idempotency-Key` | `external_transaction_id` |
|---|---|---|
| Where | Request header | Body field (create only) |
| What it protects | The **transport**: a network retry | The **economic identity**: the same transaction |
| Lifetime | 24 hours | No expiry |
| Same value, same request | Replays the memorised response, `Noryo-Idempotency-Replayed: true` | Returns the transaction's current result, `deduplicated: true`, not billed |
| Same value, different request | `409 idempotency_key_reuse` | `409 transaction_id_conflict` |
| Scope | Workspace and environment | Workspace and environment |

## At least one is required

On `POST /merchant/v1/enrichments`, a request with neither `Idempotency-Key` nor `external_transaction_id` is refused:

```json
{
  "type": "https://developer.noryo.app/errors/identifier_required",
  "title": "Identifier required",
  "status": 400,
  "code": "identifier_required",
  "detail": "provide an Idempotency-Key or an external_transaction_id",
  "request_id": "req_…"
}
```

Without either, the deduplication branch would be skipped entirely, and two identical calls would become two billed events that nothing can tell apart. The API refuses rather than let that happen. On refresh, the `Idempotency-Key` is required in every case.

## Send both

The key protects the retry, the transaction identifier protects the invoice. They cover different failures:

- **A retry after a timeout**, a few seconds later: the `Idempotency-Key` returns the memorised body, byte for byte.
- **The same statement re-imported three weeks later**: the idempotency key expired long ago, but the `external_transaction_id` recognises the transaction and returns its result without billing it again.
- **Refreshing a result**: only possible if the original call carried an `external_transaction_id`. See [Refresh](https://developer.noryo.app/en-GB/merchant/refresh).

## `external_transaction_id`: economic deduplication

The `external_transaction_id` is your identifier for the transaction: a string of 1 to 255 characters, opaque to Noryo and compared verbatim. The first call carrying it, in a given workspace and environment, records the transaction and its economic input.

| Later call with the same identifier | Response |
|---|---|
| Same economic input | `200` with the transaction's current result: `deduplicated: true`, `deduplicated_by: "external_transaction_id"`, `billable: false`, `units: 0` |
| Different economic input | `409 transaction_id_conflict` |
| Result content expired (over 90 days) | `410 result_content_expired`. See [Retention and 410](https://developer.noryo.app/en-GB/merchant/retention). |

The "current result" is the transaction's first result, replaced only by a refresh that succeeds (`refresh_recalculated`). A `partial` or `not_found` refresh does not replace it.

Two details:

- **"Same input"** is judged on the canonical form of the six economic fields, not on bytes: a difference of case or whitespace in `descriptor` is not a difference, a difference of amount is. See [Formats](https://developer.noryo.app/en-GB/merchant/requests/formats).
- **An engine failure does not record the transaction.** A `503 engine_error` does not claim the identifier: the next retry is processed as a first call.

## `Idempotency-Key`: transport protection

1 to 255 bytes of visible ASCII (`0x21` to `0x7E`). **Noryo applies no normalisation**: no trimming, no case folding, no Unicode processing. `KEY-1` and `key-1` are two different keys. The only whitespace handling is what the HTTP stack itself strips around any header value, which is outside our control and outside yours.

Replay window: **24 hours**. Inside it, the same key with the same request returns the memorised response, with `Noryo-Idempotency-Replayed: true`. The detailed cases (concurrency, refusals, expiry) are in [Idempotency](https://developer.noryo.app/en-GB/merchant/requests/idempotency).

---

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