# Enrich a transaction descriptor

`POST /merchant/v1/enrichments` — Merchant API · [API reference](https://developer.noryo.app/en-GB/merchant/reference)

Authentication: `bearerCredential` (http bearer).

Returns a merchant identity for a raw bank descriptor.

**At least one identifier is required** — an `Idempotency-Key` header, an
`external_transaction_id`, or both. Without one, the deduplication branch is
skipped entirely and two identical calls would produce two billed events that
nothing can tell apart. The API refuses rather than let that happen.

## Parameters

| Name | In | Required | Type | Description |
| --- | --- | --- | --- | --- |
| `Idempotency-Key` | header | — | string · length 1–255 | 1 to 255 bytes, printable ASCII (0x21–0x7E). Noryo applies **no normalisation**: no trimming, no case folding, no Unicode work. The key you send is the key that is stored — beyond the optional whitespace an HTTP stack strips around any header value, which is out of our hands. Replay window: **24 hours**. Same key and same payload replays the memorised response byte for byte. Same key, different payload, returns `409`. Pattern: `^[\x21-\x7E]+$`. |

## Request body

`application/json` · required · Schema `CreateRequest`

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `descriptor` | string · length ≤ 512 | yes | The raw bank label. **Noryo does not retain it** (retention 0 days). |
| `amount` | number | — | Transaction amount. Part of the economic identity. |
| `currency` | string | — | ISO 4217, **uppercase**. `eur` is rejected — see the guide. Pattern: `^[A-Z]{3}$`. |
| `occurred_at` | string · date-time | — | UTC, with the `Z` suffix: `2026-09-06T11:42:00Z`, optional fraction of a second. An offset such as `+02:00`, or a date that does not exist, is refused with `400`. Pattern: `^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\.[0-9]{1,9})?Z$`. |
| `country` | string | — | ISO 3166-1 alpha-2, **uppercase**. Part of the economic identity; **not yet used by the resolver** — measured, and documented in the guide. Pattern: `^[A-Z]{2}$`. |
| `mcc` | string | — | Pattern: `^[0-9]{4}$`. |
| `external_transaction_id` | string · length ≤ 255 | — | Your identifier for the transaction. Carries economic deduplication: sent again with the same input, it returns the transaction's **current** result (the first one, or the latest successful refresh) with `usage.deduplicated: true`, and bills nothing. |

Example — Descriptor plus an economic identifier

```json
{
  "descriptor": "CB MCDONALDS PARIS 4519",
  "external_transaction_id": "tx_8f21c0"
}
```

Example — Every accepted field

```json
{
  "descriptor": "PAIEMENT CB 1203 68 MULHOUSE CARREFOUR CITY",
  "amount": 12.4,
  "currency": "EUR",
  "occurred_at": "2026-09-06T11:42:00Z",
  "country": "FR",
  "mcc": "5814",
  "external_transaction_id": "tx_8f21c0"
}
```

## Responses

| Status | Description |
| --- | --- |
| `200` | Settled. **A `200` does not mean a merchant was found** — read `enrichment.status`. `not_found` is a normal, complete answer. |
| `400` | `invalid_request`, `invalid_json`, `identifier_required`, `invalid_idempotency_key`, `unsupported_canonicalization`. |
| `401` | `invalid_api_key`. Missing, malformed, unknown, revoked — **always the same code**. Any distinction would be an oracle. |
| `402` | `no_active_subscription`, `sandbox_quota_exhausted`. |
| `403` | `insufficient_scope`, `account_inactive`, `production_not_enabled`. |
| `409` | `idempotency_key_reuse` — same key, different payload. `transaction_id_conflict` — same `external_transaction_id`, different economic input. `input_mismatch`, `refresh_not_available`. |
| `410` | `result_content_expired`. A deduplicated replay (same `external_transaction_id`, same input) of a transaction whose current result has passed its 90-day retention. The body repeats the `enrichment_id`. Nothing is billed; recompute with a refresh. |
| `413` | `request_entity_too_large`. The body limit is 16 KiB, counted in **bytes**. |
| `415` | `unsupported_media_type`. Send `Content-Type: application/json`. |
| `429` | `rate_limited`. Nothing was computed and nothing was billed. |
| `500` | `internal_error`. The body carries no detail about the incident; quote the `request_id` to support. Retrying with the same `Idempotency-Key` is safe. |
| `503` | `engine_error`, `busy` or `pricing_not_activated`. **Nothing was billed and nothing was memorised**: retry with the same `Idempotency-Key`. |

### `200` — `Enrichment`

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `enrichment_id` | string | — | Example: `"enr_7f3c1e20-…"`. |
| `merchant` | Merchant \| null | — | — |
| `merchant.name` | string | — | Example: `"McDonald's"`. |
| `merchant.category` | string | — | Example: `"alimentation.restaurant"`. |
| `merchant.logo_url` | string · uri | — | — |
| `merchant.location` | object \| null | — | **Always present**, `null` when unknown. Absent and unknown look too alike for a client to have to tell them apart. |
| `merchant.location.city` | string | — | Example: `"PARIS"`. |
| `merchant.location.postal_code` | string | — | Example: `"75018"`. |
| `merchant.confidence` | number · 0–1 | — | A **model confidence score** from 0 to 1. It should **not** be interpreted as a calibrated probability of correctness — Noryo has measured that its ordering is not monotonic above 0.95. See the guide. |
| `enrichment` | object | — | — |
| `enrichment.status` | string | — | `resolved` — identity complete and above the policy threshold. `partial` — something is missing or below threshold. `not_found` — the engine answered, and its answer is "nobody". Values: `resolved`, `partial`, `not_found`, `refresh_recalculated`. |
| `enrichment.reason` | string | — | — |
| `enrichment.environment` | string | — | Values: `sandbox`, `production`. |
| `enrichment.source` | string | — | — |
| `enrichment.customized` | boolean | — | `true` when one of **your organization's rules** (Platform → Rules) changed this response — for example a display name or a category. The Noryo result itself is stored unchanged and rules never apply outside your organization and the key's environment. `false` otherwise. A `POST` is rendered with the rules published at that moment and an idempotent replay returns that same body; a `GET` applies the rules published **now**. |
| `enrichment.rules` | array<object> | — | **Optional** — present only when `customized` is `true`: the rules that changed this response, each with the version that applied. Which field each rule changed is shown in the Platform logs. |
| `enrichment.rules[].id` | string · uuid | — | — |
| `enrichment.rules[].version` | integer ≥ 1 | — | — |
| `usage` | object | — | What **this call** cost. |
| `usage.billable` | boolean | — | — |
| `usage.units` | integer | — | — |
| `usage.deduplicated` | boolean | — | — |
| `usage.deduplicated_by` | string \| null | — | — |

## Errors named by the contract

- [`identifier_required`](https://developer.noryo.app/en-GB/errors/identifier_required)
- [`invalid_idempotency_key`](https://developer.noryo.app/en-GB/errors/invalid_idempotency_key)
- [`invalid_json`](https://developer.noryo.app/en-GB/errors/invalid_json)
- [`invalid_request`](https://developer.noryo.app/en-GB/errors/invalid_request)
- [`unsupported_canonicalization`](https://developer.noryo.app/en-GB/errors/unsupported_canonicalization)
- [`invalid_api_key`](https://developer.noryo.app/en-GB/errors/invalid_api_key)
- [`no_active_subscription`](https://developer.noryo.app/en-GB/errors/no_active_subscription)
- [`sandbox_quota_exhausted`](https://developer.noryo.app/en-GB/errors/sandbox_quota_exhausted)
- [`account_inactive`](https://developer.noryo.app/en-GB/errors/account_inactive)
- [`insufficient_scope`](https://developer.noryo.app/en-GB/errors/insufficient_scope)
- [`production_not_enabled`](https://developer.noryo.app/en-GB/errors/production_not_enabled)
- [`idempotency_key_reuse`](https://developer.noryo.app/en-GB/errors/idempotency_key_reuse)
- [`input_mismatch`](https://developer.noryo.app/en-GB/errors/input_mismatch)
- [`refresh_not_available`](https://developer.noryo.app/en-GB/errors/refresh_not_available)
- [`transaction_id_conflict`](https://developer.noryo.app/en-GB/errors/transaction_id_conflict)
- [`result_content_expired`](https://developer.noryo.app/en-GB/errors/result_content_expired)
- [`request_entity_too_large`](https://developer.noryo.app/en-GB/errors/request_entity_too_large)
- [`unsupported_media_type`](https://developer.noryo.app/en-GB/errors/unsupported_media_type)
- [`rate_limited`](https://developer.noryo.app/en-GB/errors/rate_limited)
- [`internal_error`](https://developer.noryo.app/en-GB/errors/internal_error)
- [`busy`](https://developer.noryo.app/en-GB/errors/busy)
- [`engine_error`](https://developer.noryo.app/en-GB/errors/engine_error)
- [`pricing_not_activated`](https://developer.noryo.app/en-GB/errors/pricing_not_activated)

## Request examples

cURL

```bash
IDEMPOTENCY_KEY=$(uuidgen)
curl -sS -X POST 'https://api.noryo.app/merchant/v1/enrichments' \
  -H "Authorization: Bearer $NORYO_API_KEY" \
  -H "Idempotency-Key: $IDEMPOTENCY_KEY" \
  -H 'Content-Type: application/json' \
  -d '{
  "descriptor": "CB MCDONALDS PARIS 4519",
  "external_transaction_id": "tx_8f21c0"
}'
```

JavaScript

```js
// Reuse the same key when you retry this request.
const idempotencyKey = crypto.randomUUID();

const response = await fetch("https://api.noryo.app/merchant/v1/enrichments", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.NORYO_API_KEY}`,
    "Idempotency-Key": idempotencyKey,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    "descriptor": "CB MCDONALDS PARIS 4519",
    "external_transaction_id": "tx_8f21c0"
  }),
});
console.log(response.status, await response.text());
```

Python

```python
import os
import uuid

import requests  # pip install requests

# Reuse the same key when you retry this request.
idempotency_key = str(uuid.uuid4())

response = requests.post(
    "https://api.noryo.app/merchant/v1/enrichments",
    headers={
        "Authorization": "Bearer " + os.environ["NORYO_API_KEY"],
        "Idempotency-Key": idempotency_key,
        "Content-Type": "application/json",
    },
    json={
        "descriptor": "CB MCDONALDS PARIS 4519",
        "external_transaction_id": "tx_8f21c0",
    },
)
print(response.status_code, response.text)
```

Go

```go
package main

import (
	"crypto/rand"
	"fmt"
	"io"
	"net/http"
	"os"
	"strings"
)

func main() {
	// Reuse the same key when you retry this request.
	idempotencyKey := newUUID()

	body := strings.NewReader(`{
  "descriptor": "CB MCDONALDS PARIS 4519",
  "external_transaction_id": "tx_8f21c0"
}`)
	req, err := http.NewRequest("POST", "https://api.noryo.app/merchant/v1/enrichments", body)
	if err != nil {
		panic(err)
	}
	req.Header.Set("Authorization", "Bearer "+os.Getenv("NORYO_API_KEY"))
	req.Header.Set("Idempotency-Key", idempotencyKey)
	req.Header.Set("Content-Type", "application/json")

	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		panic(err)
	}
	defer resp.Body.Close()
	data, err := io.ReadAll(resp.Body)
	if err != nil {
		panic(err)
	}
	fmt.Println(resp.StatusCode, string(data))
}

// newUUID returns a random (version 4) UUID: the standard library has none.
func newUUID() string {
	b := make([]byte, 16)
	if _, err := rand.Read(b); err != nil {
		panic(err)
	}
	b[6] = b[6]&0x0f | 0x40
	b[8] = b[8]&0x3f | 0x80
	return fmt.Sprintf("%x-%x-%x-%x-%x", b[0:4], b[4:6], b[6:8], b[8:10], b[10:])
}
```

Java

```java
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.util.UUID;

public class Main {
    public static void main(String[] args) throws Exception {
        // Reuse the same key when you retry this request.
        String idempotencyKey = UUID.randomUUID().toString();
        String body = """
            {
              "descriptor": "CB MCDONALDS PARIS 4519",
              "external_transaction_id": "tx_8f21c0"
            }""";
        HttpRequest request = HttpRequest.newBuilder(URI.create("https://api.noryo.app/merchant/v1/enrichments"))
            .method("POST", HttpRequest.BodyPublishers.ofString(body))
            .header("Authorization", "Bearer " + System.getenv("NORYO_API_KEY"))
            .header("Idempotency-Key", idempotencyKey)
            .header("Content-Type", "application/json")
            .build();
        HttpResponse<String> response = HttpClient.newHttpClient().send(request, HttpResponse.BodyHandlers.ofString());
        System.out.println(response.statusCode() + " " + response.body());
    }
}
```

C#

```csharp
using System;
using System.Net.Http;
using System.Text;

// Reuse the same key when you retry this request.
var idempotencyKey = Guid.NewGuid().ToString();

using var client = new HttpClient();
using var request = new HttpRequestMessage(new HttpMethod("POST"), "https://api.noryo.app/merchant/v1/enrichments");
request.Headers.TryAddWithoutValidation("Authorization", "Bearer " + Environment.GetEnvironmentVariable("NORYO_API_KEY"));
request.Headers.TryAddWithoutValidation("Idempotency-Key", idempotencyKey);
var body = """
{
  "descriptor": "CB MCDONALDS PARIS 4519",
  "external_transaction_id": "tx_8f21c0"
}
""";
request.Content = new StringContent(body, Encoding.UTF8, "application/json");

using var response = await client.SendAsync(request);
Console.WriteLine((int)response.StatusCode);
Console.WriteLine(await response.Content.ReadAsStringAsync());
```

---

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