# Recurring series

> What a series (`series[]`) is, why its identifier stays stable, and how to read `status`, `cadence`, `calendar_pattern` and `history_coverage`.

A series (`series[]` on an enrichment) groups several occurrences that are economically linked over time, within your workspace: a subscription, rent, a salary, a pension. `lifecycle` and `identity` each have their own page: [Series lifecycle](https://developer.noryo.app/en-GB/transaction-intelligence/series-lifecycle) and [Split, merge, supersession](https://developer.noryo.app/en-GB/transaction-intelligence/split-merge-supersession).

## A series is not a counterparty

A counterparty is the other end of a transaction; a series is a flow that comes back. One counterparty can carry several series: two distinct Netflix subscriptions are two series, with two `series_id` values. And a series never collapses into "every transaction with this counterparty".

A series is private: it lives in your workspace and never feeds the shared entity graph.

## An identifier that does not depend on the latest occurrence

`series_id` derives from what does not change: the workspace, the series family, the counterparty, the direction and the currency. It depends neither on the latest occurrence nor on the current descriptor. One more occurrence does not create another series.

You can therefore store `series_id`. Only an identity correction replaces it; the old one remains readable, marked `superseded` under `identity`, and `membership` links what you had stored to what you now receive.

## The four types

| `type` | What it asserts |
| --- | --- |
| `recurring_payment` | This payment comes back, nothing more: rent and a loan instalment are this too. |
| `subscription` | A stronger claim, which needs its own signal; without it, the answer stays `recurring_payment`. |
| `recurring_income` | Income that comes back: a pension, a benefit, rent received. |
| `salary_series` | A specialisation of `recurring_income`, not a synonym. |

`implies` carries the more general type that a type specialises: a `subscription` implies `recurring_payment`. `counterparty.semantics.recurring_model` says what is usual in the counterparty's line of business, not what is true of this transaction: on its own it never concludes; `series[].type` is what decides. There is no `subscription` category: a subscription is a model of commercial relationship, not a kind of spending.

## `status`: how much of a series it is

| `status` | Reading |
| --- | --- |
| `emerging` | Two occurrences make a series emerge. A single one, never. |
| `established` | Three occurrences can establish it. |
| `uncertain` | Not described by the contract beyond its name; its example shows it on an `irregular_recurring` cadence (`cadence_detail.strength` at `0`). |
| `broken` | Computed at read time: a series becomes late through the mere passage of time, with no write taking place. |

`status` says neither what is happening to the series now (`lifecycle.state`) nor whether its name is still the right one (`identity.status`).

A first occurrence opens no series: `series[]` is empty and `relationships_policy.unresolved` carries an entry with `type = series`, `outcome` and `occurrences` (`single_occurrence` and 1 in the contract's example, `no_series_key` and 0 in its unresolved example). The other outcomes are listed in the [Reference](https://developer.noryo.app/en-GB/transaction-intelligence/reference).

## Cadence and anchoring: two different questions

`cadence` says how often the event repeats: `weekly`, `biweekly`, `monthly`, `quarterly`, `yearly`, `irregular_recurring` or `unknown`. It is read on the calendar, not in bare days: 31 January and 28 February are one month apart. A 28-day recurrence is not monthly: its day moves back with every cycle, and it is filed under `irregular_recurring`. `cadence_detail.strength` is the share of expected cycles that carry an occurrence; `0` means "no calendar hypothesis retained", not "weak".

`calendar_pattern` answers a different question: where, within each period, the series tends to fall. A series can be squarely monthly while floating between the 1st and the 6th; without this block, `monthly` would read as "the same day every month".

| `calendar_pattern.type` | What it describes |
| --- | --- |
| `fixed_day_of_month` | Always around the same day. |
| `month_end` | At month end, whatever the number of days. |
| `last_business_day` | The last business day, a common case for salaries. |
| `business_day_adjusted` | A nominal day moved off the weekend. |
| `fixed_weekday` | Always the same day of the week. |
| `flexible_calendar_window` | One occurrence per period, with no stable day. |
| `unknown` | Nothing identifiable. |

Public holidays are not modelled: a holiday shift is absorbed by the window's tolerance, not recognised. Anchoring is also what gives `lifecycle.expected_next_window` its width.

**Schema `CalendarPattern`**

Where, within each period, the series tends to fall: a different question from `cadence`. A series can be squarely monthly while floating between the 1st and the 6th: without this field, "monthly" would read as "the same day every month", which is false for most real series. It is also what gives `expected_next_window` its width: a direct debit always on the 5th justifies three days, a rent that floats justifies eight. Applying the same window to both would declare a perfectly normal series "late".

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `type` | string | — | `fixed_day_of_month`: always around the same day. `month_end`: at month end, whatever the number of days; a direct debit on 31 January, 28 February and 31 March is a single pattern. `last_business_day`: the last business day, a common case for salaries. `business_day_adjusted`: a nominal day moved off the weekend (Saturday → Friday, Sunday → Monday). We do not model public holidays: neither French, nor target, nor banking holidays. A holiday shift is absorbed by the window's tolerance, not recognised; claiming to know a calendar we do not have would be worse than ignoring it. `fixed_weekday`: always the same day of the week. `flexible_calendar_window`: one occurrence per period, with no stable day. `unknown`: nothing identifiable. Values: `fixed_day_of_month`, `month_end`, `last_business_day`, `business_day_adjusted`, `fixed_weekday`, `flexible_calendar_window`, `unknown`. |
| `nominal_day` | integer \| null | — | — |
| `weekday` | integer \| null | — | — |
| `day_range` | array<integer> \| null | — | — |
| `weekend_shifts` | integer \| null | — | — |
| `strength` | number · 0–1 | — | A constant per anchor type, or a linear formula on the spread of days for `fixed_day_of_month` and `flexible_calendar_window`. `0` for `unknown`. A 0.33 `fixed_day_of_month` and a 0.3 `flexible_calendar_window` cannot be compared, and `flexible_calendar_window` changes its rule with the cadence (a constant 0.3 when weekly, `1 - etendue/10` when monthly). |
| `calibration` | string | — | Always `non_calibree` in this version, and that is a constraint, not a convention: the value is a vocabulary in the database, the columns that carry it reference it, and a guard rejects any literal outside the vocabulary at every producer. When a real calibration exists, it will enter through a declared migration, together with the corpus that supports it. Values: `non_calibree`. |

> **Note** `cadence_detail.strength` (class `calendar_fill`) and `calendar_pattern.strength` (class `anchor_stability`) are not probabilities: each is only comparable between series of the same cadence, and of the same anchor type for the latter. See [Score semantics](https://developer.noryo.app/en-GB/transaction-intelligence/score-semantics).

## Amounts and the next due date

`amount_behavior` does not require exact equality: an electricity bill varies, and so does a salary. `kind` is `fixed`, `narrow_variable` or `broadly_variable`; `min_minor_units` and `max_minor_units` are strings in minor units, never JSON numbers.

`expected_next_at` is optional, by design: it is absent until the series is established, and whenever the cadence is irregular. This is not a forecasting API.

## What memory lets you conclude

`history_coverage` separates two memories.

| Field | Meaning |
| --- | --- |
| `raw_days` | Lifetime of the raw transaction; past this delay it is deleted and nothing brings it back. `retention_limit_days` is identical, kept for compatibility. |
| `derived_days` | Lifetime of what we have learnt about a series, counted from its latest occurrence. Once it expires, a later occurrence opens a new series. |
| `observed_days` | The span actually observed, from the first to the last known occurrence. |
| `sufficiency` | One question only: can the cadence be believed? `sufficient`, `limited` or `insufficient`. |
| `first_occurrence_bounded` | A different question: do we know when it started? `first_occurrence_reason` says why. |

A purged transaction stays purged, but the series remembers that it happened: this is what makes a monthly cadence confirmable when January's transaction no longer exists.

`insufficient` means "our memory is too short to judge", never "there is no older occurrence". `first_observed_on` is the first day we observed the series, not the day it started (there is no `started_at` field); `may_predate_observation` is `true` when it may have begun before us.

> **Warning** A yearly series cannot become established within the observable window: `status` stays `emerging`. Read `sufficiency` and `observed_days` rather than concluding from `occurrence_count` alone.

## `match_strength` and `evidence`

`match_strength.value` is not a probability: it is the share of five heterogeneous criteria present (`corroborations.present` over `corroborations.possible`). It is not the same number as `relationships[].match_strength`, and it is only comparable between series of the same `detector_version`. `evidence[]` lists the signals the series rests on (`occurrences` carries an integer).

## Example

An established subscription series, an extract of the contract's example:

```json
{
  "series_id": "ser_6ba7b810-9dad-11d1-80b4-00c04fd430c8",
  "type": "subscription",
  "implies": ["recurring_payment"],
  "status": "established",
  "cadence": "monthly",
  "cadence_detail": { "type": "monthly", "strength": 1, "calibration": "non_calibree" },
  "calendar_pattern": {
    "type": "fixed_day_of_month", "nominal_day": 5, "weekday": null,
    "day_range": [4, 6], "weekend_shifts": 0, "strength": 0.67, "calibration": "non_calibree"
  },
  "occurrence_count": 6,
  "amount_behavior": { "kind": "fixed", "min_minor_units": "1349", "max_minor_units": "1349", "currency": "EUR" },
  "first_observed_on": "2026-04-05",
  "last_observed_on": "2026-09-05",
  "may_predate_observation": true,
  "expected_next_at": "2026-10-05T00:00:00Z",
  "match_strength": { "value": 1, "calibration": "non_calibree", "corroborations": { "present": 5, "possible": 5 } },
  "evidence": [
    { "code": "stable_counterparty", "value": "entite:b83d1756-258c-4541-adc7-54eec83dbd3c" },
    { "code": "occurrences", "value": 6 },
    { "code": "cadence", "value": "monthly" },
    { "code": "amount_behavior", "value": "fixed" },
    { "code": "subscription_category" }
  ],
  "history_coverage": {
    "raw_days": 30, "derived_days": 400, "observed_days": 153, "retention_limit_days": 30,
    "occurrence_count": 6, "cadence": "monthly", "sufficiency": "sufficient",
    "first_occurrence_bounded": true
  }
}
```

**Schema `Series`**

Several occurrences economically linked over time, within this workspace. A private series never becomes a truth of the entity graph.

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `series_id` | string | yes | Stable. It derives from what does not change (workspace, family, counterparty, direction, currency), never from the latest occurrence or the current descriptor. One more occurrence does not create another series. Pattern: `^ser_[0-9a-f-]{36}$`. |
| `type` | string | yes | `recurring_payment` says "this payment comes back", and nothing more: rent and a loan instalment are this too. `subscription` is a stronger claim and requires its own signal; without it, the answer stays `recurring_payment`. Likewise, `recurring_income` covers a pension, a benefit and rent received; `salary_series` is a specialisation of it, not a synonym. Values: `recurring_payment`, `subscription`, `recurring_income`, `salary_series`. |
| `implies` | array<string> | — | The more general type that this one specialises, if there is one. |
| `status` | string | yes | Two occurrences make a series emerge, three can establish it, a single one never. `broken` is computed at read time: a series becomes late through the mere passage of time, with no write taking place. Values: `emerging`, `established`, `uncertain`, `broken`. |
| `cadence` | string | yes | Read on the calendar, not in bare days: 31 January and 28 February are one month apart, and a salary on the last business day shifts. Values: `weekly`, `biweekly`, `monthly`, `quarterly`, `yearly`, `irregular_recurring`, `unknown`. |
| `occurrence_count` | integer | yes | — |
| `amount_behavior` | object | yes | Exact equality is not required: an electricity bill varies, and so does a salary. Amounts in minor units, as a string, never a JSON number. |
| `amount_behavior.kind` | string | — | Values: `fixed`, `narrow_variable`, `broadly_variable`. |
| `amount_behavior.min_minor_units` | string | — | — |
| `amount_behavior.max_minor_units` | string | — | — |
| `amount_behavior.currency` | string | — | — |
| `first_seen` | string · date-time | — | — |
| `last_seen` | string · date-time | — | — |
| `expected_next_at` | string · date-time \| null | — | Optional, and that is the point. Absent as long as the series is not established or the cadence is irregular: returning a date on the basis of two weak observations would pass it off as a forecast. This is not a forecasting API. |
| `match_strength` | object | yes | This is not a probability: it is the share of corroborating signals present out of those this type can produce. And it is not the same number as `relationships[].match_strength`: same name, different denominator, different signals. Five heterogeneous criteria out of five: calendar cadence, fixed or narrow amount, at least three occurrences, known entity, specialised type. They only add up here. |
| `match_strength.value` | number · 0.001–1 | yes | — |
| `match_strength.calibration` | string | yes | Always `non_calibree` in this version, and that is a constraint, not a convention: the value is a vocabulary in the database, the columns that carry it reference it, and a guard rejects any literal outside the vocabulary at every producer. When a real calibration exists, it will enter through a declared migration, together with the corpus that supports it. Values: `non_calibree`. |
| `match_strength.corroborations` | object | yes | — |
| `match_strength.corroborations.present` | integer | yes | — |
| `match_strength.corroborations.possible` | integer | yes | Values: `5`. |
| `evidence` | array<object> | — | The signals the series rests on. `value` is a string or an integer depending on the code (`occurrences` carries an integer). |
| `evidence[].code` | string | yes | — |
| `evidence[].value` | string \| integer \| null | — | — |
| `lifecycle` | SeriesLifecycle | — | An axis different from `status`. `status` says how strongly we believe this is a series; `lifecycle` says what is happening to it now. A very well-established series may have stopped collecting, and a fresh series may be perfectly alive. And no state is concluded from an absence without coverage: if the source was not observed completely over the expected window, the state is `unknown`, not `late`. "I saw nothing" never becomes "nothing happened": that is the difference between a client that stopped sending and a subscription that stopped collecting. |
| `lifecycle.state` | string | yes | `active`: the expected occurrence has arrived, or is not yet due. `late`: an occurrence is missing and the account was under observation. `inactive`: several consecutive occurrences are missing under complete observation; the series is no longer collecting, and we do not know why. `ended`: reserved for explicit terminal evidence. No accumulation of absences produces this state: a payment that stops may be a cancellation, a suspension, an expired card or a failed collection, and we do not guess which. `unknown`: we cannot conclude, and `reason` says why. Values: `active`, `late`, `inactive`, `ended`, `unknown`. |
| `lifecycle.reason` | string | — | — |
| `lifecycle.last_observed_on` | string · date \| null | — | — |
| `lifecycle.expected_next_window` | object \| null | — | A window, not a date. A salary lands around the last business days, a SEPA direct debit around the 5th. Returning an exact instant would be a precision we do not have, and you would believe it. |
| `lifecycle.expected_next_window.from` | string · date | — | — |
| `lifecycle.expected_next_window.to` | string · date | — | — |
| `lifecycle.expected_next_window.tolerance_days` | integer | — | The half-width of the window, in days. Larger = less precise. Comes from the anchor type, raised to the floor of the series family. |
| `lifecycle.expected_next_window.anchor` | string | — | The anchor type that produced the window. |
| `lifecycle.expected_next_window.widened_to_family_floor` | boolean | — | `true` if the tolerance was raised to the family floor (5 days for a subscription, 6 for a salary, 7 otherwise). |
| `lifecycle.consecutive_misses` | integer \| null | — | The counter only advances under complete coverage. Absent when nothing is expected, which is not the same ignorance as "we were not looking". |
| `lifecycle.observation_coverage` | string | — | `not_required`: no due date has closed, nothing had to be observed. `complete`: every source of the series was covered over the expected window; a miss is a miss. `incomplete`: at least one source was not; the absence proves nothing, `state` is `unknown`. `unknown`: the sources are unknown or too numerous to be checked. Values: `not_required`, `complete`, `incomplete`, `unknown`. |
| `lifecycle.cadence_strength` | number \| null | — | The same number as `cadence_detail.strength`, returned here so that the lifecycle can be read on its own. It is not a second score. |
| `lifecycle.calendar_anchor` | any | — | The same object as `calendar_pattern` (class `anchor_stability`), returned here for the same reason; `{ type: unknown }` when no anchor was inferred. Absent when no occurrence is expected. duplicate_of: `series[].calendar_pattern`; it is not a second score. |
| `lifecycle.evidence` | array<object> | — | Present for `ended`: the terminal evidence that produced it. |
| `lifecycle.evidence[].code` | string | — | — |
| `lifecycle.identity_status` | string \| null | — | Present only when the identity of the series has been corrected, to say why `state` is `unknown`: this is not economic ignorance, it is that the verdict now belongs to the successors. The full identity state is read in `identity`, which is an axis in its own right; this field is only a pointer, and it never takes the value `current`. Values: `superseded`, `null`. |
| `lifecycle.policy_version` | string | yes | — |
| `lifecycle.calibration` | string | yes | Always `non_calibree` in this version, and that is a constraint, not a convention: the value is a vocabulary in the database, the columns that carry it reference it, and a guard rejects any literal outside the vocabulary at every producer. When a real calibration exists, it will enter through a declared migration, together with the corpus that supports it. Values: `non_calibree`. |
| `identity` | SeriesIdentity | — | A third axis, and it mixes with neither `status` nor `lifecycle`. `status` says how strongly we believe this is a series; `lifecycle` says what is happening to it economically; `identity` says whether its name is still the right one. A series can debit every month and still have changed identity because we worked out that it was in fact two flows, or two halves of one. And a `series_id` is never rewritten or silently reused. When we correct, the old name remains, marked `superseded`, and it carries its successors. |
| `identity.status` | string | yes | `current`: this name is the current name of this flow. `superseded`: this name has been replaced by an identity correction. It stays valid as a historical reference (it will not disappear), but future occurrences will carry the name of its successors. Values: `current`, `superseded`. |
| `identity.superseded_by` | object \| null | — | After a split there is no single redirect, and that is why this field returns a list. If we discover that a series was mixing two flows, neither successor is "the continuation" of the old one: half of the old one's occurrences are missing from each of them. Electing a canonical one would be comfortable and wrong. |
| `identity.superseded_by.reason` | string | — | `split`: this identity was covering several, and each now has its own. `merge`: this identity and another were the same flow; both are replaced by a new identity. Values: `split`, `merge`. |
| `identity.superseded_by.at` | string · date-time | — | — |
| `identity.superseded_by.evidence` | array<string> | — | The evidence codes that authorised the correction. Never an internal score, never a row identifier. |
| `identity.superseded_by.successor_series_ids` | array<string> | — | One after a merge, several after a split. Never empty when `status` is `superseded`. |
| `identity.supersedes` | array<string> | yes | The way back, and it matters as much as the other one. The identities this one replaces. Without it, the old `series_id` you had stored would no longer correspond to anything you receive, and you would have no way to make the link. Empty for a series that was not born of any correction. |
| `membership` | SeriesMembership | — | Where this occurrence came from, and where it is now. When an identity is corrected, the occurrence changes series: without this block, you would have no way to relate the `series_id` you had stored to the one you receive. You never have to rebuild the graph yourself. |
| `membership.original_series_id` | string \| null | — | The series this occurrence was attached to the first time. Equal to `current_series_id` for as long as no correction has taken place. Pattern: `^ser_[0-9a-f-]{36}$`. |
| `membership.current_series_id` | string \| null | yes | Pattern: `^ser_[0-9a-f-]{36}$`. |
| `membership.corrections` | array<object> | yes | The successive moves, from oldest to most recent. Empty for an occurrence that has never moved. |
| `membership.corrections[].reason` | string | — | Values: `split`, `merge`. |
| `membership.corrections[].from_series_id` | string \| null | — | Pattern: `^ser_[0-9a-f-]{36}$`. |
| `membership.corrections[].to_series_id` | string \| null | — | Pattern: `^ser_[0-9a-f-]{36}$`. |
| `membership.corrections[].at` | string · date-time | — | — |
| `first_observed_on` | string · date | — | The first day **WE** Observed this series, not the day it started. A series may have been running for five years while we have only been observing it for three months. There is deliberately NO `started_at` field: we do not know it. |
| `last_observed_on` | string · date | — | — |
| `may_predate_observation` | boolean | — | `true`: our observation begins with the first known occurrence, so the series may have started before us and nothing will show it. `false`: the first known occurrence predates the start of our observation, so its beginning is observed, not assumed. |
| `cadence_detail` | object | — | How often the event repeats, and nothing else. The cadence is deduced from the progression of calendar periods, not from gaps in days: three consecutive months are three consecutive months, whether the gaps are 28, 30 or 31 days. And a 28-day recurrence is not monthly: it falls once a month for eleven months, then twice, and its day moves back with every cycle. We do not call it `monthly`: this version has no cadence for it, and `irregular_recurring` is better than a false monthly. `strength` is the share of expected cycles that carry an occurrence. It is not a probability. |
| `cadence_detail.type` | string | — | — |
| `cadence_detail.strength` | number · 0–1 \| null | — | `0` = no calendar hypothesis retained, not "weak". |
| `cadence_detail.calibration` | string | — | Always `non_calibree` in this version, and that is a constraint, not a convention: the value is a vocabulary in the database, the columns that carry it reference it, and a guard rejects any literal outside the vocabulary at every producer. When a real calibration exists, it will enter through a declared migration, together with the corpus that supports it. Values: `non_calibree`. |
| `calendar_pattern` | CalendarPattern | — | Where, within each period, the series tends to fall: a different question from `cadence`. A series can be squarely monthly while floating between the 1st and the 6th: without this field, "monthly" would read as "the same day every month", which is false for most real series. It is also what gives `expected_next_window` its width: a direct debit always on the 5th justifies three days, a rent that floats justifies eight. Applying the same window to both would declare a perfectly normal series "late". |
| `calendar_pattern.type` | string | — | `fixed_day_of_month`: always around the same day. `month_end`: at month end, whatever the number of days; a direct debit on 31 January, 28 February and 31 March is a single pattern. `last_business_day`: the last business day, a common case for salaries. `business_day_adjusted`: a nominal day moved off the weekend (Saturday → Friday, Sunday → Monday). We do not model public holidays: neither French, nor target, nor banking holidays. A holiday shift is absorbed by the window's tolerance, not recognised; claiming to know a calendar we do not have would be worse than ignoring it. `fixed_weekday`: always the same day of the week. `flexible_calendar_window`: one occurrence per period, with no stable day. `unknown`: nothing identifiable. Values: `fixed_day_of_month`, `month_end`, `last_business_day`, `business_day_adjusted`, `fixed_weekday`, `flexible_calendar_window`, `unknown`. |
| `calendar_pattern.nominal_day` | integer \| null | — | — |
| `calendar_pattern.weekday` | integer \| null | — | — |
| `calendar_pattern.day_range` | array<integer> \| null | — | — |
| `calendar_pattern.weekend_shifts` | integer \| null | — | — |
| `calendar_pattern.strength` | number · 0–1 | — | A constant per anchor type, or a linear formula on the spread of days for `fixed_day_of_month` and `flexible_calendar_window`. `0` for `unknown`. A 0.33 `fixed_day_of_month` and a 0.3 `flexible_calendar_window` cannot be compared, and `flexible_calendar_window` changes its rule with the cadence (a constant 0.3 when weekly, `1 - etendue/10` when monthly). |
| `calendar_pattern.calibration` | string | — | Always `non_calibree` in this version, and that is a constraint, not a convention: the value is a vocabulary in the database, the columns that carry it reference it, and a guard rejects any literal outside the vocabulary at every producer. When a real calibration exists, it will enter through a declared migration, together with the corpus that supports it. Values: `non_calibree`. |
| `history_coverage` | object | yes | What we can see, and what we cannot. `insufficient` means "our memory is too short to judge", never "there is no older occurrence". And there are two memories, which must not be confused. `raw_days` (30) says how long the transaction remains readable; `derived_days` (400) says how long what we have learnt about a series outlives it. A purged transaction stays purged (this API never brings it back), but the series remembers that it happened. That is what makes a monthly cadence confirmable when January's transaction no longer exists. `observed_days` is what we actually saw: a client reading "monthly / sufficient" can check that the conclusion does not come from thirty days. |
| `history_coverage.observed_from` | string · date | — | — |
| `history_coverage.observed_to` | string · date | — | — |
| `history_coverage.raw_days` | integer | — | Lifetime of the raw transaction. Past this delay it is physically deleted and nothing brings it back. |
| `history_coverage.derived_days` | integer | — | Lifetime of the series memory, counted from the latest occurrence. Without a new occurrence, the derived state is physically deleted when it expires, and a later occurrence opens a new series. |
| `history_coverage.observed_days` | integer | — | The span actually observed, from the first to the last known occurrence. |
| `history_coverage.retention_limit_days` | integer | yes | Kept for compatibility: identical to `raw_days`. |
| `history_coverage.occurrence_count` | integer | yes | — |
| `history_coverage.cadence` | string | — | — |
| `history_coverage.sufficiency` | string | yes | Answers one question only: can the cadence be believed? Not "do we know when it started", which is the next field. Values: `sufficient`, `limited`, `insufficient`. |
| `history_coverage.reason` | string | — | — |
| `history_coverage.first_occurrence_reason` | string | — | Says why `first_occurrence_bounded` has the value it has. |
| `history_coverage.first_occurrence_bounded` | boolean | — | A different question from `sufficiency`, and one that used to be confused with it. `true`: our observation of this series begins with the first occurrence we know of, so it may have started before us, and nothing will ever show it. `false`: the first known occurrence predates the start of our observation, so the beginning is observed, not assumed. A history import returns `false`; a series discovered as transactions arrive returns `true`. |
| `detector_version` | string | — | — |

## Further reading

- [Series lifecycle](https://developer.noryo.app/en-GB/transaction-intelligence/series-lifecycle)
- [Observation coverage](https://developer.noryo.app/en-GB/transaction-intelligence/observation-coverage)
- [Split, merge, supersession](https://developer.noryo.app/en-GB/transaction-intelligence/split-merge-supersession)
- [Score semantics](https://developer.noryo.app/en-GB/transaction-intelligence/score-semantics)
- [Reference](https://developer.noryo.app/en-GB/transaction-intelligence/reference)

---

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