# Observation coverage

> Declare what you supplied for an account over a period, so that a missing due date can be read as a miss rather than as an unknown.

An observation coverage window is a declaration: "every operation on this account between these two dates has been supplied to you". It is a fact about an account and a period, not about a transaction. It has its own resource, `POST /transactions/v1/coverage-windows`, rather than a field on [`POST /transactions/v1/enrichments`](https://developer.noryo.app/en-GB/transaction-intelligence/enrich-a-transaction), because it exists precisely when there is no transaction to send: a month with no direct debit is the case where the declaration matters most.

## Why an absence proves nothing

Transaction Intelligence can tell that an expected due date has not arrived. On its own, it cannot tell whether that is because nothing happened or because nobody sent it. Without that distinction, "the client stopped calling the API" and "the subscription stopped collecting" would be one and the same thing.

Coverage is never inferred from traffic: ten calls in October may be ten operations out of a hundred. It requires an explicit declaration per account and per period. Until one exists, `lifecycle.state` stays `unknown`, never `late`: "I saw nothing" does not become "nothing happened".

## Declaring coverage

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

The body is a `CoverageWindow`, a closed schema: any unknown field is rejected (422).

**Schema `CoverageWindow`**

Closed schema: an unknown field is rejected (422). In particular, `provenance` is not an input field.

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

The account is named through `account_hint`: `iban` when it has one, otherwise `external_account_id`, your own identifier for cards, wallets and rails without an IBAN. At least one of the two is required (`account_identity_required`). Neither is stored: Noryo keeps only an opaque fingerprint, scoped to your workspace, the same one computed on the enrichment. Use the same `account_hint` in both calls: a transaction sent without it has no source, and no coverage will ever meet it.

`covered_from` and `covered_to` are dates in the form `2026-07-01`. A non-existent day is rejected (`must_be_iso_date`), and so is a reversed interval (`after_covered_to`). Noryo does not certify what has not happened yet: a `covered_to` beyond tomorrow, by the server's date in UTC, is rejected (`interval_in_future`).

### The exact meaning of `completeness`

| Value | What you are asserting | Allows a conclusion from an absence |
| --- | --- | --- |
| `complete` | Every operation on this account over this interval has been supplied. | Yes, and it is the only one. |
| `partial` | Only some of them have been supplied. | No. |
| `unknown` | You do not know. | No. |

`complete` has a precise meaning: **all** operations on the account over the interval have been transmitted, including when there were none. It is the only value that allows an absence to be read as a miss.

> **Warning** When `completeness` is omitted, the contract applies `complete`. Do not leave it out for convenience: declare `complete` only over periods where your synchronisation genuinely guarantees it.

### Provenance is not an input

The response carries `provenance: caller_asserted`, always. It is not a field you choose: Noryo records what you assert, does not verify it, and you cannot declare anything else. Sending `provenance` in the body is rejected as `unknown_field`.

## Response and interval merging

**Schema `CoverageWindowResult`**

What the declaration became once recorded. `merged_intervals` counts the pre-existing intervals this declaration absorbed: `0` when it stands alone, `1` on an exact replay (it absorbed its own copy), `1` or more when it touches or overlaps earlier declarations. `covered_from` / `covered_to` are the bounds after merging. Measured at the public edge: a first version of this document said "1 if it stands alone"; the engine was right, the documentation was not.

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `covered_from` | string · date | yes | — |
| `covered_to` | string · date | yes | — |
| `completeness` | string | yes | Values: `complete`, `partial`, `unknown`. |
| `provenance` | string | yes | Always `caller_asserted`: we record what you assert, we do not verify it, and you cannot declare anything else. Values: `caller_asserted`. |
| `merged_intervals` | integer ≥ 0 | yes | — |
| `policy_version` | string | yes | — |
| `note` | string | — | — |
| `request_id` | string | yes | — |

The response is a 200 in both cases, recorded on its own or merged with a neighbour. `covered_from` and `covered_to` are the bounds **after** merging. `merged_intervals` counts the pre-existing intervals the declaration absorbed:

| Situation | `covered_from` / `covered_to` | `merged_intervals` |
| --- | --- | --- |
| First declaration, 2026-07-01 to 2026-07-31 | 2026-07-01 / 2026-07-31 | `0` |
| Declaration of 2026-08-01 to 2026-08-15, adjacent to the previous one | 2026-07-01 / 2026-08-15 | `1` |
| Exact replay of the previous declaration | 2026-07-01 / 2026-08-15 | `1` |

Two intervals for the same account that touch or overlap become one: an hourly synchronisation does not manufacture intervals, and an exact replay returns `1` because the declaration absorbs its own copy. The call is idempotent by construction, with no `Idempotency-Key` header.

## What coverage changes in a series

Coverage acts on the `lifecycle` axis of a [recurring series](https://developer.noryo.app/en-GB/transaction-intelligence/recurring-series) (see [Series lifecycle](https://developer.noryo.app/en-GB/transaction-intelligence/series-lifecycle)). For each closed due date, Noryo checks that every source of the series was covered `complete` over the whole expected window (`expected_next_window`, from `from` to `to`); a coverage that contains only part of the window does not allow a conclusion.

| `lifecycle.observation_coverage` | Meaning | What an absence allows you to conclude |
| --- | --- | --- |
| `not_required` | No closed due date: nothing had to be observed. | Nothing to conclude; no miss is possible. |
| `complete` | Every source was covered over the expected window. | A miss is a miss: `late`, then `inactive` when consecutive misses repeat. |
| `incomplete` | At least one source was not. | Nothing: `state` is `unknown`, and `reason` says why. |
| `unknown` | The sources are unknown or too numerous to check. | Nothing: `state` is `unknown`. |

`consecutive_misses` only advances under complete coverage. `ended` is never produced by an accumulation of absences, with or without coverage: it requires explicit terminal evidence.

What coverage does not change: `status` (how much of a series this is) and `history_coverage.sufficiency` (whether the cadence can be trusted). Both readings depend on the occurrences observed, not on your declarations.

## What Noryo does not verify

Noryo does not query your bank and does not compare your declarations with what you sent. A `complete` coverage declared over a period where operations were missing will produce `late` and `inactive` states that do not exist: the declaration is the caller's responsibility. The contract exposes no way to read back or withdraw a declaration: declare what you know, at the pace of your synchronisation.

## Examples

A SEPA account, with July supplied in full:

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

A prepaid card, named by your own identifier, partial synchronisation:

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

## Rejections specific to this route

A 422 returns a `Problem` of type `invalid_input`, where `errors[].code` names the rule: `unknown_field` (a field outside the schema, `provenance` included), `required` (`account_hint` missing or not an object), `account_identity_required`, `malformed_iban`, `must_be_iso_date` (date missing, malformed or a non-existent day), `after_covered_to`, `interval_in_future`, `unknown_completeness`. The full list, including `must_be_object`, `must_be_non_empty_string`, `too_long` and `contains_control_character`, is in the operation above and on [/errors/invalid_input](https://developer.noryo.app/en-GB/transaction-intelligence/errors/invalid_input).

The other responses (400, 401, 403, 413, 415, 429, 500) are shared across the API. This route counts towards the [rate limits](https://developer.noryo.app/en-GB/transaction-intelligence/rate-limits); a 422 consumes a token, since the key was valid. Full catalogue: [Errors](https://developer.noryo.app/en-GB/transaction-intelligence/errors).

---

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