# Relire un enrichissement

> Relire un enrichissement par son enrichment_id pendant 30 jours, comprendre le 404 unique, et distinguer ce qui est recalculé à la lecture de ce qui reste immuable.

Chaque enrichissement créé par [`POST /transactions/v1/enrichments`](https://developer.noryo.app/fr-FR/transaction-intelligence/enrich-a-transaction) reçoit un `enrichment_id` de la forme `enr_<uuid>`. Cet identifiant permet de le relire pendant 30 jours sans renvoyer la transaction. La relecture n'est pas une copie figée de la réponse de création : la résolution ne bouge pas, mais les relations et les séries sont recalculées au moment où vous lisez, pour refléter ce que votre `workspace` a appris depuis.

## L'appel

**`GET /transactions/v1/enrichments/{enrichment_id}`** — Relire un enrichissement rendu.

#### Paramètres

| Nom | Dans | Requis | Type | Description |
| --- | --- | --- | --- | --- |
| `enrichment_id` | path | oui | string | Motif : `^enr_[0-9a-f-]{36}$`. |

#### Réponses

| Statut | Description |
| --- | --- |
| `200` | Trouvé. |
| `400` | Hôte non déclaré. Jamais rendu pour la forme de l'identifiant. |
| `401` | Credential inutilisable. |
| `403` | Workspace ou organisation inactif. |
| `404` | Malformé, inconnu, expiré, ou appartenant à un autre workspace — les quatre cas rendent la même réponse. Rendre 400 sur un identifiant malformé dirait qu'un `enr_<uuid>` bien formé « à la bonne tête » : c'est un oracle de forme, et il est fermé. |
| `429` | `rate_limited` : la limite opérationnelle de débit est atteinte, pour cette clef ou pour le workspace (le `detail` le dit). Rien n'a été calculé, rien n'a été rangé, aucun événement n'a été émis. Rejouez après `Retry-After`. Voir `x-noryo-rate-limit`. |
| `500` | Erreur interne. |

#### Erreurs nommées par le contrat

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

#### Limites de débit

Cette opération consomme la limite de débit `debit_ti_v1`.

`debit_ti_v1` — operational_burst_protection · token_bucket

- par clé : 120 requêtes / minute (capacité 120, recharge 2/s)
- par workspace : 600 requêtes / minute (capacité 600, recharge 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.

Ce n'est pas : `sandbox_lifetime_quota`, `production_usage_quota`, `billing_entitlement`, `commercial_promise`.

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

L'appel s'authentifie avec une clé de la famille `txn` dans l'en-tête `Authorization` (voir [Authentification](https://developer.noryo.app/fr-FR/transaction-intelligence/authentication)). Il n'a ni corps ni paramètre de requête.

```bash
curl -s "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 res = await fetch(
  `https://api.noryo.app/transactions/v1/enrichments/${enrichmentId}`,
  { headers: { Authorization: `Bearer ${process.env.NORYO_API_KEY}` } },
);

const requestId = res.headers.get("noryo-request-id");

if (res.status === 404) {
  // Malformé, inconnu, expiré ou d'un autre workspace : même réponse.
  const problem = await res.json();
  console.warn("enrichissement introuvable", problem.request_id ?? requestId);
} else if (res.ok) {
  const stored = await res.json();
  console.log(stored.expires_at, stored.series?.length ?? 0);
}
```

> **Note** La réponse de création porte un en-tête `Location` (sur le 201, et sur le 200 d'un rejeu idempotent) qui donne le chemin de la ressource. Conservez-le plutôt que de reconstruire l'URL.

## La réponse 200

Le corps est un `StoredEnrichment` (voir la [référence](https://developer.noryo.app/fr-FR/transaction-intelligence/reference)). Sa forme diffère de la réponse de création (`Enrichment`) : elle ajoute `expires_at`, et elle ne porte ni `contract_version`, ni `canonical_version`, ni `fingerprint`, ni `signals`, ni `relationships_policy`. Ces cinq champs ne se lisent qu'à la création.

**Schéma `StoredEnrichment`**

| Champ | Type | Requis | Description |
| --- | --- | --- | --- |
| `enrichment_id` | string | oui | — |
| `created_at` | string · date-time | oui | — |
| `expires_at` | string · date-time | oui | La réponse est opérationnelle et bornée : ce n'est pas un corpus. |
| `resolution` | Resolution | oui | La forme rendue par le moteur `ti_v4`. |
| `resolution.policy` | object | — | Ce que « RÉSOLU » veut dire dépend de la nature de la transaction. Un virement à une personne est `resolved` sans qu'aucune identité ne soit créée — c'est la bonne réponse, pas un échec. Un retrait d'espèces est `resolved` sans marchand. Un achat carte, lui, exige sa contrepartie. |
| `resolution.policy.outcome` | string | oui | `resolved` : toutes les dimensions exigées par l'archétype sont là. `partial` : la nature est connue, une dimension exigée manque — ou des signaux se contredisent, auquel cas on refuse de conclure. `unresolved` : on ignore jusqu'à la nature. Les erreurs techniques (401, 409, 503) ne sont jamais une issue sémantique : elles ne portent pas ce champ. Valeurs : `resolved`, `partial`, `unresolved`. |
| `resolution.policy.archetype` | string | oui | Le vocabulaire est français et c'est celui de la base (le vocabulaire des archétypes de résolution), conservé tel quel pour `ti_v1`. `inconnu` : la nature même de la transaction est inconnue. `non_couvert` : la nature est connue mais aucune politique ne la couvre (`missing = [resolution_policy]`). Valeurs : `achat_carte`, `frais_bancaires`, `impot`, `indetermine`, `prelevement`, `remboursement`, `retrait_especes`, `revenu`, `transfert`, `transfert_personne`, `inconnu`, `non_couvert`. |
| `resolution.policy.missing` | array<string> | oui | Les dimensions exigées par l'archétype et absentes. |
| `resolution.policy.policy_version` | string | oui | Valeurs : `ti_resolution_v1`. |
| `resolution.normalized` | object | — | Ce que le normaliseur a lu. Des chaînes, pas des scores. |
| `resolution.normalized.descriptor` | string | — | — |
| `resolution.normalized.signal` | string | — | — |
| `resolution.normalized.useful` | string | — | — |
| `resolution.normalized.residual` | string | — | — |
| `resolution.normalized.normalizer_version` | string | — | — |
| `resolution.counterparty` | object \| null | — | `null` est un résultat, pas une panne : un inconnu honnête. |
| `resolution.counterparty.entity_id` | string · uuid | — | L'identifiant stable de l'entité dans le graphe, sous forme d'UUID nu, sans préfixe. Il est opaque : ne lui prêtez aucune structure. Le même identifiant désigne la même entité d'une réponse à l'autre et d'un workspace à l'autre (le graphe est partagé ; l'identifiant n'est pas une donnée de votre workspace). Il est cité tel quel dans `series[].evidence` sous la forme `entite:<uuid>`. |
| `resolution.counterparty.name` | string | — | — |
| `resolution.counterparty.kind` | string | — | Valeurs : `brand`, `organisation`. |
| `resolution.counterparty.evidence` | object | — | Comment le motif a apparié le libellé. Deux rapports de longueur, qui sont des propriétés du texte et pas une preuve d'identité. `match` dit par quelle voie (`jetons` : un alias entier dans le libellé ; `tronque` : un fragment de libellé coupé par la banque). `pattern` est l'alias apparié, en clair. |
| `resolution.counterparty.evidence.match` | string | — | Valeurs : `jetons`, `tronque`. |
| `resolution.counterparty.evidence.pattern` | string | — | — |
| `resolution.counterparty.evidence.coverage` | number ≥ 0 | — | — |
| `resolution.counterparty.evidence.covered` | number · 0.6–1 | — | — |
| `resolution.counterparty.semantics` | CounterpartySemantics \| null | — | Quatre choses différentes qui se ressemblent, et que ce bloc refuse de confondre. Le type d'entité (`counterparty.kind` : marque, entité légale, intermédiaire de paiement) dit ce que c'est. L'activité (`activity`) dit ce que ça fait, en noms d'industrie. La catégorie (`category`, hors de cet objet) dit à quoi l'argent est allé, en lignes de budget. Le type de transaction (`transaction_type`) dit ce qui s'est passé. Aucun ne se déduit d'un autre. Et il n'existe aucun `category = subscription` : un abonnement n'est pas une sorte de dépense, c'est un modèle de relation commerciale. `null` quand le graphe ne sait rien de cette entité — une abstention, pas une panne. |
| `resolution.counterparty.semantics.activity` | object \| null | — | Le métier, sur deux niveaux (`parent` puis `code`). On s'arrête au parent quand la source s'y arrête : un rôle `utility` dit « réseau » sans dire lequel, et on rend alors `utilities`, jamais `utilities.electricity`. Ce qui n'est pas su ne se complète pas. |
| `resolution.counterparty.semantics.activity.code` | string | — | — |
| `resolution.counterparty.semantics.activity.label` | string | — | — |
| `resolution.counterparty.semantics.activity.parent` | string \| null | — | — |
| `resolution.counterparty.semantics.recurring_model` | string \| null | — | Ce qui est habituel dans ce métier — pas un verdict sur cette transaction. Un service de streaming se vend par abonnement ; un fournisseur d'électricité facture de façon répétée sans qu'on s'abonne ; un supermarché vend à l'acte. C'est une plausibilité qui renforce un rythme déjà observé dans le workspace. Seul, il ne conclut jamais : `commonly_subscription_based` sur une transaction isolée ne fait pas un abonnement, et c'est la série (`series[].type = subscription`) qui tranche. Valeurs : `commonly_subscription_based`, `commonly_recurring_but_not_subscription`, `usually_one_off`, `both_recurring_and_one_off`, `unknown`, `null`. |
| `resolution.counterparty.semantics.roles` | array<string> | — | Les rôles portés par l'entité dans le graphe (`merchant`, `utility`, `government`, `financial_institution`, `payment_intermediary`…). Un rôle n'est pas un métier, mais il en dit assez pour en dériver un quand aucun catalogue ne couvre l'entité. |
| `resolution.counterparty.semantics.legal_entity` | object \| null | — | Une marque n'est pas une entité légale. « Netflix » est ce qu'on lit sur le relevé ; « Netflix Inc » est ce qui contracte. Les deux existent séparément dans le graphe, reliés par `brand_of`, parce que les fusionner ferait disparaître le jour où une marque change de propriétaire. `null` quand aucune société mère n'est connue — pas quand il n'y en a pas. |
| `resolution.counterparty.semantics.legal_entity.entity_id` | string · uuid | — | Le même espace d'identifiants que `counterparty.entity_id` : UUID nu, opaque, stable. |
| `resolution.counterparty.semantics.legal_entity.name` | string | — | — |
| `resolution.counterparty.semantics.provenance` | object \| null | — | D'où vient ce qu'on affirme. `nature` classe la connaissance par autorité déclarée, dans cet ordre : `curated` (une personne l'a vérifié), `deterministic` (dérivé mécaniquement d'un fait déjà tenu), `provider` (un tiers l'annonce), `inferred` (nous l'avons déduit), `participant` (un seul client l'a dit). Cet ordre est déclaré, pas déduit d'un nombre : sans lui, un 0,95 déduit l'emporterait sur un 0,90 vérifié. `strength` ne départage qu'à nature égale, et n'est pas calibrée. Et aucune connaissance globale ne vient d'un seul client : ce qu'un workspace observe reste dans ce workspace. Une nature `participant` ne peut jamais devenir une vérité du graphe partagé. |
| `resolution.counterparty.semantics.provenance.nature` | string | — | Valeurs : `curated`, `deterministic`, `provider`, `inferred`, `participant`. |
| `resolution.counterparty.semantics.provenance.source` | string | — | — |
| `resolution.counterparty.semantics.provenance.strength` | number · 0–1 | — | — |
| `resolution.counterparty.semantics.provenance.calibration` | string | — | Toujours `non_calibree` dans cette version, et c'est une contrainte, pas une convention : la valeur est un vocabulaire en base, les colonnes qui la portent y font référence, et une garde refuse tout littéral hors vocabulaire chez chaque producteur. Le jour où une calibration réelle existe, elle entrera par une migration déclarée, avec le corpus qui la défend. Valeurs : `non_calibree`. |
| `resolution.counterparty.semantics.provenance.evidence` | object | — | Comment la provenance a été établie. Les clefs varient : `derivation` (`categorie_de_depense_vers_activite`, `role_vers_activite`…), `categorie_source` ou `role`, et `note`, une phrase constante en français. Aucune de ces valeurs n'est un nombre. |
| `resolution.counterparty_type` | string | — | — |
| `resolution.counterparty_note` | string \| null | — | Dit pourquoi aucune identité n'a été établie. |
| `resolution.category` | object \| null | — | Une catégorie n'est pas une identité. Quand le libellé désigne un métier (« BOULANGERIE DUPONT », « PHARMACIE DE LA GARE ») et non une enseigne connue, la réponse rend ce métier ici et laisse `counterparty` à `null`. Rendre « Boulangerie » comme contrepartie reviendrait à nommer un rayon à la place d'une entreprise. Et c'est une ligne de budget, pas un métier. Cette catégorie répond à « à quoi cet argent est-il allé », du point de vue de la personne. Ce que l'entité fait est un autre champ, `counterparty.semantics.activity`, et les deux ne se substituent pas : EDF vend de l'électricité (`activity = utilities.electricity`) et la dépense se range sous `logement.electricite`. `source` dit d'où vient la ligne : `entity_graph` quand elle est portée par l'enseigne reconnue, `generic_trade_entry` quand elle est déduite d'un mot de métier lu dans le libellé. La première est bornée par `confidence.entity` — une enseigne mal reconnue ne donne pas une catégorie sûre. Quand une règle de votre organisation a posé la catégorie (voir `customization`), `source` est absent : `slug` et `label` viennent alors du catalogue Noryo, et aucune des deux origines Noryo ne serait vraie. |
| `resolution.category.slug` | string | — | — |
| `resolution.category.label` | string \| null | — | — |
| `resolution.category.source` | string | — | Valeurs : `entity_graph`, `generic_trade_entry`. |
| `resolution.identity_abstention` | string \| null | — | Pourquoi le moteur a refusé de nommer, alors qu'il aurait pu. `trade_not_identity` : un métier a été reconnu, pas une enseigne. `atm_operator_not_counterparty` : sur un mouvement d'espèces, la banque reconnue exploite le distributeur — elle n'est pas en face. `account_bank_not_counterparty` : sur un frais bancaire, c'est la banque du compte, et la nommer n'apprend rien. `person_named_not_brand` : le libellé nomme une personne, et l'enseigne reconnue appartient au motif du virement. Une abstention n'est pas une panne : `policy.outcome` peut rester `resolved` si l'archétype n'exigeait pas d'entité. |
| `resolution.intermediaries` | array<object> | — | Les facilitateurs de paiement lus dans le libellé (`PAYPAL *NETFLIX` en donne un). Ils ne sont jamais la contrepartie. `evidence` est un code et une source — plus jamais le motif d'appariement lui-même, qui est une règle interne. |
| `resolution.intermediaries[].name` | string | oui | — |
| `resolution.intermediaries[].role` | string | oui | — |
| `resolution.intermediaries[].entity_id` | string · uuid \| null | — | L'entité du facilitateur dans le graphe, quand elle y est. |
| `resolution.intermediaries[].carries_sub_merchant` | boolean | — | — |
| `resolution.intermediaries[].evidence` | object | oui | — |
| `resolution.intermediaries[].evidence.code` | string | oui | Le code du facilitateur au référentiel. |
| `resolution.intermediaries[].evidence.source` | string | oui | Valeurs : `referentiel_facilitateurs`. |
| `resolution.transaction_type` | object | — | Ce que la transaction signifie, par opposition à `payment` qui dit comment l'argent a circulé. `evidence` est un code de règle, pas une explication : la note interne de la règle sortait ici dans une version antérieure ; les réponses créées alors portent `legacy_note_redacted`. |
| `resolution.transaction_type.canonical` | string | oui | — |
| `resolution.transaction_type.evidence` | string | oui | `rail_rule` : le rail seul a décidé. `descriptor_rule` : le libellé seul. `descriptor_sense_rail_mechanism` : le libellé donne le sens, le rail le mécanisme, sans conflit. `rail_and_descriptor_agree` : les deux disent la même chose. `descriptor_refines_rail` / `rail_refines_descriptor` : l'un précise l'autre. `rail_descriptor_conflict` : contradiction, aucune certitude fabriquée (voir `signal_conflicts`). `no_rule` : rien ne s'applique. Le suffixe `+counterparty_nature` dit que la nature de la contrepartie a affiné le type. Motif : `^(rail_rule\|descriptor_rule\|descriptor_sense_rail_mechanism\|rail_and_descriptor_agree\|descriptor_refines_rail\|rail_refines_descriptor\|rail_descriptor_conflict\|no_rule)(\+counterparty_nature)?$\|^legacy_note_redacted$`. |
| `resolution.transaction_type.contradiction` | string \| null | oui | — |
| `resolution.payment` | object \| null | — | Ce que le moteur a compris du rail et du mécanisme de paiement. |
| `resolution.payment.rail` | string \| null | — | — |
| `resolution.payment.mechanism` | string \| null | — | — |
| `resolution.payment.note` | string \| null | — | Une phrase explicative constante, en français, qui rappelle la différence entre `rail` et `transaction_type`. Pas une donnée. |
| `resolution.signal_conflicts` | array<SignalConflict> | — | Les signaux qui se contredisent. Quand il y en a, `policy.outcome` est `partial` : on refuse de conclure. |
| `resolution.signal_conflicts[].dimension` | string | oui | — |
| `resolution.signal_conflicts[].signals` | array<string> | oui | — |
| `resolution.signal_conflicts[].resolution` | string | oui | Une phrase, en français : comment le conflit a été arbitré. |
| `resolution.signal_conflicts[].resolved_to` | string | oui | — |
| `resolution.submerchant_lane` | string | — | L'issue de la voie « sous-marchand » (un facilitateur porte l'enseigne réelle). `none` : voie non ouverte. `no_candidate` : ouverte, aucune entité. `resolved` : une entité unique. `ambiguous` : plusieurs, abstention. Absent sur les réponses les plus anciennes, créées avant que cette voie existe. Valeurs : `none`, `no_candidate`, `resolved`, `ambiguous`. |
| `resolution.truncated_lane` | string | — | L'issue de la voie « libellé tronqué » (la banque a coupé le nom). Mêmes valeurs que `submerchant_lane`. Absent sur les réponses les plus anciennes, créées avant que cette voie existe. Valeurs : `none`, `no_candidate`, `resolved`, `ambiguous`. |
| `resolution.confidence` | object | — | Le nom est conservé pour `ti_v1` ; ce n'est pas une confiance. Chacun de ces nombres est une constante déclarée — posée par la personne qui a écrit le motif ou la règle, et lue telle quelle. Toute transaction qui touche le même motif reçoit le même nombre. Mesuré sur 644 libellés : la précision en fonction du seuil n'est pas monotone — 0,85 donne 0,86 de précision, 0,88 donne 0,33, 0,92 donne 0,25, 0,97 donne 0,79. Ce nombre n'ordonne pas le juste avant le faux. Ne l'utilisez ni comme probabilité ni comme seuil : utilisez `policy.outcome`. `0` a un sens précis par champ : abstention (`entity`), rôle inconnu (`counterparty`), conflit sans ancêtre commun (`transaction_type`). `location` vaut toujours `0` : rien n'est résolu sur cet axe dans cette version. |
| `resolution.confidence.entity` | number · 0–1 | oui | — |
| `resolution.confidence.counterparty` | number · 0–1 | oui | — |
| `resolution.confidence.transaction_type` | number · 0–1 | oui | — |
| `resolution.confidence.category` | number · 0–1 | oui | — |
| `resolution.confidence.location` | number | oui | Valeurs : `0`. |
| `resolution.confidence.calibration` | string | oui | Toujours `non_calibree` dans cette version, et c'est une contrainte, pas une convention : la valeur est un vocabulaire en base, les colonnes qui la portent y font référence, et une garde refuse tout littéral hors vocabulaire chez chaque producteur. Le jour où une calibration réelle existe, elle entrera par une migration déclarée, avec le corpus qui la défend. Valeurs : `non_calibree`. |
| `resolution.timings_ms` | object | — | Des durées, en millisecondes. Rien à voir avec une confiance. |
| `resolution.timings_ms.normalization` | number | — | — |
| `resolution.timings_ms.entity_resolution` | number | — | — |
| `resolution.timings_ms.submerchant` | number | — | — |
| `resolution.timings_ms.total` | number | — | — |
| `resolution.versions` | object | — | Les versions qui ont produit cette réponse. Un client qui conserve un nombre doit conserver la version à côté : la même valeur, sous une autre version, peut ne plus mesurer la même chose. |
| `resolution.versions.graph` | string | — | L'ontologie du graphe d'entités (`onto_v1`). |
| `resolution.versions.rules` | string | — | Les règles de type de transaction (`txtype_v1`). |
| `resolution.versions.engine` | string | oui | Le moteur de résolution (`ti_v4`). |
| `resolution.versions.resolution_policy` | string | oui | La même valeur que `policy.policy_version`. |
| `resolution.versions.calibration` | string | oui | Toujours `non_calibree` dans cette version, et c'est une contrainte, pas une convention : la valeur est un vocabulaire en base, les colonnes qui la portent y font référence, et une garde refuse tout littéral hors vocabulaire chez chaque producteur. Le jour où une calibration réelle existe, elle entrera par une migration déclarée, avec le corpus qui la défend. Valeurs : `non_calibree`. |
| `relationships` | array<Relationship> | — | — |
| `relationships[].type` | string | oui | `refund_of` : l'achat a eu lieu, puis l'argent est revenu — les deux écritures subsistent. `reversal_of` : l'écriture d'origine est effacée, il ne s'est rien passé. `transfer_pair` : deux jambes d'un seul mouvement. Valeurs : `refund_of`, `reversal_of`, `transfer_pair`. |
| `relationships[].role` | string | oui | Le rôle de cette transaction. `peer` pour une relation symétrique, où ni source ni cible ne veut rien dire. Valeurs : `source`, `target`, `peer`. |
| `relationships[].symmetric` | boolean | oui | — |
| `relationships[].counterpart` | string | oui | L'autre transaction, par son identifiant d'enrichissement. Motif : `^enr_[0-9a-f-]{36}$`. |
| `relationships[].match_strength` | object | oui | Ce n'est pas une probabilité. C'est la part des signaux corroborants disponibles pour ce type qui sont effectivement présents. `calibration` vaut `non_calibree` tant qu'aucune vérité terrain n'a certifié une lecture chiffrée de ce nombre. |
| `relationships[].match_strength.value` | number · 0.001–1 | oui | Le dénominateur dépend du type : 5 pour `refund_of`, 4 pour `reversal_of`, 2 pour `transfer_pair`. Un `0.5` de paire de virement (1/2) et un `0.5` de contre-passation (2/4) ne sont pas la même force. Lisez `corroborations` à côté. `present` ne compte que les signaux de l'ensemble propre au type : un marqueur de remboursement sur une paire de virement reste visible dans `evidence` mais ne corrobore pas. |
| `relationships[].match_strength.calibration` | string | oui | Toujours `non_calibree` dans cette version, et c'est une contrainte, pas une convention : la valeur est un vocabulaire en base, les colonnes qui la portent y font référence, et une garde refuse tout littéral hors vocabulaire chez chaque producteur. Le jour où une calibration réelle existe, elle entrera par une migration déclarée, avec le corpus qui la défend. Valeurs : `non_calibree`. |
| `relationships[].match_strength.corroborations` | object | oui | — |
| `relationships[].match_strength.corroborations.present` | integer ≥ 0 | oui | — |
| `relationships[].match_strength.corroborations.possible` | integer ≥ 1 | oui | — |
| `relationships[].evidence` | array<object> | oui | Les signaux qui fondent la relation, y compris les préconditions. Une relation qui ne peut pas dire pourquoi elle existe n'est pas créée. |
| `relationships[].evidence[].code` | string | oui | — |
| `relationships[].evidence[].value` | string | — | — |
| `relationships[].evidence[].value_seconds` | integer | — | Pour `within_window` : l'écart mesuré, en secondes. Une durée, pas une confiance. |
| `relationships[].window_seconds` | integer \| null | — | La fenêtre du type de relation, en secondes. Une constante de politique, pas une mesure. |
| `relationships[].detector_version` | string | oui | — |
| `relationships[].status` | string | oui | Valeurs : `active`, `supersedee`, `invalidee`. |
| `relationships[].created_at` | string · date-time | — | — |
| `series` | array<Series> | — | — |
| `series[].series_id` | string | oui | Stable. Il se dérive de ce qui ne change pas — workspace, famille, contrepartie, sens, devise — jamais de la dernière occurrence ni du libellé courant. Une occurrence de plus ne crée pas une autre série. Motif : `^ser_[0-9a-f-]{36}$`. |
| `series[].type` | string | oui | `recurring_payment` dit « ce paiement revient », et rien de plus : un loyer et une échéance de prêt le sont aussi. `subscription` est une affirmation plus forte et exige son propre signal ; sans lui, la réponse reste `recurring_payment`. De même `recurring_income` couvre pension, prestation et loyer perçu — `salary_series` en est une spécialisation, pas un synonyme. Valeurs : `recurring_payment`, `subscription`, `recurring_income`, `salary_series`. |
| `series[].implies` | array<string> | — | Le type plus général que celui-ci spécialise, s'il existe. |
| `series[].status` | string | oui | Deux occurrences font émerger, trois peuvent établir, une seule jamais. `broken` se calcule au moment de la lecture : une série devient en retard par le seul passage du temps, sans qu'aucune écriture n'ait lieu. Valeurs : `emerging`, `established`, `uncertain`, `broken`. |
| `series[].cadence` | string | oui | Lue au calendrier, pas en jours secs : le 31 janvier et le 28 février sont à un mois d'intervalle, et un salaire au dernier jour ouvré glisse. Valeurs : `weekly`, `biweekly`, `monthly`, `quarterly`, `yearly`, `irregular_recurring`, `unknown`. |
| `series[].occurrence_count` | integer | oui | — |
| `series[].amount_behavior` | object | oui | L'égalité exacte n'est pas exigée : une facture d'électricité varie, un salaire aussi. Montants en unités mineures, en chaîne — jamais un nombre JSON. |
| `series[].amount_behavior.kind` | string | — | Valeurs : `fixed`, `narrow_variable`, `broadly_variable`. |
| `series[].amount_behavior.min_minor_units` | string | — | — |
| `series[].amount_behavior.max_minor_units` | string | — | — |
| `series[].amount_behavior.currency` | string | — | — |
| `series[].first_seen` | string · date-time | — | — |
| `series[].last_seen` | string · date-time | — | — |
| `series[].expected_next_at` | string · date-time \| null | — | Facultative, et c'est le point. Absente tant que la série n'est pas établie ou que la cadence est irrégulière : rendre une date sur deux observations faibles la ferait passer pour une prévision. Ce n'est pas une API de prévision. |
| `series[].match_strength` | object | oui | Ce n'est pas une probabilité : c'est la part des signaux corroborants présents sur ceux que ce type sait produire. Et ce n'est pas le même nombre que `relationships[].match_strength` : même nom, autre dénominateur, autres signaux. Cinq critères hétérogènes sur cinq — cadence calendaire, montant fixe ou étroit, au moins trois occurrences, entité connue, type spécialisé. Ils ne s'additionnent qu'ici. |
| `series[].match_strength.value` | number · 0.001–1 | oui | — |
| `series[].match_strength.calibration` | string | oui | Toujours `non_calibree` dans cette version, et c'est une contrainte, pas une convention : la valeur est un vocabulaire en base, les colonnes qui la portent y font référence, et une garde refuse tout littéral hors vocabulaire chez chaque producteur. Le jour où une calibration réelle existe, elle entrera par une migration déclarée, avec le corpus qui la défend. Valeurs : `non_calibree`. |
| `series[].match_strength.corroborations` | object | oui | — |
| `series[].match_strength.corroborations.present` | integer | oui | — |
| `series[].match_strength.corroborations.possible` | integer | oui | Valeurs : `5`. |
| `series[].evidence` | array<object> | — | Les signaux qui fondent la série. `value` est une chaîne ou un entier selon le code (`occurrences` porte un entier). |
| `series[].evidence[].code` | string | oui | — |
| `series[].evidence[].value` | string \| integer \| null | — | — |
| `series[].lifecycle` | SeriesLifecycle | — | Un axe différent de `status`. `status` dit à quel point nous croyons que c'est une série ; `lifecycle` dit ce qui lui arrive maintenant. Une série très bien établie peut avoir cessé de prélever, et une série fraîche peut être parfaitement vivante. Et aucun état ne se conclut d'une absence sans couverture : si la source n'était pas observée de façon complète sur la fenêtre attendue, l'état est `unknown`, pas `late`. « Je n'ai rien vu » ne devient jamais « il n'y a rien eu » — c'est la différence entre un client qui a cessé d'envoyer et un abonnement qui a cessé de prélever. |
| `series[].lifecycle.state` | string | oui | `active` : l'échéance attendue est arrivée, ou n'est pas encore échue. `late` : une échéance manque ET le compte était observé. `inactive` : plusieurs échéances consécutives manquent sous observation complète — la série ne prélève plus, sans que nous sachions pourquoi. `ended` : réservé à une preuve terminale explicite. Aucune accumulation d'absences ne produit cet état : un prélèvement qui s'arrête peut être une résiliation, une suspension, une carte expirée ou un échec de prélèvement, et nous ne devinons pas laquelle. `unknown` : nous ne pouvons pas conclure, et `reason` dit pourquoi. Valeurs : `active`, `late`, `inactive`, `ended`, `unknown`. |
| `series[].lifecycle.reason` | string | — | — |
| `series[].lifecycle.last_observed_on` | string · date \| null | — | — |
| `series[].lifecycle.expected_next_window` | object \| null | — | Une fenêtre, pas une date. Un salaire tombe vers les derniers jours ouvrés, un prélèvement SEPA vers le 5. Rendre un instant exact serait une précision que nous n'avons pas, et vous la croiriez. |
| `series[].lifecycle.expected_next_window.from` | string · date | — | — |
| `series[].lifecycle.expected_next_window.to` | string · date | — | — |
| `series[].lifecycle.expected_next_window.tolerance_days` | integer | — | La demi-largeur de la fenêtre, en jours. Plus grand = moins précis. Vient du type d'ancrage, relevé au plancher de la famille de série. |
| `series[].lifecycle.expected_next_window.anchor` | string | — | Le type d'ancrage qui a donné la fenêtre. |
| `series[].lifecycle.expected_next_window.widened_to_family_floor` | boolean | — | `true` si la tolérance a été relevée au plancher de la famille (5 jours pour un abonnement, 6 pour un salaire, 7 sinon). |
| `series[].lifecycle.consecutive_misses` | integer \| null | — | Le compteur n'avance que sous couverture complète. Absent quand rien n'est attendu — ce qui n'est pas la même ignorance que « nous ne regardions pas ». |
| `series[].lifecycle.observation_coverage` | string | — | `not_required` : aucune échéance close, rien n'avait à être observé. `complete` : chaque source de la série était couverte sur la fenêtre attendue — un manque est un manque. `incomplete` : au moins une source ne l'était pas — l'absence ne prouve rien, `state` est `unknown`. `unknown` : les sources sont inconnues ou trop nombreuses pour être vérifiées. Valeurs : `not_required`, `complete`, `incomplete`, `unknown`. |
| `series[].lifecycle.cadence_strength` | number \| null | — | Le même nombre que `cadence_detail.strength`, rendu ici pour que le cycle de vie soit lisible seul. Ce n'est pas un second score. |
| `series[].lifecycle.calendar_anchor` | any | — | Le même objet que `calendar_pattern` (classe `anchor_stability`), rendu ici pour la même raison ; `{ type: unknown }` quand aucun ancrage n'a été inféré. Absent quand aucune échéance n'est attendue. duplicate_of : `series[].calendar_pattern` — ce n'est pas un second score. |
| `series[].lifecycle.evidence` | array<object> | — | Présent pour `ended` : la preuve terminale qui l'a produit. |
| `series[].lifecycle.evidence[].code` | string | — | — |
| `series[].lifecycle.identity_status` | string \| null | — | Présent uniquement quand l'identité de la série a été corrigée, pour dire pourquoi `state` vaut `unknown` : ce n'est pas une ignorance économique, c'est que le verdict appartient désormais aux successeurs. L'état d'identité complet se lit dans `identity`, qui est un axe à part entière ; ce champ n'est qu'un renvoi, et il ne prend jamais la valeur `current`. Valeurs : `superseded`, `null`. |
| `series[].lifecycle.policy_version` | string | oui | — |
| `series[].lifecycle.calibration` | string | oui | Toujours `non_calibree` dans cette version, et c'est une contrainte, pas une convention : la valeur est un vocabulaire en base, les colonnes qui la portent y font référence, et une garde refuse tout littéral hors vocabulaire chez chaque producteur. Le jour où une calibration réelle existe, elle entrera par une migration déclarée, avec le corpus qui la défend. Valeurs : `non_calibree`. |
| `series[].identity` | SeriesIdentity | — | Un troisième axe, et il ne se mélange ni à `status` NI À `lifecycle`. `status` dit à quel point nous croyons que c'est une série ; `lifecycle` dit ce qui lui arrive économiquement ; `identity` dit si son nom est toujours le bon. Une série peut prélever tous les mois et avoir changé d'identité parce que nous avons compris qu'elle était en réalité deux flux, ou deux moitiés d'un seul. ET UN `series_id` n'est jamais réécrit ni réutilisé en silence. Quand nous corrigeons, l'ancien nom subsiste, marqué `superseded`, et il porte ses successeurs. |
| `series[].identity.status` | string | oui | `current` : ce nom est le nom courant de ce flux. `superseded` : ce nom a été remplacé par une correction d'identité. Il reste valide comme référence historique — il ne disparaîtra pas — mais les occurrences futures porteront le nom de ses successeurs. Valeurs : `current`, `superseded`. |
| `series[].identity.superseded_by` | object \| null | — | Après une scission il n'y a pas de redirection unique, et c'est pourquoi ce champ rend une liste. Si nous découvrons qu'une série mélangeait deux flux, aucun des deux successeurs n'est « la suite » de l'ancien : la moitié des occurrences de l'ancien n'est pas dans l'un d'eux. Élire un canonique serait confortable et faux. |
| `series[].identity.superseded_by.reason` | string | — | `split` : cette identité en recouvrait plusieurs, et chacune a désormais la sienne. `merge` : cette identité et une autre étaient le même flux ; toutes deux sont remplacées par une identité neuve. Valeurs : `split`, `merge`. |
| `series[].identity.superseded_by.at` | string · date-time | — | — |
| `series[].identity.superseded_by.evidence` | array<string> | — | Les codes de preuve qui ont autorisé la correction. Jamais un score interne, jamais un identifiant de ligne. |
| `series[].identity.superseded_by.successor_series_ids` | array<string> | — | Un seul après une fusion, plusieurs après une scission. Jamais vide quand `status` vaut `superseded`. |
| `series[].identity.supersedes` | array<string> | oui | Le chemin de retour, et il compte autant que l'autre. Les identités que celle-ci remplace. Sans lui, l'ancien `series_id` que vous aviez stocké ne correspondrait plus à rien de ce que vous recevez, et vous n'auriez aucun moyen de faire le lien. Vide pour une série qui n'est née d'aucune correction. |
| `series[].membership` | SeriesMembership | — | D'où vient cette occurrence, et où est-elle maintenant. Quand une identité est corrigée, l'occurrence change de série : sans ce bloc, vous n'auriez aucun moyen de relier le `series_id` que vous aviez stocké à celui que vous recevez. Vous n'avez jamais à reconstruire le graphe vous-même. |
| `series[].membership.original_series_id` | string \| null | — | La série à laquelle cette occurrence a été rattachée la première fois. Égale à `current_series_id` tant qu'aucune correction n'a eu lieu. Motif : `^ser_[0-9a-f-]{36}$`. |
| `series[].membership.current_series_id` | string \| null | oui | Motif : `^ser_[0-9a-f-]{36}$`. |
| `series[].membership.corrections` | array<object> | oui | Les déplacements successifs, du plus ancien au plus récent. Vide pour une occurrence qui n'a jamais bougé. |
| `series[].membership.corrections[].reason` | string | — | Valeurs : `split`, `merge`. |
| `series[].membership.corrections[].from_series_id` | string \| null | — | Motif : `^ser_[0-9a-f-]{36}$`. |
| `series[].membership.corrections[].to_series_id` | string \| null | — | Motif : `^ser_[0-9a-f-]{36}$`. |
| `series[].membership.corrections[].at` | string · date-time | — | — |
| `series[].first_observed_on` | string · date | — | Le premier jour où **nous** Avons observé cette série — pas le jour où elle a commencé. Une série peut tourner depuis cinq ans alors que nous ne l'observons que depuis trois mois. Il n'existe volontairement aucun champ `started_at` : nous ne le savons pas. |
| `series[].last_observed_on` | string · date | — | — |
| `series[].may_predate_observation` | boolean | — | `true` : notre observation commence avec la première occurrence connue, donc la série a pu commencer avant nous et rien ne le montrera. `false` : la première occurrence connue est antérieure au début de notre observation, donc son commencement est observé et non supposé. |
| `series[].cadence_detail` | object | — | À quelle fréquence l'événement se répète — et rien d'autre. La cadence se déduit de la progression des périodes calendaires, pas des écarts en jours : trois mois consécutifs sont trois mois consécutifs, que les écarts vaillent 28, 30 ou 31. Et une récurrence de 28 jours n'est pas mensuelle : elle tombe une fois par mois pendant onze mois puis deux, et son jour recule à chaque cycle. Nous ne l'appelons pas `monthly` — cette version n'a pas de cadence pour elle, et `irregular_recurring` vaut mieux qu'un faux mensuel. `strength` est la part des cycles attendus qui portent une occurrence. Ce n'est pas une probabilité. |
| `series[].cadence_detail.type` | string | — | — |
| `series[].cadence_detail.strength` | number · 0–1 \| null | — | `0` = aucune hypothèse calendaire retenue, pas « faible ». |
| `series[].cadence_detail.calibration` | string | — | Toujours `non_calibree` dans cette version, et c'est une contrainte, pas une convention : la valeur est un vocabulaire en base, les colonnes qui la portent y font référence, et une garde refuse tout littéral hors vocabulaire chez chaque producteur. Le jour où une calibration réelle existe, elle entrera par une migration déclarée, avec le corpus qui la défend. Valeurs : `non_calibree`. |
| `series[].calendar_pattern` | CalendarPattern | — | Où, dans chaque période, la série tend à tomber — une question différente de `cadence`. Une série peut être franchement mensuelle en flottant entre le 1er et le 6 : sans ce champ, « monthly » se lirait comme « le même jour chaque mois », ce qui est faux pour la majorité des séries réelles. C'est aussi ce qui donne sa largeur à `expected_next_window` : un prélèvement toujours au 5 justifie trois jours, un loyer qui flotte en justifie huit. Appliquer la même fenêtre aux deux ferait déclarer « en retard » une série parfaitement normale. |
| `series[].calendar_pattern.type` | string | — | `fixed_day_of_month` : toujours autour du même jour. `month_end` : en fin de mois, quel que soit le nombre de jours — un prélèvement du 31 janvier, 28 février, 31 mars est un seul motif. `last_business_day` : le dernier jour ouvré, cas fréquent des salaires. `business_day_adjusted` : un jour nominal déplacé hors du week-end (samedi → vendredi, dimanche → lundi). Nous NE modélisons pas les jours fériés : ni français, ni target, ni bancaires. Un report de férié est absorbé par la tolérance de la fenêtre, pas reconnu — prétendre connaître un calendrier que nous n'avons pas serait pire que de l'ignorer. `fixed_weekday` : toujours le même jour de la semaine. `flexible_calendar_window` : une occurrence par période, sans jour stable. `unknown` : rien d'identifiable. Valeurs : `fixed_day_of_month`, `month_end`, `last_business_day`, `business_day_adjusted`, `fixed_weekday`, `flexible_calendar_window`, `unknown`. |
| `series[].calendar_pattern.nominal_day` | integer \| null | — | — |
| `series[].calendar_pattern.weekday` | integer \| null | — | — |
| `series[].calendar_pattern.day_range` | array<integer> \| null | — | — |
| `series[].calendar_pattern.weekend_shifts` | integer \| null | — | — |
| `series[].calendar_pattern.strength` | number · 0–1 | — | Une constante par type d'ancrage, ou une formule linéaire sur l'étendue des jours pour `fixed_day_of_month` et `flexible_calendar_window`. `0` pour `unknown`. Un 0,33 `fixed_day_of_month` et un 0,3 `flexible_calendar_window` ne se comparent pas — et `flexible_calendar_window` change de règle avec la cadence (constante 0,3 en hebdomadaire, `1 - etendue/10` en mensuel). |
| `series[].calendar_pattern.calibration` | string | — | Toujours `non_calibree` dans cette version, et c'est une contrainte, pas une convention : la valeur est un vocabulaire en base, les colonnes qui la portent y font référence, et une garde refuse tout littéral hors vocabulaire chez chaque producteur. Le jour où une calibration réelle existe, elle entrera par une migration déclarée, avec le corpus qui la défend. Valeurs : `non_calibree`. |
| `series[].history_coverage` | object | oui | Ce que nous pouvons voir, et ce que nous ne pouvons pas. `insufficient` signifie « notre mémoire est trop courte pour en juger », jamais « il n'y a pas d'occurrence plus ancienne ». Et il y a deux mémoires, qu'il ne faut pas confondre. `raw_days` (30) dit combien de temps la transaction reste lisible ; `derived_days` (400) dit combien de temps ce que nous avons appris d'une série lui survit. Une transaction purgée reste purgée — cette API ne la ressuscite jamais — mais la série se souvient qu'elle a eu lieu. C'est ce qui rend une cadence mensuelle confirmable alors que la transaction de janvier n'existe plus. `observed_days` est ce que nous avons effectivement vu : un client qui lit « monthly / sufficient » peut vérifier que la conclusion ne sort pas de trente jours. |
| `series[].history_coverage.observed_from` | string · date | — | — |
| `series[].history_coverage.observed_to` | string · date | — | — |
| `series[].history_coverage.raw_days` | integer | — | Durée de vie de la transaction brute. Passé ce délai elle est physiquement supprimée et rien ne la rend. |
| `series[].history_coverage.derived_days` | integer | — | Durée de vie de la mémoire de série, comptée depuis la dernière occurrence. Sans nouvelle occurrence, l'état dérivé est physiquement supprimé à l'échéance et une occurrence ultérieure ouvre une série neuve. |
| `series[].history_coverage.observed_days` | integer | — | Étendue réellement observée, de la première à la dernière occurrence connue. |
| `series[].history_coverage.retention_limit_days` | integer | oui | Conservé pour compatibilité : identique à `raw_days`. |
| `series[].history_coverage.occurrence_count` | integer | oui | — |
| `series[].history_coverage.cadence` | string | — | — |
| `series[].history_coverage.sufficiency` | string | oui | Répond à une seule question : peut-on croire la cadence ? Pas à « sait-on quand ça a commencé », qui est le champ suivant. Valeurs : `sufficient`, `limited`, `insufficient`. |
| `series[].history_coverage.reason` | string | — | — |
| `series[].history_coverage.first_occurrence_reason` | string | — | Dit pourquoi `first_occurrence_bounded` vaut ce qu'il vaut. |
| `series[].history_coverage.first_occurrence_bounded` | boolean | — | Une question différente de `sufficiency`, et qui était confondue avec elle. `true` : notre observation de cette série commence avec la première occurrence que nous connaissons — elle a donc pu commencer avant nous, et rien ne le montrera jamais. `false` : la première occurrence connue est antérieure au début de notre observation, donc le commencement est observé et non supposé. Un import d'historique rend `false` ; une série découverte au fil de l'eau rend `true`. |
| `series[].detector_version` | string | — | — |
| `customization` | Customization | — | Présent seulement quand une règle de votre organisation a changé cette réponse (Platform, Règles). Le résultat Noryo reste conservé intact ; une règle ne vaut que pour votre organisation et l'environnement de la clef. Un POST est rendu avec les règles publiées à cet instant (un rejeu idempotent rend le même corps) ; un GET applique les règles publiées aujourd'hui. Un champ sur lequel deux règles se contredisent garde la valeur Noryo. |
| `customization.rules` | array<object> | oui | — |
| `customization.rules[].id` | string · uuid | oui | — |
| `customization.rules[].version` | integer ≥ 1 | oui | — |
| `customization.rules[].fields` | array<string> | oui | Les champs que la règle a changés : `contrepartie.nom` (`resolution.counterparty.name`), `categorie` (`resolution.category`), `recurrence` (`resolution.counterparty.semantics.recurring_model`). |

| Champ | Contenu | Change entre deux lectures ? |
|---|---|---|
| `enrichment_id` | L'identifiant reçu à la création | Non |
| `created_at` | L'instant de création | Non |
| `expires_at` | L'instant à partir duquel la relecture rend 404 | Non |
| `resolution` | La résolution rendue à la création | Non |
| `relationships` | Les relations avec d'autres transactions du workspace | Oui, calculées à la lecture |
| `series` | Les séries auxquelles cette occurrence appartient | Oui, calculées à la lecture |

La réponse porte aussi les en-têtes `noryo-request-id` (à citer au support), `noryo-ratelimit-limit` et `noryo-ratelimit-remaining`. Extrait réduit aux champs d'identité, après une scission :

```json
{
  "enrichment_id": "enr_3f2504e0-4f89-41d3-9a0c-0305e82c3301",
  "created_at": "2026-09-05T01:10:00Z",
  "expires_at": "2026-10-05T01:10:00Z",
  "series": [
    {
      "series_id": "ser_6ba7b811-9dad-11d1-80b4-00c04fd430c8",
      "identity": {
        "status": "superseded",
        "superseded_by": {
          "reason": "split",
          "at": "2026-09-10T22:14:00Z",
          "successor_series_ids": [
            "ser_6ba7b812-9dad-11d1-80b4-00c04fd430c8",
            "ser_6ba7b810-9dad-11d1-80b4-00c04fd430c8"
          ]
        },
        "supersedes": []
      },
      "membership": {
        "original_series_id": "ser_6ba7b811-9dad-11d1-80b4-00c04fd430c8",
        "current_series_id": "ser_6ba7b810-9dad-11d1-80b4-00c04fd430c8",
        "corrections": [
          {
            "reason": "split",
            "from_series_id": "ser_6ba7b811-9dad-11d1-80b4-00c04fd430c8",
            "to_series_id": "ser_6ba7b810-9dad-11d1-80b4-00c04fd430c8",
            "at": "2026-09-10T22:14:00Z"
          }
        ]
      }
    }
  ]
}
```

## Le 404 unique

Quatre situations rendent exactement la même réponse `404`, de type `not_found` :

| Situation | Réponse |
|---|---|
| L'identifiant est malformé | 404 |
| L'identifiant est bien formé mais inconnu | 404 |
| L'enrichissement a dépassé `expires_at` | 404 |
| L'enrichissement appartient à un autre workspace | 404 |

Ce choix est délibéré : rendre un 400 sur un identifiant malformé dirait qu'un `enr_<uuid>` bien formé « a la bonne tête », et l'API refuse de servir d'oracle sur la forme de ses identifiants. Le seul 400 possible ici concerne l'hôte (hôte non déclaré), jamais l'identifiant. Si un enrichissement récent devrait exister, vérifiez que la clé appartient au workspace qui l'a créé.

Les autres codes sont ceux de toute l'API : `401` pour une credential inutilisable, `403` pour un workspace ou une organisation inactifs, `429` (`rate_limited`, avec `Retry-After`) quand la limite de débit est atteinte, `500` en cas d'erreur interne. Chacun rend un `Problem` avec un `request_id`. Voir [Erreurs](https://developer.noryo.app/fr-FR/transaction-intelligence/errors) et le [catalogue généré](https://developer.noryo.app/fr-FR/transaction-intelligence/errors).

## Ce qui peut changer entre la création et une relecture

`relationships` et `series` sont calculées à la lecture, pas figées à l'écriture. L'achat du lundi ignore le remboursement du vendredi au moment où il répond ; c'est la relecture qui le montre.

**Relations.** Une transaction enrichie plus tard peut créer une relation avec celle-ci. `relationships` porte alors le lien (`refund_of`, `reversal_of` ou `transfer_pair`), avec `role`, `counterpart` et `status`. Voir [Relations](https://developer.noryo.app/fr-FR/transaction-intelligence/relationships).

**Statut et cycle de vie d'une série.** La troisième occurrence d'hier a pu faire passer une série d'`emerging` à `established`, et la première transaction doit le refléter. `broken` se calcule aussi à la lecture : une série devient en retard par le seul passage du temps, sans qu'aucune écriture n'ait lieu. `lifecycle.state` suit la même logique, selon les échéances et la couverture d'observation déclarée. Voir [Séries récurrentes](https://developer.noryo.app/fr-FR/transaction-intelligence/recurring-series) et [Cycle de vie d'une série](https://developer.noryo.app/fr-FR/transaction-intelligence/series-lifecycle).

**Identité d'une série.** C'est le changement à prévoir si vous stockez des `series_id`. Une correction d'identité ne réécrit jamais un identifiant : l'ancienne série reste lisible, mais son bloc `identity` passe à `status = superseded`, et `superseded_by` donne `reason` (`split` ou `merge`), `at`, `evidence` et `successor_series_ids`. Après une scission, `successor_series_ids` en compte plusieurs : aucun successeur n'est « la suite » de l'ancien, et le contrat refuse d'en élire un. Après une fusion, il n'en compte qu'un. Chaque successeur porte en retour `supersedes`, la liste des identités qu'il remplace.

**Appartenance de l'occurrence.** Le bloc `membership` dit d'où vient cette occurrence et où elle est maintenant : `original_series_id` est la série de son premier rattachement, `current_series_id` celle où elle se trouve aujourd'hui, et `corrections` liste ses déplacements du plus ancien au plus récent, chacun avec `reason`, `from_series_id`, `to_series_id` et `at`. Sans correction, les deux identifiants sont égaux et `corrections` est vide. Vous n'avez jamais à reconstruire le graphe vous-même.

Quand une série a été remplacée, `lifecycle.identity_status` vaut `superseded` pour expliquer que `lifecycle.state` soit `unknown` : le verdict appartient désormais aux successeurs. Voir [Scission, fusion, supersession](https://developer.noryo.app/fr-FR/transaction-intelligence/split-merge-supersession).

> **Attention** Si vous stockez un `series_id`, relisez `membership.current_series_id` et `identity.supersedes` plutôt que de supposer que l'identifiant reçu au premier POST est toujours le courant.

## Ce qui ne change pas

`enrichment_id`, `created_at`, `expires_at` et `resolution` sont immuables : la relecture rend la résolution établie à la création. Si vous conservez un nombre de la résolution, conservez `resolution.versions` à côté : sous une autre version, la même valeur peut ne plus mesurer la même chose. Seule nuance de forme : sur les réponses les plus anciennes, `transaction_type.evidence` rend `legacy_note_redacted` à la place d'une note interne d'une version antérieure.

Un `series_id` n'est jamais réécrit ni réutilisé en silence. Une identité remplacée reste lisible comme référence historique.

Relecture et rejeu diffèrent. Un rejeu par `Idempotency-Key` sur le POST rend la réponse d'origine telle quelle, version de moteur comprise (200). Une relecture par GET rend la résolution d'origine, mais des relations et des séries calculées aujourd'hui.

## Rétention et débit

Un enrichissement est relisible 30 jours : c'est la valeur `retention_limit_days` (identique à `raw_days`) de `series[].history_coverage`, et `expires_at` donne l'échéance exacte de chacun. Passé ce délai, la transaction brute est supprimée et l'API ne la ressuscite jamais : la réponse est opérationnelle et bornée, pas un corpus. La mémoire dérivée d'une série lui survit (`derived_days`) : une cadence mensuelle reste confirmable quand la transaction de janvier n'existe plus. Voir [Limites du sandbox](https://developer.noryo.app/fr-FR/transaction-intelligence/sandbox-limitations).

Cette lecture compte dans les limites de débit opérationnelles, comme un POST. Un 404 rendu après une credential valide consomme un jeton ; un 401 n'en consomme aucun. Les valeurs sont dans [Limites de débit](https://developer.noryo.app/fr-FR/transaction-intelligence/rate-limits). Sur Platform, la page [Usage](https://platform.noryo.app/usage) compte ces lectures dans leur propre famille.

---

Page canonique : https://developer.noryo.app/fr-FR/transaction-intelligence/read-an-enrichment
