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:
{
"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_…"
}JSONWithout 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-Keyreturns the memorised body, byte for byte. - The same statement re-imported three weeks later: the idempotency key expired long ago, but the
external_transaction_idrecognises 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.
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. |
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
descriptoris not a difference, a difference of amount is. See Formats. - An engine failure does not record the transaction. A
503 engine_errordoes 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.
Published on 2026-10-08 21:04 UTC (version 1).