# 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](https://developer.noryo.app/en-GB/merchant/identifiers).

Exact formats are in [Formats](https://developer.noryo.app/en-GB/merchant/requests/formats), refusals in [Validation](https://developer.noryo.app/en-GB/merchant/requests/validation).

## Example

```bash
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"
  }'
```

## The response

A `200` carries four objects:

```json
{
  "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 }
}
```

With the headers `Noryo-Request-Id`, `Noryo-Idempotency-Replayed`, `Noryo-RateLimit-Limit` and `Noryo-RateLimit-Remaining`. Each field is described in [Enrichment](https://developer.noryo.app/en-GB/merchant/models/enrichment) and [Merchant](https://developer.noryo.app/en-GB/merchant/models/merchant).

**A `200` does not mean a merchant was found.** Read `enrichment.status`: `resolved`, `partial` or `not_found`. See [Statuses](https://developer.noryo.app/en-GB/merchant/enrichments/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](https://developer.noryo.app/en-GB/merchant/retention). |
| 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

```ts
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
}
```

For retries, see [Retries](https://developer.noryo.app/en-GB/merchant/operations/retries).

---

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