# 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.

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](https://developer.noryo.app/en-GB/transaction-intelligence/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`.

```bash
export NORYO_API_KEY="nry_txn_sbx_..."
```

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](https://developer.noryo.app/en-GB/transaction-intelligence/authentication).

## 4. First call: enrich a transaction

**`POST /transactions/v1/enrichments`** — Enrich a transaction.

#### Parameters

| Name | In | Required | Type | Description |
| --- | --- | --- | --- | --- |
| `Idempotency-Key` | header | — | string | 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. Pattern: `^[\x21-\x7E]{1,255}$`. |

#### Request body

`application/json` · required · Schema `TransactionInput`

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `descriptor` | string · length 1–512 | yes | — |
| `direction` | string | yes | Values: `incoming`, `outgoing`. |
| `occurred_at` | string · date-time | — | RFC 3339 with an offset. Never reduced to a date: midnight is not the same day everywhere. |
| `amount` | Money | — | — |
| `amount.value` | string | yes | 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). Pattern: `^-?(0\|[1-9][0-9]{0,15})(\.[0-9]{1,4})?$`. |
| `amount.currency` | string | yes | A currency outside this list is rejected with 422 `unknown_currency`. Values: `CAD`, `CHF`, `DKK`, `EUR`, `GBP`, `ISK`, `JPY`, `KWD`, `NOK`, `PLN`, `SEK`, `USD`. |
| `payment` | object | — | — |
| `payment.rail` | string · length ≤ 128 | — | — |
| `payment.mcc` | string | — | Pattern: `^[0-9]{4}$`. |
| `payment.card_present` | boolean | — | — |
| `payment.network_merchant_id` | string · length ≤ 128 | — | — |
| `payment.raw_type` | string · length ≤ 128 | — | — |
| `location_hint` | object | — | — |
| `location_hint.country` | string | — | Pattern: `^[A-Z]{2}$`. |
| `location_hint.city` | string · length ≤ 128 | — | — |
| `location_hint.postal_code` | string · length ≤ 128 | — | — |
| `counterparty_hint` | object | — | 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. |
| `counterparty_hint.iban` | string | — | Pattern: `^[A-Z]{2}[0-9]{2}[A-Z0-9]{10,30}$`. |
| `counterparty_hint.bic` | string | — | Pattern: `^[A-Z]{6}[A-Z0-9]{2}([A-Z0-9]{3})?$`. |
| `account_hint` | object | — | 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`. |
| `account_hint.iban` | string | — | Pattern: `^[A-Z]{2}[0-9]{2}[A-Z0-9]{10,30}$`. |
| `account_hint.external_account_id` | string · length 1–128 | — | — |
| `external_transaction_id` | string · 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. |

Example — A card purchase, with the account to link a coverage window

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

Example — A recurring SEPA direct debit

```json
{
  "descriptor": "PRLV SEPA NETFLIX INTERNATIONAL",
  "direction": "outgoing",
  "occurred_at": "2026-09-05T03:10:00+02:00",
  "amount": {
    "value": "13.49",
    "currency": "EUR"
  },
  "payment": {
    "rail": "sepa_direct_debit"
  },
  "account_hint": {
    "iban": "FR7630006000011234567890189"
  }
}
```

Example — A prepaid card: the account is named by your identifier

```json
{
  "descriptor": "APPLE.COM/BILL",
  "direction": "outgoing",
  "amount": {
    "value": "2.99",
    "currency": "EUR"
  },
  "account_hint": {
    "external_account_id": "carte-4970"
  }
}
```

#### Responses

| Status | Description |
| --- | --- |
| `200` | Already enriched: request replayed, nothing was created. `Location` is set here too: the client learns where the resource it did not create lives. |
| `201` | Enrichment created. |
| `400` | Unreadable body, or Idempotency-Key outside the alphabet. |
| `401` | Credential 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. |
| `403` | Workspace or organisation inactive. |
| `409` | This 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. |
| `413` | Body too large. |
| `415` | The body must be application/json. |
| `422` | Closed 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`. |
| `429` | `rate_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`. |
| `500` | Internal error. |

#### Errors named by the contract

- [`invalid_api_key`](https://developer.noryo.app/en-GB/transaction-intelligence/errors/invalid_api_key)
- [`account_inactive`](https://developer.noryo.app/en-GB/transaction-intelligence/errors/account_inactive)
- [`idempotency_conflict`](https://developer.noryo.app/en-GB/transaction-intelligence/errors/idempotency_conflict)
- [`request_entity_too_large`](https://developer.noryo.app/en-GB/transaction-intelligence/errors/request_entity_too_large)
- [`unsupported_media_type`](https://developer.noryo.app/en-GB/transaction-intelligence/errors/unsupported_media_type)
- [`invalid_input`](https://developer.noryo.app/en-GB/transaction-intelligence/errors/invalid_input)
- [`unknown_currency`](https://developer.noryo.app/en-GB/transaction-intelligence/errors/unknown_currency)
- [`rate_limited`](https://developer.noryo.app/en-GB/transaction-intelligence/errors/rate_limited)
- [`internal_error`](https://developer.noryo.app/en-GB/transaction-intelligence/errors/internal_error)

#### Rate limits

This operation consumes the rate limit `debit_ti_v1`.

`debit_ti_v1` — operational_burst_protection · token_bucket

- per key: 120 requests / minute (capacity 120, refill 2/s)
- per workspace: 600 requests / minute (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.

It is not: `sandbox_lifetime_quota`, `production_usage_quota`, `billing_entitlement`, `commercial_promise`.

→ [POST /transactions/v1/enrichments](https://developer.noryo.app/en-GB/transaction-intelligence/reference/create-enrichment.md)

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

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

```ts
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);
```

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`.

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

```ts
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);
```

### What the response contains

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

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

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](https://developer.noryo.app/en-GB/transaction-intelligence/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](https://developer.noryo.app/en-GB/transaction-intelligence/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.

#### Parameters

| Name | In | Required | Type | Description |
| --- | --- | --- | --- | --- |
| `enrichment_id` | path | yes | string | Pattern: `^enr_[0-9a-f-]{36}$`. |

#### Responses

| Status | Description |
| --- | --- |
| `200` | Found. |
| `400` | Undeclared host. Never returned for the shape of the identifier. |
| `401` | Unusable credential. |
| `403` | Workspace or organisation inactive. |
| `404` | Malformed, 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. |
| `429` | `rate_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`. |
| `500` | Internal error. |

#### Errors named by the contract

- [`invalid_api_key`](https://developer.noryo.app/en-GB/transaction-intelligence/errors/invalid_api_key)
- [`account_inactive`](https://developer.noryo.app/en-GB/transaction-intelligence/errors/account_inactive)
- [`not_found`](https://developer.noryo.app/en-GB/transaction-intelligence/errors/not_found)
- [`rate_limited`](https://developer.noryo.app/en-GB/transaction-intelligence/errors/rate_limited)
- [`internal_error`](https://developer.noryo.app/en-GB/transaction-intelligence/errors/internal_error)

#### Rate limits

This operation consumes the rate limit `debit_ti_v1`.

`debit_ti_v1` — operational_burst_protection · token_bucket

- per key: 120 requests / minute (capacity 120, refill 2/s)
- per workspace: 600 requests / minute (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.

It is not: `sandbox_lifetime_quota`, `production_usage_quota`, `billing_entitlement`, `commercial_promise`.

→ [GET /transactions/v1/enrichments/{enrichment_id}](https://developer.noryo.app/en-GB/transaction-intelligence/reference/get-enrichment.md)

With the identifier received in step 4:

```bash
curl -sS https://api.noryo.app/transactions/v1/enrichments/enr_3f2504e0-4f89-41d3-9a0c-0305e82c3301 \
  -H "Authorization: Bearer $NORYO_API_KEY"
```

```ts
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);
```

The `200` response is a `StoredEnrichment`:

| Field | Contents |
| --- | --- |
| `enrichment_id`, `created_at` | The same as at creation. |
| `expires_at` | After this date, reading back returns `404`: an enrichment can be read back for 30 days. |
| `resolution` | The resolution rendered at creation. |
| `relationships`, `series` | What 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](https://developer.noryo.app/en-GB/transaction-intelligence/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

Coverage is a fact about an account and a period, not about a transaction. It answers "have all the operations on this account between these two dates been supplied to you?", and it exists precisely when there is NO transaction to send: a month with no direct debit is the case where the declaration matters most. That is why it has its own resource rather than a field on the enrichment. Without it, we will never say that a series is late: a missing due date proves nothing as long as we do not know whether the account was being observed, and `lifecycle.state` stays `unknown`. And we verify nothing: what you declare is recorded as a caller assertion (`caller_asserted`). Provenance is not a field you choose. The call is idempotent by construction: intervals that touch are merged, so an hourly synchronisation does not manufacture intervals.

#### Request body

`application/json` · required · Schema `CoverageWindow`

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `account_hint` | object | yes | 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. |
| `account_hint.iban` | string | — | Pattern: `^[A-Z]{2}[0-9]{2}[A-Z0-9]{10,30}$`. |
| `account_hint.external_account_id` | string · length 1–128 | — | — |
| `covered_from` | string · date | yes | — |
| `covered_to` | string · date | yes | We do not certify what has not happened yet: a future date is rejected. |
| `completeness` | string | — | `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. Values: `complete`, `partial`, `unknown`. |

#### Responses

| Status | Description |
| --- | --- |
| `200` | Coverage recorded, or merged with a neighbour. |
| `400` | Unreadable body, or undeclared host. |
| `401` | Key missing, unknown or revoked. |
| `403` | Organisation or workspace inactive. |
| `413` | Body too large. |
| `415` | The body must be application/json. |
| `422` | Closed 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. |
| `429` | `rate_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`. |
| `500` | Internal error. |

#### Errors named by the contract

- [`invalid_api_key`](https://developer.noryo.app/en-GB/transaction-intelligence/errors/invalid_api_key)
- [`account_inactive`](https://developer.noryo.app/en-GB/transaction-intelligence/errors/account_inactive)
- [`request_entity_too_large`](https://developer.noryo.app/en-GB/transaction-intelligence/errors/request_entity_too_large)
- [`unsupported_media_type`](https://developer.noryo.app/en-GB/transaction-intelligence/errors/unsupported_media_type)
- [`rate_limited`](https://developer.noryo.app/en-GB/transaction-intelligence/errors/rate_limited)
- [`internal_error`](https://developer.noryo.app/en-GB/transaction-intelligence/errors/internal_error)

#### Rate limits

This operation consumes the rate limit `debit_ti_v1`.

`debit_ti_v1` — operational_burst_protection · token_bucket

- per key: 120 requests / minute (capacity 120, refill 2/s)
- per workspace: 600 requests / minute (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.

It is not: `sandbox_lifetime_quota`, `production_usage_quota`, `billing_entitlement`, `commercial_promise`.

→ [POST /transactions/v1/coverage-windows](https://developer.noryo.app/en-GB/transaction-intelligence/reference/declare-coverage-window.md)

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`).

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

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

The contract's `200` response:

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

`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](https://developer.noryo.app/en-GB/transaction-intelligence/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](https://developer.noryo.app/en-GB/transaction-intelligence/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_v1` — operational_burst_protection · token_bucket

- per key: 120 requests / minute (capacity 120, refill 2/s)
- per workspace: 600 requests / minute (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.

It is not: `sandbox_lifetime_quota`, `production_usage_quota`, `billing_entitlement`, `commercial_promise`.

## What next

- [Authentication](https://developer.noryo.app/en-GB/transaction-intelligence/authentication): key families, revocation, where to keep the secret.
- [Enrich a transaction](https://developer.noryo.app/en-GB/transaction-intelligence/enrich-a-transaction), [Read an enrichment](https://developer.noryo.app/en-GB/transaction-intelligence/read-an-enrichment), [Observation coverage](https://developer.noryo.app/en-GB/transaction-intelligence/observation-coverage).
- [Recurring series](https://developer.noryo.app/en-GB/transaction-intelligence/recurring-series) and [Relationships](https://developer.noryo.app/en-GB/transaction-intelligence/relationships): what several transactions reveal together.
- [Errors](https://developer.noryo.app/en-GB/transaction-intelligence/errors), [Rate limits](https://developer.noryo.app/en-GB/transaction-intelligence/rate-limits), [Sandbox limitations](https://developer.noryo.app/en-GB/transaction-intelligence/sandbox-limitations).
- The [reference](https://developer.noryo.app/en-GB/transaction-intelligence/reference) and the [error catalogue](https://developer.noryo.app/en-GB/transaction-intelligence/errors), generated from `https://api.noryo.app/openapi.json`.

---

Canonical page: https://developer.noryo.app/en-GB/transaction-intelligence/quickstart
