Appearance
Sell-out
3 reads and 3 writes, on train 2026-11. Scopes: sell_out:read · sell_out:write.
| Operation | Method | Scope | Path |
|---|---|---|---|
| Which accounts reported which periods | GET | sell_out:read | /api/v1/sell-out/coverage |
| What one account has confirmed about its own sell-out reports | GET | sell_out:read | /api/v1/sell-out/declaration |
| Confirm the four things Tightly must know about an account's reports | POST | sell_out:write | /api/v1/sell-out/declaration |
| One account's sell-out by door | GET | sell_out:read | /api/v1/sell-out/doors |
| Land a retailer's own sell-out report against one named account | POST | sell_out:write | /api/v1/sell-out/import |
| Recognise a retailer's report and say what would happen | POST | sell_out:write | /api/v1/sell-out/preview |
Which accounts reported which periods
GET /api/v1/sell-out/coverage
Scopes: sell_out:read
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.
| Optional | In | What it is |
|---|---|---|
weeks | query | How many weeks of axis to return. Clamped to 1 to 52. |
commitment_id | query | Optional tenant-local book. |
as_of | query | ISO date for commitment scope; requires commitment_id. Defaults to today. |
Tightly-Version | header | The date train to answer on. |
bash
curl "https://api.app.tightly.io/api/v1/sell-out/coverage" \
-H "Authorization: Bearer $TIGHTLY_API_KEY"What it answers
json
{
"data": {
"accounts": [
{
"account": "Selfridges",
"account_id": "tp_00417",
"coverage_sentence": "2 weeks reported",
"grain": "store",
"last_reported": "2026-08-24",
"period_grain": "week",
"periods_reported": 2,
"reported": {
"2026-08-10/2026-08-16": {
"grain": "week",
"period_days": 7,
"period_end": "2026-08-16",
"period_start": "2026-08-10",
"units": 1841,
"variants": 96
},
"2026-08-24/2026-08-30": {
"grain": "week",
"period_days": 7,
"period_end": "2026-08-30",
"period_start": "2026-08-24",
"units": null,
"variants": 96
}
},
"weekly_rate_absent": null
},
{
"account": "Le Bon Marché",
"account_id": "tp_00902",
"coverage_sentence": null,
"grain": null,
"last_reported": null,
"period_grain": null,
"periods_reported": 0,
"reported": {}
…
}
]
}
}What it refuses
- 400 Empty commitment_id, invalid as_of, or as_of without commitment_id.
- 401
key_invalid: "The API key is not valid." 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
plan_excludes: "This organisation's plan does not include Tightly Connect. It is sold with Essentials+." The key may not reach this operation.plan_excludeswhen Sell-out is sold with Essentials+ and this organisation's plan does not include it;scope_missingwhen the key does not hold Sell-out;organization_mismatchwhen the path names an organisation that is not the key's;ip_not_allowedwhen the caller's address is outside the key's allowlist;account_mismatchwhen a key issued to one account names another inaccount_id, and the sentence names both accounts. - 404 No book with that ID in the authorized tenant.
- 429
What one account has confirmed about its own sell-out reports
GET /api/v1/sell-out/declaration
Scopes: sell_out:read
The four answers one account has confirmed about its own sell-out reports, or null when nobody has answered yet: which day its week ends on, whether the figures cover stores, distribution centres or the whole account, the currency and whether prices include tax, and whether a product missing from a file means it sold none or means the retailer did not report it. account_id is required.
| Required | In | What it is |
|---|---|---|
account_id | query | The account. |
| Optional | In | What it is |
|---|---|---|
Tightly-Version | header | The date train to answer on. |
bash
curl "https://api.app.tightly.io/api/v1/sell-out/declaration?account_id=<account_id>" \
-H "Authorization: Bearer $TIGHTLY_API_KEY"What it answers
json
{
"data": {
"absence_convention": "row_omitted",
"confirmed_at": "2026-08-24T10:41:03+00:00",
"confirmed_by": "66c1f0a2e4b09a3d5c7f1a02",
"confirmed_by_name": "Priya Raman",
"covers": "Each store",
"currency": "GBP",
"location_grain": "store",
"missing_row_means": "The retailer did not report it",
"price_tax_basis": "inclusive",
"prices_are": "Including tax",
"week_anchor_dow": 6,
"week_ends_on": "Sunday"
},
"message": {
"desc": "OK",
"service": "sell_out",
"severity": "SUCCESS"
}
}What it refuses
- 400 No account was named
- 401
key_invalid: "The API key is not valid." 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
plan_excludes: "This organisation's plan does not include Tightly Connect. It is sold with Essentials+." The key may not reach this operation.plan_excludeswhen Sell-out is sold with Essentials+ and this organisation's plan does not include it;scope_missingwhen the key does not hold Sell-out;organization_mismatchwhen the path names an organisation that is not the key's;ip_not_allowedwhen the caller's address is outside the key's allowlist;account_mismatchwhen a key issued to one account names another inaccount_id, and the sentence names both accounts. - 429
Confirm the four things Tightly must know about an account's reports
POST /api/v1/sell-out/declaration
Scopes: sell_out:write
Records a person's answers for one account: location_grain (store, dc or none), currency, price_tax_basis and absence_convention, all four required in one call, with account_id in the query. Re-confirming REPLACES them, which is how a retailer that has changed its reporting week is corrected.
| Required | In | What it is |
|---|---|---|
account_id | query | The account these answers are for. An id, never a name. |
| Optional | In | What it is |
|---|---|---|
Tightly-Version | header | The date train to answer on. |
Idempotency-Key | header | A string of your own, up to 255 characters, that makes this write safe to retry. |
bash
curl -X POST "https://api.app.tightly.io/api/v1/sell-out/declaration?account_id=<account_id>" \
-H "Authorization: Bearer $TIGHTLY_API_KEY" \
-H "Content-Type: application/json" \
-d @body.jsonWhat to send, as body.json
json
{
"absence_convention": "explicit_zero",
"currency": "<currency>",
"location_grain": "store",
"price_tax_basis": "inclusive",
"source_profile_key": "<source_profile_key>",
"week_anchor_dow": 1
}What it answers
json
{
"data": {
"absence_convention": "row_omitted",
"confirmed_at": "2026-08-24T10:41:03+00:00",
"confirmed_by": "66c1f0a2e4b09a3d5c7f1a02",
"confirmed_by_name": "Priya Raman",
"covers": "Each store",
"currency": "GBP",
"location_grain": "store",
"missing_row_means": "The retailer did not report it",
"price_tax_basis": "inclusive",
"prices_are": "Including tax",
"week_anchor_dow": 6,
"week_ends_on": "Sunday"
},
"message": {
"desc": "recorded; this account's reports can now be imported",
"service": "sell_out",
"severity": "SUCCESS"
}
}What it refuses
- 400 No account was named, or one of the four is missing or outside its closed list. The refusal says which and what to answer.
- 401
key_invalid: "The API key is not valid." 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
plan_excludes: "This organisation's plan does not include Tightly Connect. It is sold with Essentials+." The key may not reach this operation.plan_excludeswhen Sell-out is sold with Essentials+ and this organisation's plan does not include it;scope_missingwhen the key does not hold Sell-out;organization_mismatchwhen the path names an organisation that is not the key's;ip_not_allowedwhen the caller's address is outside the key's allowlist;account_mismatchwhen a key issued to one account names another inaccount_id, and the sentence names both accounts. - 429
One account's sell-out by door
GET /api/v1/sell-out/doors
Scopes: sell_out:read
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.
| Required | In | What it is |
|---|---|---|
account_id | query | The account. |
| Optional | In | What it is |
|---|---|---|
weeks | query | How many weeks back to read. Clamped to 1 to 52. |
product_id | query | Narrow every figure to one style, resolved to all of its variants. |
variant_id | query | 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. |
family_id | query | Narrow every figure to one product family. |
category | query | 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. |
Tightly-Version | header | The date train to answer on. |
bash
curl "https://api.app.tightly.io/api/v1/sell-out/doors?account_id=<account_id>" \
-H "Authorization: Bearer $TIGHTLY_API_KEY"What it answers
json
{
"data": {
"account_total": {
"by_week": {
"2026-06-08/2026-06-14": {
"grain": "week",
"on_hand": 742,
"period_days": 7,
"period_end": "2026-06-14",
"period_start": "2026-06-08",
"units": 201,
"variants": 96
},
"2026-06-15/2026-06-21": {
"grain": "week",
"on_hand": 701,
"period_days": 7,
"period_end": "2026-06-21",
"period_start": "2026-06-15",
"units": 188,
"variants": 96
},
"2026-06-22/2026-06-28": {
"grain": "week",
"on_hand": 663,
"period_days": 7,
"period_end": "2026-06-28",
"period_start": "2026-06-22",
"units": 176,
"variants": 95
},
"2026-06-29/2026-07-05": {
"grain": "week",
"on_hand": 612,
"period_days": 7,
"period_end": "2026-07-05",
"period_start": "2026-06-29",
"units": 231,
"variants": 131
}
…
}
}
}
}What it refuses
- 400 No account was named.
- 401
key_invalid: "The API key is not valid." 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
plan_excludes: "This organisation's plan does not include Tightly Connect. It is sold with Essentials+." The key may not reach this operation.plan_excludeswhen Sell-out is sold with Essentials+ and this organisation's plan does not include it;scope_missingwhen the key does not hold Sell-out;organization_mismatchwhen the path names an organisation that is not the key's;ip_not_allowedwhen the caller's address is outside the key's allowlist;account_mismatchwhen a key issued to one account names another inaccount_id, and the sentence names both accounts. - 429
Land a retailer's own sell-out report against one named account
POST /api/v1/sell-out/import
Scopes: sell_out:write
Import a retailer report for the explicitly named account_id; the account is never inferred. The account declaration and mapping must be confirmed. Reports without a date require week_start (and week_end where appropriate). Unrecognized formats or incomplete declarations are refused. Existing idempotency and account-bound authorization apply to both input sources. Send original CSV/XLSX/XLS bytes inline (up to 8 MiB), or use /files/presigned-url and send application/json with exactly {"s3_key":"organization/user/uploaded-file.csv"} (up to 100 MiB). Uploaded keys must belong to the authenticated organization; foreign keys and URLs are refused. Both sources use the same native retailer parser and preserve its period and provenance. Use Confirm the four things Tightly must know about an account's reports to confirm account answers before importing; use Recognise a retailer's report and say what would happen to inspect the same file without writing. not_matched.by_reason counts every row by resolver outcome, including matches; not_matched.total counts refused rows, and not_matched.by_reason_words explains each outcome. Repeated files report both rows submitted and rows actually changed. Requires Tightly Connect; unavailable plans return 403 plan_excludes.
| Required | In | What it is |
|---|---|---|
account_id | query | The account that sent this report. An id, never a name. "Intersport" is a dozen buying groups. |
| Optional | In | What it is |
|---|---|---|
week_start | query | The week this file covers (YYYY-MM-DD), for the retailers whose export carries no date anywhere in it. |
Tightly-Version | header | The date train to answer on. |
Idempotency-Key | header | A string of your own, up to 255 characters, that makes this write safe to retry. |
Accepts application/json, application/octet-stream, application/vnd.ms-excel, application/vnd.openxmlformats-officedocument.spreadsheetml.sheet and text/csv; the curl sends application/json.
bash
curl -X POST "https://api.app.tightly.io/api/v1/sell-out/import?account_id=<account_id>" \
-H "Authorization: Bearer $TIGHTLY_API_KEY" \
-H "Content-Type: application/json" \
-d @body.jsonWhat to send, as body.json
json
{
"s3_key": "<s3_key>"
}What it answers
json
{
"data": {
"confirmed": {
"absence_convention": "row_omitted",
"confirmed_at": "2026-08-24T10:41:03+00:00",
"confirmed_by": "66c1f0a2e4b09a3d5c7f1a02",
"confirmed_by_name": "Priya Raman",
"covers": "Each store",
"currency": "GBP",
"location_grain": "store",
"missing_row_means": "The retailer did not report it",
"price_tax_basis": "inclusive",
"prices_are": "Including tax",
"week_anchor_dow": 6,
"week_ends_on": "Sunday"
},
"needs_you": [],
"not_matched": {
"by_reason": {
"matched": 62916,
"unknown_product": 118
},
"by_reason_words": {
"matched": "we carry this product",
"unknown_product": "a valid barcode for a product that is not in your catalogue"
},
"examples": [
"5010029000016"
],
"total": 118
},
"period": {
"end": "2026-08-30",
"start": "2026-08-24"
},
"recognised_as": "Intersport weekly sell-through",
"refused": null,
"reorder_rules": null,
"rows_changed": 4118,
"rows_read": 63034
…
}
}What it refuses
- 400 No file, no account, a format we do not recognise yet, a file too large to send inline, or a declaration this account has not confirmed, each with its own sentence, and the same payload so the four questions can be answered without re-uploading.
- 401
key_invalid: "The API key is not valid." 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
plan_excludes: "This organisation's plan does not include Tightly Connect. It is sold with Essentials+." The key may not reach this operation.plan_excludeswhen Sell-out is sold with Essentials+ and this organisation's plan does not include it;scope_missingwhen the key does not hold Sell-out;organization_mismatchwhen the path names an organisation that is not the key's;ip_not_allowedwhen the caller's address is outside the key's allowlist;account_mismatchwhen a key issued to one account names another inaccount_id, and the sentence names both accounts. - 429
Recognise a retailer's report and say what would happen
POST /api/v1/sell-out/preview
Scopes: sell_out:write
Preview the recognized profile, period, matched rows and refusal reasons without importing sell-out. account_id is optional; when supplied, the account's confirmed declaration and mapping are used. Unrecognized account reports return a mapping proposal for review. Preview outcome counts use the same resolver as import; rows_written and rows_changed remain zero. Send original CSV/XLSX/XLS bytes inline (up to 8 MiB), or use /files/presigned-url and send application/json with exactly {"s3_key":"organization/user/uploaded-file.csv"} (up to 100 MiB). Uploaded keys must belong to the authenticated organization; foreign keys and URLs are refused. Both sources use the same native retailer parser and preserve its period and provenance. Use Land a retailer's own sell-out report against one named account to record the report; preview never imports it. Use What one account has confirmed about its own sell-out reports to check the account declaration without supplying a file. Requires Tightly Connect; unavailable plans return 403 plan_excludes.
| Optional | In | What it is |
|---|---|---|
account_id | query | The account, when it is known. |
file_name | query | What the file was called, recorded against the mapping proposal so a person reviewing it later can tell which upload it was matched from. |
Tightly-Version | header | The date train to answer on. |
Idempotency-Key | header | A string of your own, up to 255 characters, that makes this write safe to retry. |
Accepts application/json, application/octet-stream, application/vnd.ms-excel and text/csv; the curl sends application/json.
bash
curl -X POST "https://api.app.tightly.io/api/v1/sell-out/preview" \
-H "Authorization: Bearer $TIGHTLY_API_KEY" \
-H "Content-Type: application/json" \
-d @body.jsonWhat to send, as body.json
json
{
"s3_key": "<s3_key>"
}What it answers
json
{
"data": {
"confirmed": null,
"needs_you": [
{
"detail": "This retailer lists only movement in the weeks we have seen.",
"kind": "absence_convention",
"proposed": "row_omitted",
"question": "Does a product missing from this file mean it sold none?"
}
],
"not_matched": {
"by_reason": {
"matched": 62916,
"unknown_product": 118
},
"by_reason_words": {
"matched": "we carry this product",
"unknown_product": "a valid barcode for a product that is not in your catalogue"
},
"examples": [
"5010029000016"
],
"total": 118
},
"period": {
"end": "2026-08-30",
"start": "2026-08-24"
},
"recognised_as": "Intersport weekly sell-through",
"refused": null,
"rows_changed": 0,
"rows_read": 63034,
"rows_written": 0,
"we_read_it_as": {
"absence_convention": "row_omitted",
"confirmed_at": "2026-08-24T10:41:03+00:00",
"confirmed_by": "66c1f0a2e4b09a3d5c7f1a02",
"confirmed_by_name": "Priya Raman",
"covers": "Each store"
…
}
}
}What it refuses
- 400 No file was received, the file is over 8 MB, or the format is not recognised yet. The refusal says what to do next. An unrecognised file is a request for a profile, not a dead end.
- 401
key_invalid: "The API key is not valid." 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
plan_excludes: "This organisation's plan does not include Tightly Connect. It is sold with Essentials+." The key may not reach this operation.plan_excludeswhen Sell-out is sold with Essentials+ and this organisation's plan does not include it;scope_missingwhen the key does not hold Sell-out;organization_mismatchwhen the path names an organisation that is not the key's;ip_not_allowedwhen the caller's address is outside the key's allowlist;account_mismatchwhen a key issued to one account names another inaccount_id, and the sentence names both accounts. - 429