Skip to content

Sell-out ​

3 reads and 3 writes, on train 2026-11. Scopes: sell_out:read · sell_out:write.

OperationMethodScopePath
Which accounts reported which periodsGETsell_out:read/api/v1/sell-out/coverage
What one account has confirmed about its own sell-out reportsGETsell_out:read/api/v1/sell-out/declaration
Confirm the four things Tightly must know about an account's reportsPOSTsell_out:write/api/v1/sell-out/declaration
One account's sell-out by doorGETsell_out:read/api/v1/sell-out/doors
Land a retailer's own sell-out report against one named accountPOSTsell_out:write/api/v1/sell-out/import
Recognise a retailer's report and say what would happenPOSTsell_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.

OptionalInWhat it is
weeksqueryHow many weeks of axis to return. Clamped to 1 to 52.
commitment_idqueryOptional tenant-local book.
as_ofqueryISO date for commitment scope; requires commitment_id. Defaults to today.
Tightly-VersionheaderThe 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_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

Try it in the reference

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.

RequiredInWhat it is
account_idqueryThe account.
OptionalInWhat it is
Tightly-VersionheaderThe 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_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.
  • 429

Try it in the reference

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.

RequiredInWhat it is
account_idqueryThe account these answers are for. An id, never a name.
OptionalInWhat it is
Tightly-VersionheaderThe date train to answer on.
Idempotency-KeyheaderA 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.json

What 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_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.
  • 429

Try it in the reference

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.

RequiredInWhat it is
account_idqueryThe account.
OptionalInWhat it is
weeksqueryHow many weeks back to read. Clamped to 1 to 52.
product_idqueryNarrow every figure to one style, resolved to all of its variants.
variant_idqueryNarrow 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_idqueryNarrow every figure to one product family.
categoryqueryNarrow 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-VersionheaderThe 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_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.
  • 429

Try it in the reference

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.

RequiredInWhat it is
account_idqueryThe account that sent this report. An id, never a name. "Intersport" is a dozen buying groups.
OptionalInWhat it is
week_startqueryThe week this file covers (YYYY-MM-DD), for the retailers whose export carries no date anywhere in it.
Tightly-VersionheaderThe date train to answer on.
Idempotency-KeyheaderA 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.json

What 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_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.
  • 429

Try it in the reference

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.

OptionalInWhat it is
account_idqueryThe account, when it is known.
file_namequeryWhat the file was called, recorded against the mapping proposal so a person reviewing it later can tell which upload it was matched from.
Tightly-VersionheaderThe date train to answer on.
Idempotency-KeyheaderA 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.json

What 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_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.
  • 429

Try it in the reference

Tightly API, version 2026-11.