Skip to content
Sandbox

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.

View as Markdown

POST /merchant/v1/enrichments returns a merchant identity for a raw bank descriptor. It requires the enrich:write scope.

The body

FieldRequiredRole
descriptoryesThe raw label, 512 characters at most. The only field that currently drives resolution. Not retained.
amountnoTransaction amount. Part of the economic identity.
currencynoISO 4217, upper case. Part of the economic identity.
occurred_atnoDate and time in UTC, Z suffix. Part of the economic identity.
countrynoISO 3166-1 alpha-2, upper case. Part of the economic identity; does not filter the answer.
mccnoMerchant category code, four digits as a string. Part of the economic identity.
external_transaction_idno*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"
  }'
Shell

The 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 }
}
JSON

With 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

StatusProductionSandbox
resolvedbillable: true, units: 1billable: false, units: 1 (taken from the allowance)
partialnot billed, units: 0units: 0
not_foundnot billed, units: 0units: 0

What happens if…

SituationResponse
You send the same request again with the same Idempotency-Key within 24 hours200, the memorised body, Noryo-Idempotency-Replayed: true; nothing is billed
You send the same transaction again (same external_transaction_id, same input), whenever200, the current result, deduplicated: true, billable: false, units: 0
Same Idempotency-Key, different request409 idempotency_key_reuse
Same external_transaction_id, different economic input409 transaction_id_conflict
Neither Idempotency-Key nor external_transaction_id400 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 unit402 sandbox_quota_exhausted
The resolution engine does not answer503 engine_error: nothing is billed or memorised; retry with the same Idempotency-Key
An identical request is in progress with the same keyYours waits for it to finish and returns the replay, or 503 busy with Retry-After
Your rate-limit bucket is empty429 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
}
TypeScript

For retries, see Retries.

Published on 2026-10-08 21:04 UTC (version 1).

Was this page helpful?