Create an enrichment
POST /merchant/v1/enrichments in detail: the body, the identifiers, the response, what is billed, and every possible outcome, from the replay to the 409 and the 503.
POST /merchant/v1/enrichments returns a merchant identity for a raw bank descriptor. It requires the enrich:write scope.
The body
| Field | Required | Role |
|---|---|---|
descriptor | yes | The raw label, 512 characters at most. The only field that currently drives resolution. Not retained. |
amount | no | Transaction amount. Part of the economic identity. |
currency | no | ISO 4217, upper case. Part of the economic identity. |
occurred_at | no | Date and time in UTC, Z suffix. Part of the economic identity. |
country | no | ISO 3166-1 alpha-2, upper case. Part of the economic identity; does not filter the answer. |
mcc | no | Merchant category code, four digits as a string. Part of the economic identity. |
external_transaction_id | no* | Your identifier for the transaction. Carries economic deduplication. |
* At least one of the two is required: the Idempotency-Key header or the external_transaction_id field. In production, send both. See The two identifiers.
Exact formats are in Formats, refusals in Validation.
Example
curl -sS -X POST "https://api.noryo.app/merchant/v1/enrichments" \
-H "Authorization: Bearer $NORYO_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: 6c1a2f0e-1b7d-4c55-8f3a-0d9e2b4c7a61" \
-d '{
"descriptor": "CB MCDONALDS PARIS 4519",
"amount": 12.40,
"currency": "EUR",
"occurred_at": "2026-09-06T11:42:00Z",
"country": "FR",
"mcc": "5814",
"external_transaction_id": "tx_8f21c0"
}'ShellThe response
A 200 carries four objects:
{
"enrichment_id": "enr_ffe80956-cc3e-4e1f-b4b6-724ad231fbb7",
"merchant": {
"name": "McDonald's",
"category": "alimentation.restaurant",
"location": { "city": "PARIS", "postal_code": "75018" },
"logo_url": "https://www.mcdonalds.fr/apple-touch-icon.png",
"confidence": 0.96
},
"enrichment": { "status": "resolved", "reason": null, "environment": "sandbox", "source": "noryo", "customized": false },
"usage": { "billable": false, "units": 1, "deduplicated": false, "deduplicated_by": null }
}JSONWith the headers Noryo-Request-Id, Noryo-Idempotency-Replayed, Noryo-RateLimit-Limit and Noryo-RateLimit-Remaining. Each field is described in Enrichment and Merchant.
A 200 does not mean a merchant was found. Read enrichment.status: resolved, partial or not_found. See Statuses.
What is billed
| Status | Production | Sandbox |
|---|---|---|
resolved | billable: true, units: 1 | billable: false, units: 1 (taken from the allowance) |
partial | not billed, units: 0 | units: 0 |
not_found | not billed, units: 0 | units: 0 |
What happens if…
| Situation | Response |
|---|---|
You send the same request again with the same Idempotency-Key within 24 hours | 200, the memorised body, Noryo-Idempotency-Replayed: true; nothing is billed |
You send the same transaction again (same external_transaction_id, same input), whenever | 200, the current result, deduplicated: true, billable: false, units: 0 |
Same Idempotency-Key, different request | 409 idempotency_key_reuse |
Same external_transaction_id, different economic input | 409 transaction_id_conflict |
Neither Idempotency-Key nor external_transaction_id | 400 identifier_required |
The same transaction, whose current result has passed its 90-day retention (never the case for a not_found) | 410 result_content_expired: the content was purged. See Retention and 410. |
| The Sandbox allowance is exhausted and the result would have cost a unit | 402 sandbox_quota_exhausted |
| The resolution engine does not answer | 503 engine_error: nothing is billed or memorised; retry with the same Idempotency-Key |
| An identical request is in progress with the same key | Yours waits for it to finish and returns the replay, or 503 busy with Retry-After |
| Your rate-limit bucket is empty | 429 rate_limited with Retry-After; nothing is computed or billed |
In TypeScript
import { randomUUID } from "node:crypto";
const BASE_URL = "https://api.noryo.app"; // keep it in configuration
export async function createEnrichment(tx: {
id: string; descriptor: string; amount?: number; currency?: string; occurredAt?: string;
}) {
const res = await fetch(`${BASE_URL}/merchant/v1/enrichments`, {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.NORYO_API_KEY}`,
"Content-Type": "application/json",
"Idempotency-Key": randomUUID(), // generate once per operation and keep it for retries
},
body: JSON.stringify({
descriptor: tx.descriptor,
external_transaction_id: tx.id,
// Omit a missing field: null is refused.
...(tx.amount !== undefined ? { amount: tx.amount } : {}),
...(tx.currency ? { currency: tx.currency.toUpperCase() } : {}),
...(tx.occurredAt ? { occurred_at: tx.occurredAt } : {}),
}),
});
const body = await res.json();
if (!res.ok) throw Object.assign(new Error(body.code), { problem: body });
return body; // read body.enrichment.status before body.merchant
}TypeScriptFor retries, see Retries.
Published on 2026-10-08 21:04 UTC (version 1).