One account's sell-out by door, the grain a plan is actually delivered at
One account's sell-out by door: units sold, stock on hand, variants and weeks reported, the last week reported and the sell-through rate per door, ranked by what they sold, with the account's totals beside.
account_id is required and weeks bounds the window. "24 doors clearing 98%" and "3 doors carrying the whole number" are the same headline and opposite buys.
Four optional narrowings, composing with AND: variant_id is the SKU, product_id the style, family_id the product family, category the catalogue category matched on its id or its name, exactly and case-sensitively. Every per-door figure narrows, by_week included, and all four are echoed. Unstated, nothing narrows.
The door list is the same under every filter: a door with nothing in scope keeps its row and reads units: 0, on_hand and sell_through null. scope_reason says why a narrowing could not be made.
sell_through is null rather than 0 wherever either side is unreported, since folding an unreported side to zero manufactures the 100% and 0% rates the write path refuses. grain is read from the rows: an account reporting one total answers grain: account and an empty doors list.
units and on_hand are not summed the same way. units is a flow, so a window of it is that window's sales. on_hand is a level: each variant's LAST reported reading at that door, summed within the door.
Sold with Essentials+: without Tightly Connect the answer is 403 plan_excludes.
Scope: sell_out:read.
Request
- Base URL: https://api.app.tightly.io
- URL: https://api.app.tightly.io/api/v1/sell-out/doors
- Auth: HTTP bearer
Query parameters
The account. An id, never a name. Nothing in a retailer's export reliably says who sent it, a filename is not evidence, and a wrong guess files a whole retailer's sell-through under another account's name.
How many weeks back to read. Clamped to 1 to 52.
Narrow every figure to one style, resolved to all of its variants. A plan is decided per garment per door, and unstated this read answers for the whole account. Sent empty it is unstated, not a style with no id.
Narrow every figure to one SKU. Bound to what the account reported rather than to the catalogue, so a SKU nobody has mapped still answers.
Narrow every figure to one product family. A catalogue with no families answers a page of zeros with scope_reason saying so, rather than a filter that quietly matched everything.
Narrow every figure to one catalogue category, matched on the category's id or on its name, because the word a merchandiser has is the name. The match is exact and case-sensitive: "knitwear" does not find "Knitwear".
Headers
The date train to answer on. One train exists today, 2026-11, the one this reference describes and the one every key is minted on, so the call answers on it with or without this header. Send it anyway, so a client built now names its train from the start.
Response
One row per door with units, on-hand, variants, weeks reported and sell-through, plus the money the account reported and the units that came back, the account total, the grain the account actually reports at, the four narrowings echoed (product_id, variant_id, family_id, category), each null for the whole account, and scope_reason where a narrowing could not be made. A door with nothing in the scope is a row with units: 0, never a dropped row.
value_sold_cents is what SHOPPERS SPENT at that door over the window, in whole cents of currency. Divide by 100 to show it, and show the code beside it. It is the ACCOUNT's takings at their own shelf price, not this business's revenue: it is larger, and it lands in a different period. returned is units shoppers brought back and is NOT subtracted from units; a return lands in the week the shopper walked in rather than the week they bought, so a window can carry more returns than sales, and a return rate is the two divided over a range wide enough to cover both.
MOST ACCOUNTS REPORT NEITHER, AND SAY SO IN WORDS. Measured across nine real retailer exports, four report no value and eight report no returns. Where an account has never sent one the figure is null and value_absent / returns_absent carry the sentence "Not reported". A null rendered as 0 would claim shoppers spent nothing, which is the one thing this read must not say. Where a window spans two currencies, because an account's confirmed currency was corrected mid-history, value_sold_cents is withheld and value_absent reads "Reported in more than one currency": adding pence to cents is a number with no unit, and nothing here converts between them. Those two sentences are the whole of value_absent, and returns_absent only ever carries the first. Branch on whether the field is null and show the sentence, rather than matching its text.
account_total IS THE ACCOUNT'S OWN TOTAL AND NOT A SUM OF THE DOORS. It is served from the rows a retailer sends at account grain, with no door on them, so it is present only where they send those rows and is null where they do not. Where a retailer sends both, the doors and the total are two readings of the same trade: show one or the other and never add them, which would count the account twice. grain says which shape the rows arrived in.
by_week is the periods behind each row, KEYED ON THE PERIOD: unambiguous weekly start dates and otherwise start/end dates, 2026-08-01/2026-08-31, and not on the start alone, because a month and a week that open on the same day are two periods and one would silently replace the other. Each cell holds units, on_hand and variants, and states its own extent with period_start, period_end, period_days and grain, so a face draws ONE band across a month rather than four cells the account never sent. A period the account did not report is absent from the map, never present as a zero: a dead week and a week nobody sent are different facts. A week that carried stock and no sales is present with a null units. The cells' units add up to the row's units; a cell's on_hand is that week's shelf reading and adds up to nothing, since the row's own on_hand is each variant's last reading. The account total carries the same map, which for an account reporting one total is the only period series it has.
last_reported is the START of the newest period a door reported, the date /sell-out/coverage serves for the same account and draws its axis at; last_period_end is the newest closing day it reported. Both are null on a door that sent nothing in the window. Until 25 Sep 2026 last_reported carried the closing day.
EVERY ROW NAMES ITS GRAIN. weeks_reported counts PERIODS and its name predates the accounts that file whole calendar months; period_grain says what a period is, periods_reported is the same count without a unit in its name, coverage_sentence is what a face prints ("4 months reported"), and weekly_rate_absent carries the sentence a per-week rate is refused with on a monthly account. A month is never divided by 4.33, and an account that reports both keeps its weekly rate because it has weeks.
Changes
No recorded changes to this operation across all 1 revision of this API.