---
title: "Which accounts reported which periods, and at what grain"
method: GET
path: "/api/v1/sell-out/coverage"
tags: ["sell_out"]
---

# Which accounts reported which periods, and at what grain

`GET /api/v1/sell-out/coverage`

## Request

- Base URL: `https://api.app.tightly.io`
- URL: `https://api.app.tightly.io/api/v1/sell-out/coverage`
- Auth: HTTP bearer

Which accounts reported which weeks, and at what grain: the week axis once, and each account's reported weeks keyed against it with the units and variants it sent, its last reported week and its grain. `weeks` bounds the window.

Coverage is account against time, and a table of totals cannot show that an account stopped reporting four weeks ago.

A week an account did not report is ABSENT from its map, never a row with zero units. The source data does not agree on that point: one retailer lists every product every week and means its zeros, another lists only movement, so filling the gaps would decide the argument wrongly for one of them, and a reader seeing "0 units" could not tell a dead week from a week nobody sent. Inside a reported week `units` can still be null, because a stock-only week has no sales figure.

Derived rows, the brand's own curve scaled by an account's share, are excluded: a coverage grid counting them would claim an account sent weeks nobody sent.

Sold with Essentials+: without Tightly Connect the answer is 403 `plan_excludes`.

Scope: `sell_out:read`.

## Query parameters

- `weeks` integer
- `commitment_id` string
- `as_of` string, date

## Headers

- `Tightly-Version` string

## Response `200`

The week axis, one row per account with the weeks it reported and the grain it reports at, and the two counts a verdict is written from, how many accounts reported at all, and how many there are.

- object
  - `data` object
    - `accounts` object[]
      - `account` string, nullable
      - `account_id` string
      - `coverage_sentence` string, nullable: "4 months reported", "12 weeks reported", or both. Null where nothing was reported.
      - `grain` 'store' | 'account' | 'null', nullable: The LOCATION grain. What a period is is `period_grain`.
      - `last_reported` string, date, nullable
      - `period_axis` object[]: Exact cell keys and bounds, preserving distinct weekly and monthly periods with the same start.
        - `grain` string
        - `key` string
        - `period_days` integer, nullable
        - `period_end` string, date, nullable
        - `period_start` string, date
      - `period_grain` 'week' | 'month' | 'mixed' | 'null', nullable
      - `periods_reported` integer
      - `reported` object: The periods this account reported, KEYED ON THE PERIOD: unambiguous weekly start dates and otherwise start/end dates, `2026-08-01/2026-08-31`. Not on the start alone, because a month and a week that open on the same day are two periods and one would replace the other. A period the account did not report is ABSENT, never a cell of zeroes.
      - `sales_channel_id` string, nullable: Stable channel identity, present only in scoped reads.
      - `weekly_rate_absent` string, nullable: Why a per-week rate is not measured, where the grain is the reason.
    - `as_of` string, date
    - `commitment_coverage` object[], nullable: Coverage per (account, Commitment): how much of what each Commitment expects of an account it has reported, at the account's own grain. Stored and refreshed when sell-out arrives and on every sync, so `commitment_coverage_as_of` says the date it was measured for. Every active Commitment, or the one `commitment_id` names; narrowed to the account a key is bound to. Null, with `commitment_coverage_reason`, when it could not be read or has not been measured yet; never an empty list for that. An account that carries nothing under a Commitment (no line, no showing) is absent, never an uncovered row.
      - `account` string
      - `account_id` string
      - `as_of` string, date: The date this row was measured for.
      - `commitment_id` string
      - `commitment_name` string, nullable
      - `doors_basis` 'placed' | 'reported_last_season' | 'all_doors_assumed' | 'account_level' | 'null', nullable: The weakest basis any expected door stands on. `all_doors_assumed` is said on the receipt.
      - `doors_expected` integer
      - `doors_reported` integer
      - `grain` 'week' | 'month'
      - `grain_basis` 'reported' | 'assumed'
      - `lag_basis` 'observed' | 'declared_cadence'
      - `lag_days` integer: The lag used, observed or the declared cadence plus 7 (weekly 14, monthly 38).
      - `observed_lag_days` integer, nullable: The median of the last six imports' arrival after the period they covered. Null until three imports; never 0 for "not observed".
      - `periods_due` integer: Expected periods whose end plus the account's lag has passed.
      - `periods_expected` integer: Periods of the account's grain overlapping the span. A month is one period, never split into weeks.
      - `periods_missing` integer: Due and unreported. Missing, never zero demand.
      - `periods_reported` integer
      - `reads` 'sell_out' | 'sell_in': Which demand is read for this account under this Commitment. Sell-out only at or above the floor.
      - `refreshed_at` string, date-time, nullable
      - `reported_periods` string[]: Starts of the reported periods that are past the account's lag.
      - `reported_to` string, date, nullable
      - `sentence` string
      - `span_end` string, date, nullable: Today less the account's lag; a boxed window is capped at its end, so a closed window is judged whole.
      - `span_start` string, date, nullable: Boxed, the window's start. Rolling, the later of the account's first line under the Commitment and the DTC benchmark's start.
      - `verdict` 'covered' | 'under_floor' | 'nothing_in_yet' | 'cannot_tell': `cannot_tell`: a rolling Commitment whose products have no own-channel sales to compare with, so no span can be judged.
      - `window_type` 'boxed' | 'rolling'
    - `commitment_coverage_as_of` string, date, nullable: The date the last whole-book refresh measured for. A row refreshed since, when one account's sell-out arrived, carries its own `as_of`.
    - `commitment_coverage_floor` object: The signed floor, 0.75 of the doors and 0.75 of the periods.
    - `commitment_coverage_reason` string, nullable
    - `commitment_coverage_refreshed_at` string, date-time, nullable
    - `commitment_coverage_source` 'stored' | 'live' | 'null', nullable: `stored`: the rows as last refreshed. `live`: one Commitment measured for an `as_of` other than the stored date.
    - `commitment_id` string: Only present for the optional scoped read.
    - `expected_accounts` integer, nullable: Always null for scoped reads; no expected-reporting membership exists.
    - `period_axis` object[]: Exact cell keys and bounds, preserving distinct weekly and monthly periods with the same start.
      - `grain` string
      - `key` string
      - `period_days` integer, nullable
      - `period_end` string, date, nullable
      - `period_start` string, date
    - `period_policy` string
    - `population` object
      - `end_date` string, date
      - `kind` 'as_of' | 'future_book_window'
      - `start_date` string, date
    - `reported_window` object, nullable
      - `end_date` string, date
      - `start_date` string, date
    - `reported_window_reason` string, nullable
    - `reporting` integer: How many accounts reported at least one week in the window.
    - `roster_basis` 'reported_activity': Scoped accounts are evidence-bearing reporters, not an expected roster.
    - `total_accounts` integer
    - `weeks` string[]: The period axis, one ISO date per period start. It steps at the cadence each account actually reports: seven days for a weekly feed, one CALENDAR MONTH for an account that files months, and both where accounts differ. It runs to the last COMPLETED period, so a period still running is not yet an account's silence.
  - `message` object: severity, service and desc.

## Other responses

- `400`: Empty commitment_id, invalid as_of, or as_of without commitment_id.
- `401`: No usable key: malformed, unknown, revoked, expired or stopped. One refusal covers all five, telling a caller which is which maps the surface for them. A request with no Authorization header at all is refused 400 before any key is looked for.
- `403`: The key may not reach this operation. `plan_excludes` when Sell-out is sold with Essentials+ and this organisation's plan does not include it; `scope_missing` when the key does not hold Sell-out; `organization_mismatch` when the path names an organisation that is not the key's; `ip_not_allowed` when the caller's address is outside the key's allowlist; `account_mismatch` when a key issued to one account names another in `account_id`, and the sentence names both accounts.
- `404`: No book with that ID in the authorized tenant.
- `429`: The key has spent its allowance for this minute. 600 reads and 120 writes a minute, counted per key, refused calls included. `Retry-After` says how long to wait.
- `503`: The database is briefly out of reach, which is what a failover or a full connection pool looks like from outside. Wait the seconds `Retry-After` names and send the request again. A read is safe to repeat. A write is not, because the connection was lost in the middle of it, so read the object back before sending it a second time. Branch on the status rather than on `code`, which is `POSTGRES_UNAVAILABLE` from one of the two seams that raise this and `SERVICE_UNAVAILABLE` from the other.

---

[API](https://skmtc.dev/tightly/apis/tightly-api.md) · [All operations](https://skmtc.dev/tightly/apis/tightly-api/llms.txt) · [OpenAPI document](https://skmtc.dev/tightly/apis/tightly-api/revisions/199f7eb35afb?raw)
