Skip to content
Sandbox

Quickstart

From a Noryo identity to a first enrichment read back: enable the sandbox, create a key, call the API with curl and TypeScript, then check usage on Platform.

View as Markdown

This guide takes you from a Noryo identity to a first enrichment read back, in seven steps, entirely in the sandbox: no production key exists today, and nothing is billed. You will need:

  • a Noryo identity (https://auth.noryo.app) and membership of a workspace on https://platform.noryo.app;
  • curl, or a recent Node.js where fetch is built in, for the TypeScript examples.

1. Enable the sandbox on Platform

Sign in to https://platform.noryo.app with your Noryo identity and open the Clés d'API (API keys) page at /keys.

Complete the workspace's developer profile first: first and last name, verified email, professional phone number, organisation name, then what you are building and your integration context. An owner or an admin fills it in once, for the whole workspace. While anything is missing, enabling is refused and no key is issued; Platform lists the missing items. Details on Production access.

With the profile complete, an owner or an admin clicks Activer le sandbox Transaction Intelligence (enable the Transaction Intelligence sandbox). Enabling it is a workspace decision, and it is logged: it creates nothing but the right to issue nry_txn_sbx_ keys, with no intervention from Noryo.

Note

On Platform you are a member of a workspace, the organisation that is the API's customer: neither a user of the Noryo app, nor a bank account, nor your product's end user.

2. Create a key and copy the secret

Name the key (for instance "test integration") and click Créer une clé Sandbox (create a sandbox key). An owner, an admin or a developer can create and revoke keys; every member can list them. The number of active keys per workspace is capped (the cap is read from the server; revoked keys do not count).

The credential has the form nry_txn_sbx_<key_id>.<secret>. The key_id is public (10 characters) and stays visible in the list. The secret is shown once only, at creation: Noryo does not keep it, only an HMAC verifier is stored. Copy it now. If you lose it there is no rotate button: revoke the key and create a new one. A revoked key returns 401 from the very next request.

3. Put the secret in an environment variable

The secret lives server-side, in an environment variable. Every example reads NORYO_API_KEY.

export NORYO_API_KEY="nry_txn_sbx_..."
Shell

Never put it in a browser bundle, a public repository, a mobile binary or client-side JavaScript. An nry_mer_* key (Merchant API) is refused on Transaction Intelligence, and the reverse is also true. Details on Authentication.

4. First call: enrich a transaction

POST/transactions/v1/enrichments

Enrich a transaction.

Reference Transaction Intelligence

Parameters

  • Idempotency-Keystringheader

    Optional. With the same key and the same request: replay (200), the original response is returned as it was, engine version included. With the same key and a different request: 409. "DIFFERENT REQUEST" is judged on the economic fingerprint (fingerprint), and on nothing else: two bodies that differ only by external_transaction_id, account_hint or counterparty_hint have the same fingerprint and are the same request for idempotency; the first submission is replayed as it was. Do not reuse a key for two transactions that you distinguish by those fields alone. No engine version enters this identity: a deployment between two attempts does not turn a replay into a new operation. The raw value is never stored or logged. The key is retained for at least 24 hours: it expires 24 hours after the first submission and is only erased by the daily purge that follows that expiry. Between 24 and 48 hours after the first submission, the same key therefore opens a new operation (201), never before 24 hours. From 1 to 255 bytes of visible ASCII (0x21 to 0x7E); an empty value or one outside that alphabet is rejected with 400.

Request bodyrequired

application/json · TransactionInput

  • descriptorstring · length 1–512required
  • directionstringrequired
    incomingoutgoing
  • occurred_atstring · date-time

    RFC 3339 with an offset. Never reduced to a date: midnight is not the same day everywhere.

  • amountMoney
  • valuestringrequired

    Decimal as a string. A float would lose pence in silence. The number of decimal places allowed depends on the currency: 5.905 EUR is rejected, and so is 100.50 ISK (ISK and JPY have none, KWD has three).

    ^-?(0|[1-9][0-9]{0,15})(\.[0-9]{1,4})?$
  • currencystringrequired

    A currency outside this list is rejected with 422 unknown_currency.

    CADCHFDKKEURGBPISKJPYKWDNOKPLNSEKUSD
  • paymentobject
  • railstring · length ≤ 128
  • mccstring
    ^[0-9]{4}$
  • card_presentboolean
  • network_merchant_idstring · length ≤ 128
  • raw_typestring · length ≤ 128
  • location_hintobject
  • countrystring
    ^[A-Z]{2}$
  • citystring · length ≤ 128
  • postal_codestring · length ≤ 128
  • counterparty_hintobject

    The IBAN is reduced on entry to its country and bank code. The account number does not enter the system, so it cannot leave it.

  • ibanstring
    ^[A-Z]{2}[0-9]{2}[A-Z0-9]{10,30}$
  • bicstring
    ^[A-Z]{6}[A-Z0-9]{2}([A-Z0-9]{3})?$
  • account_hintobject

    The account on which the transaction went through, as distinct from counterparty_hint, which designates the other end. Without it, a transfer pair cannot be identified: the only signal that designates it is "my account is your counterparty, and your counterparty is my account". The IBAN is reduced on entry, like the counterparty's: the account number does not enter the system. external_account_id: your own identifier for the account, for cards, wallets and rails without an IBAN. It serves one purpose: linking the transaction to a coverage declared on the same account. It does not enter the fingerprint. Measured: it used to be accepted, then discarded; the series had no source, and lifecycle.state stayed unknown.

  • ibanstring
    ^[A-Z]{2}[0-9]{2}[A-Z0-9]{10,30}$
  • external_account_idstring · length 1–128
  • external_transaction_idstring · length 1–128

    Your own transaction identifier. It does not enter the fingerprint: it says how you name the transaction, not what it is. Its purpose is to stop two submissions of the same transaction from being matched to each other.

Responses

200Already enriched: request replayed, nothing was created. Location is set here too: the client learns where the resource it did not create lives.Enrichment
201Enrichment created.Enrichment
400Unreadable body, or Idempotency-Key outside the alphabet.Problem
401Credential missing, malformed, from another family, unknown, revoked, or whose declared prefix does not match the recorded prefix. A single code for all these cases: telling them apart would be an oracle.Problem
403Workspace or organisation inactive.Problem
409This Idempotency-Key has already been used for a different request, that is, one with a different economic fingerprint (fingerprint). Same key + same fingerprint = replay (200), never 409.Problem
413Body too large.Problem
415The body must be application/json.Problem
422Closed schema: unknown field, or violated constraint. errors[].code names the rule, and the list is the runtime's own, by construction: body_not_object, unknown_field, required_non_empty_string, too_long, contains_control_character, must_be_incoming_or_outgoing, must_be_rfc3339_with_offset, must_be_object, must_be_string, must_be_boolean, must_be_four_digits, must_be_iso_3166_1_alpha_2_uppercase, malformed_iban, malformed_bic, must_be_non_empty_string, amount_not_object, amount_value_not_string, amount_value_malformed, amount_currency_not_string, amount_currency_malformed, amount_currency_unknown, amount_precision_exceeds_currency. must_be_rfc3339_with_offset covers form and calendar: 2026-02-30T00:00:00Z, +16:00 or 23:59:60 are rejected. must_be_object also rejects an array, for all four families. contains_control_character: a control character (C0, DEL, C1) in a text, except tab, line feed and carriage return. amount_currency_unknown is returned under the unknown_currency type, the others under invalid_input.Problem
429rate_limited: the operational rate limit has been reached, for this key or for the workspace (the detail says which). Nothing was computed, nothing was stored, no event was emitted. Replay after Retry-After. See x-noryo-rate-limit.Problem
500Internal error.Problem

The body is a TransactionInput. Two fields are enough: descriptor, the label as the bank renders it, and direction, either incoming or outgoing. The schema is closed: an unknown field is refused with 422, never ignored. The body is application/json, otherwise 415.

Minimal body

curl -sS -i https://api.noryo.app/transactions/v1/enrichments \
  -H "Authorization: Bearer $NORYO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"descriptor":"CARTE 09/09 MONOPRIX PARIS","direction":"outgoing"}'
Shell
const response = await fetch("https://api.noryo.app/transactions/v1/enrichments", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.NORYO_API_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({ descriptor: "CARTE 09/09 MONOPRIX PARIS", direction: "outgoing" }),
});

console.log(response.status, response.headers.get("location")); // 201
const enrichment = await response.json();
console.log(enrichment.enrichment_id);
TypeScript

Status 201: the enrichment has been created. The Location header says where it lives, and the body carries its enrichment_id, of the form enr_<uuid>. Keep it for step 5.

Full example

The contract's achat_carte example: occurred_at in RFC 3339 with an offset, amount.value as a string, the account in account_hint (so that a coverage window can be linked to it), your own identifier in external_transaction_id.

curl -sS https://api.noryo.app/transactions/v1/enrichments \
  -H "Authorization: Bearer $NORYO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "descriptor": "CARTE 09/09 MONOPRIX PARIS 15",
    "direction": "outgoing",
    "occurred_at": "2026-09-09T07:31:00+02:00",
    "amount": { "value": "49.90", "currency": "EUR" },
    "payment": { "rail": "card", "mcc": "5411", "card_present": true },
    "location_hint": { "country": "FR" },
    "account_hint": { "iban": "FR7630006000011234567890189" },
    "external_transaction_id": "tx-2026-09-09-0042"
  }'
Shell
const transaction = {
  descriptor: "CARTE 09/09 MONOPRIX PARIS 15",
  direction: "outgoing",
  occurred_at: "2026-09-09T07:31:00+02:00",
  amount: { value: "49.90", currency: "EUR" },
  payment: { rail: "card", mcc: "5411", card_present: true },
  location_hint: { country: "FR" },
  account_hint: { iban: "FR7630006000011234567890189" },
  external_transaction_id: "tx-2026-09-09-0042",
};

const response = await fetch("https://api.noryo.app/transactions/v1/enrichments", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.NORYO_API_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify(transaction),
});

const enrichment = await response.json();
console.log(enrichment.resolution.policy.outcome);
TypeScript

What the response contains

An extract of the contract's 201 response for this example:

{
  "enrichment_id": "enr_3f2504e0-4f89-41d3-9a0c-0305e82c3301",
  "created_at": "2026-09-09T05:31:02Z",
  "contract_version": "ti_v1",
  "canonical_version": "canon_ti_v1",
  "fingerprint": "canon_ti_v1:c2a1…",
  "signals": {
    "used": ["descriptor", "direction", "payment.rail", "location_hint.country", "occurred_at", "amount", "account_hint.iban", "external_transaction_id"],
    "ignored": ["payment.mcc", "payment.card_present"],
    "conflicting": []
  },
  "resolution": {
    "policy": { "outcome": "resolved", "archetype": "achat_carte", "missing": [], "policy_version": "ti_resolution_v1" },
    "counterparty": { "entity_id": "b83d1756-258c-4541-adc7-54eec83dbd3c", "name": "Monoprix", "kind": "brand" },
    "category": { "slug": "alimentation.supermarche", "label": "Supermarché", "source": "entity_graph" },
    "transaction_type": { "canonical": "card_purchase", "evidence": "rail_rule", "contradiction": null }
  },
  "relationships": [],
  "series": [],
  "relationships_policy": {
    "detector_version": "rel_v1",
    "calibration": "non_calibree",
    "unresolved": [{ "type": "series", "outcome": "single_occurrence", "occurrences": 1 }]
  }
}
JSON

Read these first:

  • signals.used and signals.ignored say which of the signals you sent were passed to the engine and which nobody read (here payment.mcc and payment.card_present).
  • resolution.policy.outcome is the verdict (resolved). Read that, not the numbers under confidence: declared constants, not probabilities (see Score semantics).
  • counterparty can be null: an honest unknown, not a failure, and counterparty_note says why.

A single occurrence does not make a series: relationships_policy.unresolved says so (single_occurrence). Every field is on Enrich a transaction.

Note

The Idempotency-Key header is optional. Same key and same request: the API replays the original response (200) instead of creating a second enrichment.

5. Read the enrichment back

GET/transactions/v1/enrichments/{enrichment_id}

Read back a returned enrichment.

Reference Transaction Intelligence

Parameters

  • enrichment_idstringpathrequired

Responses

200Found.StoredEnrichment
400Undeclared host. Never returned for the shape of the identifier.Problem
401Unusable credential.Problem
403Workspace or organisation inactive.Problem
404Malformed, unknown, expired, or belonging to another workspace: all four cases return the same response. Returning 400 on a malformed identifier would tell a caller that a well-formed enr_<uuid> "looks right": that is an oracle on shape, and it is closed.Problem
429rate_limited: the operational rate limit has been reached, for this key or for the workspace (the detail says which). Nothing was computed, nothing was stored, no event was emitted. Replay after Retry-After. See x-noryo-rate-limit.Problem
500Internal error.Problem

With the identifier received in step 4:

curl -sS https://api.noryo.app/transactions/v1/enrichments/enr_3f2504e0-4f89-41d3-9a0c-0305e82c3301 \
  -H "Authorization: Bearer $NORYO_API_KEY"
Shell
const enrichmentId = "enr_3f2504e0-4f89-41d3-9a0c-0305e82c3301";
const response = await fetch(
  `https://api.noryo.app/transactions/v1/enrichments/${enrichmentId}`,
  { headers: { Authorization: `Bearer ${process.env.NORYO_API_KEY}` } },
);

console.log(response.status); // 200
const stored = await response.json();
console.log(stored.expires_at);
TypeScript

The 200 response is a StoredEnrichment:

FieldContents
enrichment_id, created_atThe same as at creation.
expires_atAfter this date, reading back returns 404: an enrichment can be read back for 30 days.
resolutionThe resolution rendered at creation.
relationships, seriesWhat the workspace has learnt since: a refund that arrived later, one more occurrence in a series.

An identifier that is unknown, malformed, expired or from another workspace returns 404, the same response in all four cases. See Read an enrichment.

6. Declare an observation coverage window (optional)

POST/transactions/v1/coverage-windows

Declare what was supplied for an account, over a period

Reference Transaction Intelligence

Request bodyrequired

application/json · CoverageWindow

  • account_hintobjectrequired

    The account concerned. iban when it has one, otherwise external_account_id, your own identifier, for cards, wallets and non-SEPA rails. At least one of the two. Neither is stored: we keep only an opaque fingerprint, scoped to your workspace, the same one as on the enrichment, without which the coverage would never meet the series it qualifies.

  • ibanstring
    ^[A-Z]{2}[0-9]{2}[A-Z0-9]{10,30}$
  • external_account_idstring · length 1–128
  • covered_fromstring · daterequired
  • covered_tostring · daterequired

    We do not certify what has not happened yet: a future date is rejected.

  • completenessstring

    complete means something precise: you assert that all the operations on this account over this interval have been supplied to us. It is the only value that allows us to conclude from an absence. partial and unknown do not: ten calls in October may be ten operations out of a hundred.

    completepartialunknown

Responses

200Coverage recorded, or merged with a neighbour.CoverageWindowResult
400Unreadable body, or undeclared host.Problem
401Key missing, unknown or revoked.Problem
403Organisation or workspace inactive.Problem
413Body too large.Problem
415The body must be application/json.Problem
422Closed schema: unknown field, account missing or malformed, non-existent day, interval reversed or in the future, unknown completeness. errors[].code names the rule, and the list is the runtime's own, by construction: must_be_object, unknown_field, required, malformed_iban, must_be_non_empty_string, too_long, contains_control_character, account_identity_required, must_be_iso_date, after_covered_to, unknown_completeness, interval_in_future. must_be_iso_date covers form and calendar (2026-02-30 is rejected). interval_in_future: covered_to beyond tomorrow, by the server's date in UTC. provenance is not an input field: the closed schema rejects it as unknown_field, and the store never sees it.Problem
429rate_limited: the operational rate limit has been reached, for this key or for the workspace (the detail says which). Nothing was computed, nothing was stored, no event was emitted. Replay after Retry-After. See x-noryo-rate-limit.Problem
500Internal error.Problem

A coverage window is a fact about an account and a period, not about a transaction: you declare whether every operation on an account between two dates has been provided (completeness: complete, partial or unknown). Only complete lets Noryo draw a conclusion from an absence. Without it, a missing instalment proves nothing and a series' lifecycle stays unknown. The account is the enrichment's (account_hint).

curl -sS https://api.noryo.app/transactions/v1/coverage-windows \
  -H "Authorization: Bearer $NORYO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "account_hint": { "iban": "FR7630006000011234567890189" },
    "covered_from": "2026-07-01",
    "covered_to": "2026-07-31",
    "completeness": "complete"
  }'
Shell
const response = await fetch("https://api.noryo.app/transactions/v1/coverage-windows", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.NORYO_API_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    account_hint: { iban: "FR7630006000011234567890189" },
    covered_from: "2026-07-01",
    covered_to: "2026-07-31",
    completeness: "complete",
  }),
});

console.log(response.status, await response.json()); // 200
TypeScript

The contract's 200 response:

{
  "covered_from": "2026-07-01",
  "covered_to": "2026-07-31",
  "completeness": "complete",
  "provenance": "caller_asserted",
  "merged_intervals": 0,
  "policy_version": "cov_v1",
  "note": "",
  "request_id": "req_01J9…"
}
JSON

provenance is always caller_asserted: Noryo records your declaration without verifying it. Adjacent intervals are merged (merged_intervals); covered_to cannot be in the future. See Observation coverage.

7. Check usage on Platform

On https://platform.noryo.app/usage, Transaction Intelligence section, you read over 24 hours, 7 days or 30 days, per UTC day, per call family and per key: the total of authenticated requests, successes (2xx), client errors (4xx excluding 429), rate-limited requests (429, per key or per workspace) and server errors (5xx). A revoked key keeps its history; the register is kept for 35 days.

This is not a billing ledger: a 422 counts as a request, a 401 is never counted, and nothing is billed. A zero is distinct from "not observed" (before the register went into service) and from "unavailable". See Usage and API keys.

Headers and rate limits

Once the credential is accepted, every response carries noryo-request-id, the identifier to quote to support (on an error it is also in the body, as request_id), together with noryo-ratelimit-limit and noryo-ratelimit-remaining. On 429, Retry-After gives the seconds to wait. Rate limiting is an operational protection:

debit_ti_v1operational_burst_protection · token_bucket
120 / min
per key · capacity 120 · refill 2/s
600 / min
per workspace · capacity 600 · refill 10/s

« 120 requetes / minute » est traduit en seau a jetons : capacite 120 (la rafale toleree) rechargee a 2 jetons / seconde (le regime durable). Une clef au repos peut envoyer 120 requetes d'un coup, puis 2 par seconde ; sur la premiere minute apres un repos, jusqu'a 240 passent. Le workspace, lui, borne la somme de toutes ses clefs a 600 / minute (capacite 600, 10 / s) : cent clefs ne font pas cent fois la limite.

This is not: sandbox_lifetime_quota · production_usage_quota · billing_entitlement · commercial_promise

What next

Published on 2026-10-09 10:17 UTC (version 3).

Was this page helpful?