---
title: "get sales table"
method: GET
path: "/api/v1/sales/table"
tags: ["sales"]
---

# get sales table

`GET /api/v1/sales/table`

## Request

- Base URL: `https://api.app.tightly.io`
- URL: `https://api.app.tightly.io/api/v1/sales/table`
- Auth: HTTP bearer

A page of sales per variant or per product: gross sales, discounts, returns, taxes, net sales, net items sold, returned units, return rate, sell-through rate, and the units and revenue lost to stockouts, with `filtered_max_size` for the size of the filtered set. A variant row also carries `price_sensitivity`, the pricing engine's tier, and `price_sensitivity_absence`, which says which blank a null tier is; a product row carries neither.

`type=variants` is the default and `type=products` aggregates to the product. Page with `limit` and `offset`, narrow with `filter_args` on date, location, sales channel, vendor, collection and custom fields, order with `sort_args`, and set `export=true` to receive a download URL instead of rows. `comparison_date_gte` and `comparison_date_lte` add the comparison period the change figures are measured against.

`metric_columns` adds any of the library's metrics to each row: profitability, prices, stock, plan gaps and forecast accuracy. A metric is null where its evidence is missing, never zero, and the row's `metric_reasons` says why. `include_totals=true` adds `totals` over the whole filtered set.

A key issued to one account reads that account's sales only: its rows, totals, shares and comparison figures are measured over that account's orders, and an `account_id` filter naming another account is refused.

Scope: `sales:read`.

## Query parameters

- `limit` integer
- `offset` integer
- `filter_args` string
- `sort_args` string
- `search` string
- `type` 'variants' | 'products'
- `export` boolean
- `comparison_date_gte` string, date
- `comparison_date_lte` string, date
- `metric_columns` string
- `columns` string
- `commitment_id` string
- `plan_snapshot_id` string
- `forecast_vintage_week` string, date
- `include_totals` boolean
- `age_days` integer

## Headers

- `Tightly-Version` string

## Response `200`

The sales table, or an export url

- object
  - `data` object: A page of rows with `offset`, `size` and `filtered_max_size`, or, with `export=true`, a single `url` to download instead.
    - `filtered_max_size` integer
    - `offset` integer
    - `rows` SalesTableVariantRow[]
      - `aged_stock_cost` number, nullable: Historical cost value of the same aged closing stock units.
      - `aged_stock_share` number, nullable: Aged units divided by eligible closing stock units.
      - `aged_stock_units` number, nullable: Closing units older than the selected age threshold, based on receipt or lot evidence.
      - `available_units` number, nullable: Reported sellable balance in the historical stock snapshot at the selected as-of date. Does not calculate availability to promise from current reservations or holds.
      - `average_inventory_cost` number, nullable: Time-weighted owned inventory value at historical cost across the selected period.
      - `average_original_price` number, nullable: Quantity-weighted original unit price on the same positive sales.
      - `average_selling_price` number, nullable: Net sales divided by net units, including the recorded returns basis.
      - `average_stock_units` number, nullable: Time-weighted stock units across the selected period; requires historical coverage.
      - `average_unit_cost` number, nullable: Weighted recognised unit cost on the matching sales and returns basis.
      - `category` string, nullable
      - `closing_stock_units` number, nullable: Owned stock units at the end of the selected period.
      - `cogs` number, nullable: Historical cost recognised for the same sold and returned units as net sales.
      - `current_markdown_depth` number, nullable: Reduction from matched original offer prices to effective prices at the period end.
      - `current_price_max` number, nullable: The highest effective offer price at the period end, beside current_price_min.
      - `current_price_min` number, nullable: Effective offer prices at the period end, within the selected commercial scope.
      - `custom_fields` object, nullable
      - `days_since_last_sale` number, nullable: Days between the selected period end and the most recent eligible sale in history.
      - `discount_per_unit` number, nullable: Recorded discounts divided by corresponding eligible sold units.
      - `discount_rate` number, nullable: Recorded discount amount divided by corresponding pre-discount sales.
      - `discounts` number, nullable: Null where no line in the window records a discount: an unrecorded discount is not a measured zero. A recorded 0.00 still reads 0.
      - `forecast_bias` number, nullable: Forecast minus actual divided by actual over matched completed buckets. Positive means overforecast.
      - `forecast_wape` number, nullable: Sum of absolute forecast errors divided by total actuals, using a forecast saved before the actuals.
      - `forecast_weeks_of_supply` number, nullable: Future weekly forecast demand covered by eligible stock using the selected forecast curve.
      - `full_price_quantity_share` number, nullable: Classified full-price units divided by eligible classified units; requires original-price evidence.
      - `full_price_revenue_share` number, nullable: Classified full-price revenue divided by eligible classified revenue; requires original-price evidence.
      - `full_price_sales_share` number, nullable: Sales with zero recorded transaction discount; does not establish original full-price status.
      - `full_price_sell_through_rate` number, nullable: Classified full-price net units divided by stock made available on the same lifecycle basis.
      - `full_price_units_share` number, nullable: Units with zero recorded transaction discount; does not establish original full-price status.
      - `gmroi` number, nullable: Selected-period gross profit divided by time-weighted average inventory at cost. Not annualised.
      - `gross_items_sold` number, nullable: Positive sales quantities before returns in the selected period.
      - `gross_margin` number, nullable: Gross profit divided by net sales; not markup on cost.
      - `gross_profit` number, nullable: Net sales less matched cost of sales.
      - `gross_profit_per_unit` number, nullable: Gross profit divided by corresponding net units.
      - `gross_sales` number
      - `historical_days_cover` number, nullable: Eligible closing units divided by measured daily sales velocity for the selected period.
      - `in_stock_rate` number, nullable: Ranged SKU-location-days in stock divided by all eligible ranged SKU-location-days.
      - `initial_margin` number, nullable: Margin on the first recorded positive sale, using its historical ticket price and immutable cost layer in the same currency. Does not establish the original launch margin.
      - `inventory_turns` number, nullable: Selected-period cost of sales divided by average inventory at cost. Not annualised.
      - `lost_sales_dated_days` integer, nullable: Stock-out days in the window that were priced at the rate held for that very day.
      - `lost_sales_latest_days` integer, nullable: Stock-out days priced at the variant's current rate because no dated rate survived for them. With lost_sales_dated_days it sums to the window's stock-out days; all latest-days means the figure equals what today's rate alone would give.
      - `lost_sales_units` number, nullable: Units the empty shelf cost, each stock-out day priced at the rate stored for that day; a day with no stored rate is priced at the variant's current rate.
      - `margin_gap` number, nullable: Actual gross margin less matched target, in percentage points.
      - `margin_leak` number, nullable: Absolute discounts plus absolute returns for the period.
      - `margin_target` number, nullable: Declared margin target under the same recognised margin basis.
      - `metric_reasons` object: Why a library metric on this row is null, keyed by the metric's name, with a code such as select_commitment or historical_transaction_cost_or_sales_missing. A metric with no entry here was measured, or was not requested.
      - `missed_revenue` number, nullable
      - `net_items_sold` integer
      - `net_items_sold_by_channel` object: Mapping of sales_channel_id to net items sold in that channel.
      - `net_sales` number
      - `net_sales_change` number, nullable: Signed net sales delta against the comparison window; null without one.
      - `net_sales_per_day` number, nullable: Net sales divided by calendar days in the selected period.
      - `net_sales_swing` number, nullable: Absolute value of net_sales_change; null without a comparison window.
      - `on_order_units` number, nullable: Outstanding confirmed inbound units at the as-of date; drafts excluded.
      - `opening_stock_units` number, nullable: Owned stock units immediately before the selected period starts.
      - `order_count` number, nullable: Distinct orders containing the matched product or variant.
      - `original_price_max` number, nullable: The highest original transaction unit price, beside original_price_min.
      - `original_price_min` number, nullable: Original transaction unit prices before discounts; not current catalogue prices.
      - `out_of_stock_days` number, nullable: Days in the period on which the item was empty at every location in scope, the rule the grid's stockout days use, read from dated stock snapshots. Null where a snapshot is missing.
      - `price_sensitivity` 'STILL_LEARNING' | 'INCONSISTENT' | 'LOW' | 'MODERATE' | 'HIGH' | 'null', nullable: The pricing engine's tier for this line. Null means the engine holds no reading for it; on THIS read a null is never coalesced to STILL_LEARNING, which is a verdict the engine reached about a line it did look at, and price_sensitivity_absence says which blank the null is. Other surfaces are not covered by that sentence: the analytics bindings behind Ask Tightly still substitute STILL_LEARNING for a missing tier.
      - `price_sensitivity_absence` 'not_run' | 'not_scored' | 'not_measured' | 'null', nullable: Which blank a null price_sensitivity is. Set only where the tier is null, and null where a tier is served. not_run: the engine has never measured this store -- no tier table on this tenant, or no run on record and no rows anywhere in it. not_scored: it has measured the store and holds no reading for this line. not_measured: a probe failed and the platform cannot say, which is never reported as not_scored. It does not say whether this line's product pool could lend it a number. That split (borrowed against insufficient) needs the per-variant price evidence, which is one query per line; GET /variants/{variant_id}/price-sensitivity answers it, with the sentence, for a line a reader opens.
      - `price_sensitivity_absence_word` 'Not scored yet' | 'Not scored' | 'Not measured' | 'null', nullable: The word the cell prints for the code beside it, served rather than composed on the page, one per price_sensitivity_absence code and in the same order. Null wherever price_sensitivity_absence is null, and null for a code shipped before its copy.
      - `price_sensitivity_computed_at` string, date-time, nullable: When the tier beside it was measured, ISO-8601 in UTC, so the column carries the run's own stamp rather than the reader's clock. Null where there is no tier.
      - `product_id` string
      - `product_image` string, nullable
      - `product_title` string, nullable: Null where the catalogue holds no product title.
      - `profit_share` number, nullable: Row gross profit divided by gross profit across the full filtered result.
      - `realized_average_price` number, nullable: Quantity-weighted positive-sale unit price after discounts; refunds are separate.
      - `realized_price_max` number, nullable: The highest observed positive-sale unit price after discounts, beside realized_price_min.
      - `realized_price_min` number, nullable: Observed positive-sale unit prices after discounts in the selected period.
      - `receipts_units` number, nullable: Recognised inbound units during the selected period, within its inventory boundary.
      - `reserved_units` number, nullable: Units reserved or held at the same as-of date as available stock.
      - `return_rate` number, nullable
      - `returned_units` number, nullable
      - `returns` number
      - `sales_gap` number, nullable: Actual net sales less the matched selected plan target.
      - `sales_gap_pct` number, nullable: Sales gap divided by the matched selected plan target.
      - `sales_plan` number, nullable: Selected saved plan sales target for the matching product scope and elapsed period.
      - `sales_share` number, nullable: Row net sales divided by net sales across the full filtered result.
      - `season_sell_through_rate` number, nullable: Sell-through from the selected commitment or season start on its matched inventory basis.
      - `sell_through_gap` number, nullable: Actual sell-through less matched target, in percentage points.
      - `sell_through_rate` number, nullable
      - `sell_through_target` number, nullable: Declared sell-through target for the same lifecycle and scope.
      - `selling_days` number, nullable: Distinct dates with positive sales in the selected period.
      - `shopify_tags` string[], nullable
      - `sku` string, nullable
      - `stockout_days_inferred` integer, nullable: Of the window's stock-out days, how many were read out of the order history rather than snapshotted at the time. Those days are evidence, not measurement: the line sold, went silent for longer than its own selling cadence explains, and sold again. Present them as inferred and never as measured. 0 where every day was snapshotted, which is every book whose history has not been backfilled.
      - `taxes` number, nullable: Null where no line in the window records tax, on the same terms as discounts. A recorded 0.00 still reads 0.
      - `total_sales` number, nullable: Net sales plus the tax collected on it, so it is null wherever no line in the window records tax. Read a null as not measured.
      - `units_gap` number, nullable: Actual net units less the matched selected unit target.
      - `units_gap_pct` number, nullable: Unit gap divided by the matched selected unit target.
      - `units_per_day` number, nullable: Net units divided by calendar days in the selected period.
      - `units_per_order` number, nullable: Eligible units divided by distinct matching orders.
      - `units_plan` number, nullable: Selected saved plan unit target for the matching product scope and elapsed period.
      - `units_share` number, nullable: Row net units divided by net units across the full filtered result.
      - `variant_id` string
      - `variant_title` string, nullable: Null where the catalogue holds no variant title, as a single-variant or marketplace listing often does.
    - `size` integer
    - `totals` object, nullable: With include_totals=true, the grid's figures and the computed library metrics measured over the whole filtered set rather than summed from the page, with row_count (rows in the filtered set), window_weeks (the period's length in weeks) and metric_reasons (why a total is null, by metric or by metric family). Null without include_totals and on exports.
    - `url` string: Present only on an export.
  - `message` object: severity, service and desc.

## Other responses

- `400`: Bad Request - Invalid request parameters or validation errors
- `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. `scope_missing` when the key does not hold Sales; `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 an `account_id` filter, in a sentence naming both.
- `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.
- `500`: Internal Server Error - Database connection or processing errors
- `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)
