# Couverture d'observation

> Déclarez ce que vous avez fourni pour un compte sur une période, afin qu'une échéance absente puisse être lue comme un manque et non comme une inconnue.

Une couverture d'observation (coverage window) est une déclaration : « toutes les opérations de ce compte, entre ces deux dates, vous ont été fournies ». C'est un fait de compte et de période, pas de transaction. Elle a sa propre ressource, `POST /transactions/v1/coverage-windows`, et non un champ de [`POST /transactions/v1/enrichments`](https://developer.noryo.app/fr-FR/transaction-intelligence/enrich-a-transaction), parce qu'elle existe précisément quand il n'y a aucune transaction à envoyer : un mois sans prélèvement est le cas où la déclaration compte le plus.

## Pourquoi une absence ne prouve rien

Transaction Intelligence sait dire qu'une échéance attendue n'est pas arrivée. Il ne sait pas, seul, si elle n'a pas eu lieu ou si personne ne la lui a envoyée. Sans cette distinction, « le client a cessé d'appeler l'API » et « l'abonnement a cessé de prélever » seraient une seule et même chose.

La couverture ne se déduit jamais du trafic : dix appels en octobre peuvent être dix opérations sur cent. Elle exige une déclaration explicite par compte et par période. Tant qu'elle manque, `lifecycle.state` reste `unknown`, jamais `late` : « je n'ai rien vu » ne devient pas « il n'y a rien eu ».

## Déclarer une couverture

**`POST /transactions/v1/coverage-windows`** — Déclarer ce qui a été fourni pour un compte, sur une période

Une couverture est un fait de compte et de période, pas de transaction. Elle répond à « toutes les opérations de ce compte entre ces deux dates vous ont-elles été fournies ? » — et elle existe précisément quand il n'y a aucune transaction à envoyer : un mois sans prélèvement est le cas où la déclaration compte le plus. C'est pour cela qu'elle a sa propre ressource plutôt qu'un champ sur l'enrichissement. Sans elle, nous ne dirons jamais qu'une série est en retard : l'absence d'une échéance ne prouve rien tant que nous ignorons si le compte était observé, et `lifecycle.state` reste `unknown`. Et nous ne vérifions rien : ce que vous déclarez est enregistré comme une affirmation d'appelant (`caller_asserted`). La provenance n'est pas un champ que vous choisissez. L'appel est idempotent par construction : les intervalles qui se touchent fusionnent, donc une synchronisation horaire ne fabrique pas d'intervalles.

#### Corps de la requête

`application/json` · requis · Schéma `CoverageWindow`

| Champ | Type | Requis | Description |
| --- | --- | --- | --- |
| `account_hint` | object | oui | Le compte concerné. `iban` quand il en a un, sinon `external_account_id` — votre propre identifiant, pour les cartes, portefeuilles et rails non-SEPA. Au moins l'un des deux. Aucun des deux n'est conservé : nous n'en gardons qu'une empreinte opaque, portée à votre workspace — la même que celle de l'enrichissement, sans quoi la couverture ne croiserait jamais la série qu'elle qualifie. |
| `account_hint.iban` | string | — | Motif : `^[A-Z]{2}[0-9]{2}[A-Z0-9]{10,30}$`. |
| `account_hint.external_account_id` | string · length 1–128 | — | — |
| `covered_from` | string · date | oui | — |
| `covered_to` | string · date | oui | On ne certifie pas ce qui n'a pas encore eu lieu : une date future est refusée. |
| `completeness` | string | — | `complete` signifie quelque chose de précis : vous affirmez que toutes les opérations de ce compte sur cet intervalle nous ont été fournies. C'est la seule valeur qui nous autorise à conclure d'une absence. `partial` et `unknown` n'y autorisent pas — dix appels en octobre peuvent être dix opérations sur cent. Valeurs : `complete`, `partial`, `unknown`. |

#### Réponses

| Statut | Description |
| --- | --- |
| `200` | Couverture enregistrée, ou fusionnée avec une voisine. |
| `400` | Corps illisible, ou hôte non déclaré. |
| `401` | Clef absente, inconnue ou révoquée. |
| `403` | Organisation ou workspace inactif. |
| `413` | Corps trop volumineux. |
| `415` | Le corps doit être du application/json. |
| `422` | Schéma fermé : champ inconnu, compte absent ou malformé, jour inexistant, intervalle inversé ou dans le futur, complétude inconnue. `errors[].code` nomme la règle, et la liste est celle du runtime, par 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` couvre la forme et le calendrier (`2026-02-30` est refusé). `interval_in_future` : `covered_to` au-delà de demain, date du serveur en UTC. `provenance` n'est pas un champ d'entrée : le schéma fermé le refuse `unknown_field`, la base ne le voit jamais. |
| `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)
- [`request_entity_too_large`](https://developer.noryo.app/fr-FR/transaction-intelligence/errors/request_entity_too_large)
- [`unsupported_media_type`](https://developer.noryo.app/fr-FR/transaction-intelligence/errors/unsupported_media_type)
- [`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`.

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

Le corps est un `CoverageWindow`, schéma fermé : tout champ inconnu est refusé (422).

**Schéma `CoverageWindow`**

Schéma fermé : un champ inconnu est refusé (422). En particulier `provenance` n'est pas un champ d'entrée.

| Champ | Type | Requis | Description |
| --- | --- | --- | --- |
| `account_hint` | object | oui | Le compte concerné. `iban` quand il en a un, sinon `external_account_id` — votre propre identifiant, pour les cartes, portefeuilles et rails non-SEPA. Au moins l'un des deux. Aucun des deux n'est conservé : nous n'en gardons qu'une empreinte opaque, portée à votre workspace — la même que celle de l'enrichissement, sans quoi la couverture ne croiserait jamais la série qu'elle qualifie. |
| `account_hint.iban` | string | — | Motif : `^[A-Z]{2}[0-9]{2}[A-Z0-9]{10,30}$`. |
| `account_hint.external_account_id` | string · length 1–128 | — | — |
| `covered_from` | string · date | oui | — |
| `covered_to` | string · date | oui | On ne certifie pas ce qui n'a pas encore eu lieu : une date future est refusée. |
| `completeness` | string | — | `complete` signifie quelque chose de précis : vous affirmez que toutes les opérations de ce compte sur cet intervalle nous ont été fournies. C'est la seule valeur qui nous autorise à conclure d'une absence. `partial` et `unknown` n'y autorisent pas — dix appels en octobre peuvent être dix opérations sur cent. Valeurs : `complete`, `partial`, `unknown`. |

Le compte se nomme par `account_hint` : `iban` quand il en a un, sinon `external_account_id`, votre propre identifiant pour les cartes, portefeuilles et rails sans IBAN. Au moins l'un des deux est requis (`account_identity_required`). Aucun des deux n'est conservé : Noryo n'en garde qu'une empreinte opaque, portée à votre workspace, la même que celle calculée sur l'enrichissement. Employez le même `account_hint` dans les deux appels : une transaction envoyée sans lui n'a pas de source, et aucune couverture ne la croisera.

`covered_from` et `covered_to` sont des dates de la forme `2026-07-01`. Un jour inexistant est refusé (`must_be_iso_date`), un intervalle inversé aussi (`after_covered_to`). On ne certifie pas ce qui n'a pas encore eu lieu : un `covered_to` au-delà de demain, date du serveur en UTC, est refusé (`interval_in_future`).

### Le sens exact de `completeness`

| Valeur | Ce que vous affirmez | Permet de conclure d'une absence |
| --- | --- | --- |
| `complete` | Toutes les opérations de ce compte sur cet intervalle ont été fournies. | Oui, et c'est la seule. |
| `partial` | Une partie seulement a été fournie. | Non. |
| `unknown` | Vous ne savez pas. | Non. |

`complete` a un sens précis : **toutes** les opérations du compte sur l'intervalle ont été transmises, y compris quand il n'y en avait aucune. C'est la seule valeur qui autorise à lire une absence comme un manque.

> **Attention** Quand `completeness` est omis, le contrat applique `complete`. Ne l'omettez pas par commodité : déclarez `complete` seulement sur les périodes où votre synchronisation le garantit.

### La provenance n'est pas une entrée

La réponse porte `provenance: caller_asserted`, toujours. Ce n'est pas un champ que vous choisissez : Noryo enregistre ce que vous affirmez, ne le vérifie pas, et vous ne pouvez pas déclarer autre chose. Envoyer `provenance` dans le corps est refusé comme `unknown_field`.

## Réponse et fusion d'intervalles

**Schéma `CoverageWindowResult`**

Ce que la déclaration est devenue une fois enregistrée. `merged_intervals` compte les intervalles préexistants que cette déclaration a absorbés : `0` quand elle est seule, `1` sur un rejeu exact (elle a absorbé sa propre copie), `1` ou plus quand elle touche ou chevauche des déclarations antérieures. `covered_from` / `covered_to` sont les bornes après fusion. Mesuré au bord public : une première version de ce document disait « 1 si elle est seule » — le moteur avait raison, la doc non.

| Champ | Type | Requis | Description |
| --- | --- | --- | --- |
| `covered_from` | string · date | oui | — |
| `covered_to` | string · date | oui | — |
| `completeness` | string | oui | Valeurs : `complete`, `partial`, `unknown`. |
| `provenance` | string | oui | Toujours `caller_asserted` : nous enregistrons ce que vous affirmez, nous ne le vérifions pas, et vous ne pouvez pas déclarer autre chose. Valeurs : `caller_asserted`. |
| `merged_intervals` | integer ≥ 0 | oui | — |
| `policy_version` | string | oui | — |
| `note` | string | — | — |
| `request_id` | string | oui | — |

La réponse est un 200 dans les deux cas, enregistrée seule ou fusionnée avec une voisine. `covered_from` et `covered_to` sont les bornes **après** fusion. `merged_intervals` compte les intervalles préexistants que la déclaration a absorbés :

| Situation | `covered_from` / `covered_to` | `merged_intervals` |
| --- | --- | --- |
| Première déclaration, du 2026-07-01 au 2026-07-31 | 2026-07-01 / 2026-07-31 | `0` |
| Déclaration du 2026-08-01 au 2026-08-15, contiguë à la précédente | 2026-07-01 / 2026-08-15 | `1` |
| Rejeu exact de la déclaration précédente | 2026-07-01 / 2026-08-15 | `1` |

Deux intervalles du même compte qui se touchent ou se chevauchent n'en font qu'un : une synchronisation horaire ne fabrique pas d'intervalles, et un rejeu exact rend `1` parce que la déclaration absorbe sa propre copie. L'appel est idempotent par construction, sans en-tête `Idempotency-Key`.

## Ce que la couverture change dans une série

La couverture agit sur l'axe `lifecycle` d'une [série récurrente](https://developer.noryo.app/fr-FR/transaction-intelligence/recurring-series) (voir [Cycle de vie d'une série](https://developer.noryo.app/fr-FR/transaction-intelligence/series-lifecycle)). Pour chaque échéance close, Noryo vérifie que chaque source de la série était couverte `complete` sur toute la fenêtre attendue (`expected_next_window`, de `from` à `to`) ; une couverture qui n'en contient qu'une partie ne permet pas de conclure.

| `lifecycle.observation_coverage` | Sens | Ce qu'une absence permet de conclure |
| --- | --- | --- |
| `not_required` | Aucune échéance close : rien n'avait à être observé. | Rien à conclure, aucun manque n'est possible. |
| `complete` | Chaque source était couverte sur la fenêtre attendue. | Un manque est un manque : `late`, puis `inactive` quand les manques consécutifs se répètent. |
| `incomplete` | Au moins une source ne l'était pas. | Rien : `state` est `unknown`, et `reason` dit pourquoi. |
| `unknown` | Les sources sont inconnues ou trop nombreuses pour être vérifiées. | Rien : `state` est `unknown`. |

`consecutive_misses` n'avance que sous couverture complète. `ended` n'est jamais produit par une accumulation d'absences, avec ou sans couverture : il exige une preuve terminale explicite.

Ce que la couverture ne change pas : `status` (à quel point c'est une série) et `history_coverage.sufficiency` (peut-on croire la cadence). Ces deux lectures dépendent des occurrences observées, pas de vos déclarations.

## Ce que Noryo ne vérifie pas

Noryo n'interroge pas votre banque et ne compare pas vos déclarations à vos envois. Une couverture `complete` déclarée sur une période où des opérations manquaient produira des `late` et des `inactive` qui n'existent pas : la déclaration engage l'appelant. Le contrat n'expose ni lecture ni retrait d'une déclaration : déclarez ce que vous savez, au rythme de votre synchronisation.

## Exemples

Un compte SEPA, juillet fourni intégralement :

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

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

Une carte prépayée, nommée par votre identifiant, synchronisation partielle :

```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: { external_account_id: "carte-4970" },
    covered_from: "2026-08-01",
    covered_to: "2026-08-31",
    completeness: "partial",
  }),
});

const coverage = await response.json();
console.log(response.status, coverage.merged_intervals, response.headers.get("noryo-request-id"));
```

## Refus propres à cette route

Un 422 rend un `Problem` de type `invalid_input`, dont `errors[].code` nomme la règle : `unknown_field` (champ hors schéma, `provenance` compris), `required` (`account_hint` absent ou qui n'est pas un objet), `account_identity_required`, `malformed_iban`, `must_be_iso_date` (date absente, mal formée ou jour inexistant), `after_covered_to`, `interval_in_future`, `unknown_completeness`. La liste complète, `must_be_object`, `must_be_non_empty_string`, `too_long` et `contains_control_character` compris, figure dans l'opération ci-dessus et sur [/errors/invalid_input](https://developer.noryo.app/fr-FR/transaction-intelligence/errors/invalid_input).

Les autres réponses (400, 401, 403, 413, 415, 429, 500) sont communes à l'API. Cette route compte dans les [limites de débit](https://developer.noryo.app/fr-FR/transaction-intelligence/rate-limits) ; un 422 consomme un jeton, la clé ayant été validée. Catalogue complet : [Erreurs](https://developer.noryo.app/fr-FR/transaction-intelligence/errors).

---

Page canonique : https://developer.noryo.app/fr-FR/transaction-intelligence/observation-coverage
