Appearance
Sales
15 reads, no writes, on train 2026-11. Scopes: sales:read.
| Operation | Method | Scope | Path |
|---|---|---|---|
| Get sales analytics | GET | sales:read | /api/v1/sales/analytics |
| Get sales filters | GET | sales:read | /api/v1/sales/filters |
| Net sales at category × channel type × period | GET | sales:read | /api/v1/sales/net-sales |
| Get sales new overview | GET | sales:read | /api/v1/sales/new-overview |
| Get sales channel performance | GET | sales:read | /api/v1/sales/performance/channels |
| Get sales table | GET | sales:read | /api/v1/sales/table |
| Get sales velocity event | GET | sales:read | /api/v1/sales/velocity/events/{event_id} |
| Get event analytics | GET | sales:read | /api/v1/sales/velocity/events/{event_id}/analytics |
| Get suggested events | GET | sales:read | /api/v1/sales/velocity/events/suggested |
| Get sales velocity events table | GET | sales:read | /api/v1/sales/velocity/events/table |
| Get sales velocity filters | GET | sales:read | /api/v1/sales/velocity/filters |
| Get forecast model config | GET | sales:read | /api/v1/sales/velocity/forecast-config |
| Get sales velocity table | GET | sales:read | /api/v1/sales/velocity/table |
| Get sales velocity table totals | GET | sales:read | /api/v1/sales/velocity/table/totals |
| Get variant bundle contributions | GET | sales:read | /api/v1/sales/velocity/variants/{variant_id}/bundle-contributions |
Get sales analytics
GET /api/v1/sales/analytics
Scopes: sales:read
Revenue trend and period-over-period comparison, grouped: each group carries revenue_total and revenue_monthly keyed by month, with Overall beside the groups.
| Required | In | What it is |
|---|---|---|
start_date | query | Start date for analytics period (ISO date format) |
end_date | query | End date for analytics period (ISO date format) |
| Optional | In | What it is |
|---|---|---|
in_fiscal_year | query | this or last. Shortcut for getting this fiscal year's or last fiscal year's data. If not provided, start_date and end_date are expected. |
categories | query | Comma separated categories to filter the budget |
fields | query | Comma separated fields to be fetched |
is_sample | query | When true, returns sample/demo data instead of real analytics |
Tightly-Version | header | The date train to answer on. |
bash
curl "https://api.app.tightly.io/api/v1/sales/analytics?start_date=<start_date>&end_date=<end_date>" \
-H "Authorization: Bearer $TIGHTLY_API_KEY"What it answers
json
{
"data": {
"Overall": {
"revenue_monthly": {
"2026-01-01": 164000,
"2026-02-01": 171200
},
"revenue_total": 984000
},
"cash cows": {
"revenue_monthly": {
"2026-01-01": 68800,
"2026-02-01": 71400
},
"revenue_total": 412800
}
},
"message": {
"desc": "",
"service": "sales",
"severity": "INFO"
}
}What it refuses
- 400 Bad Request - Invalid request parameters or validation errors
- 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
scope_missing: "This key cannot read Sales." The key may not reach this operation.scope_missingwhen the key does not hold Sales;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. - 429
Get sales filters
GET /api/v1/sales/filters
Scopes: sales:read
The values the sales table can be filtered on: categories, tags and custom fields as flat lists. Send field to page one field's values with limit, offset and search, which is what a long tag list needs; omit it and every filter comes back at once. Read it to build filter_args for Get sales table out of values that exist.
| Optional | In | What it is |
|---|---|---|
field | query | Optional field name to retrieve paginated values (e.g., 'shopify_tags'). Omit to get all filters. |
search | query | Optional search term to filter results (only applicable when field is provided) |
offset | query | Number of items to skip for pagination (only applicable when field is provided) |
limit | query | Maximum number of items to return per page (only applicable when field is provided) |
Tightly-Version | header | The date train to answer on. |
bash
curl "https://api.app.tightly.io/api/v1/sales/filters" \
-H "Authorization: Bearer $TIGHTLY_API_KEY"What it answers
json
{
"data": {
"categories": [
"Knitwear",
"Outerwear"
],
"custom_fields": [
"fabric_weight"
],
"shopify_tags": [
"core",
"seasonal"
]
},
"message": {
"desc": "",
"service": "sales",
"severity": "INFO"
}
}What it refuses
- 400 Bad Request - Invalid request parameters or validation errors
- 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
scope_missing: "This key cannot read Sales." The key may not reach this operation.scope_missingwhen the key does not hold Sales;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. - 429
Net sales at category × channel type × period
GET /api/v1/sales/net-sales
Scopes: sales:read
Net sales bucketed by category, channel type and period: one row per combination with net_sales and net_items_sold, plus the window, the grain, the calendar, a coverage block counting the orders and dimensions behind the rows, and a basis note. This is the coarse actuals feed a plan is measured against.
| Required | In | What it is |
|---|---|---|
start_date | query | First day of the window (YYYY-MM-DD). |
end_date | query | Last day of the window, inclusive (YYYY-MM-DD). At most 400 days after the start. |
| Optional | In | What it is |
|---|---|---|
commitment_id | query | Scope to this commitment's exact merchandise membership and dates. |
grain | query | The period the rows are bucketed into. |
categories | query | Comma-separated categories to filter to. Filters; never changes the grain. |
channel_types | query | Comma-separated channel types to filter to. |
Tightly-Version | header | The date train to answer on. |
bash
curl "https://api.app.tightly.io/api/v1/sales/net-sales?start_date=<start_date>&end_date=<end_date>" \
-H "Authorization: Bearer $TIGHTLY_API_KEY"What it answers
json
{
"data": {
"basis_note": "Net of refunds, over the organisation's own order book.",
"calendar": "gregorian",
"coverage": {
"categories": 14,
"channel_types": 3,
"orders": 41208,
"unattributed_orders": 0,
"unattributed_reason": null
},
"grain": "month",
"rows": [
{
"category": "Knitwear",
"channel_type": "wholesale",
"net_items_sold": 14020,
"net_sales": 412800,
"period_start": "2026-01-01"
},
{
"category": "Knitwear",
"channel_type": "direct",
"net_items_sold": 2410,
"net_sales": 188400,
"period_start": "2026-01-01"
}
],
"window": {
"end_exclusive": "2026-07-01",
"start": "2026-01-01"
}
},
"message": {
"desc": "",
"service": "sales",
"severity": "INFO"
}
}What it refuses
- 400 A date could not be read,
end_dateprecedesstart_date, the window exceeds 400 days, or the grain is not one the query can strike. - 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
scope_missing: "This key cannot read Sales." The key may not reach this operation.scope_missingwhen the key does not hold Sales;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. - 429
Get sales new overview
GET /api/v1/sales/new-overview
Scopes: sales:read
The sales overview a dashboard is built from: cards carrying a value, the previous period's value and the change per measure, and charts carrying revenue (the measured series), historical (the requested range), forecast (the same length forward) and last_year (the same range shifted back 365 days).
| Optional | In | What it is |
|---|---|---|
filter_args | query | JSON array of filter conditions. Valid filter keys with their allowed operations and value types: |
period | query | Aggregation granularity only. |
cards | query | Comma-separated list of card metrics to include. |
charts | query | Comma-separated list of charts to include. Available charts: revenue, sales_by_variant |
product_option | query | Product option for filtering and analysis |
Tightly-Version | header | The date train to answer on. |
bash
curl "https://api.app.tightly.io/api/v1/sales/new-overview" \
-H "Authorization: Bearer $TIGHTLY_API_KEY"What it answers
json
{
"cards": [
{
"change": 244.59,
"info": "Quantity Sold",
"key": "sold_quantity",
"previous_value": 379,
"unit": "count",
"value": 1306
},
{
"change": 263.46,
"info": "Sales Amount",
"key": "total_revenue",
"previous_value": 8685.65,
"unit": "dollar",
"value": 31569.22
}
],
"charts": {
"forecast": [
{
"date": "2025-10-27",
"sold_quantity": 18.09,
"total_revenue": 408.35
},
{
"date": "2025-11-03",
"sold_quantity": 10.22,
"total_revenue": 230.65
}
],
"historical": [
{
"date": "2025-07-28",
"sold_quantity": 302,
"total_revenue": 7001.44
},
{
"date": "2025-08-04"
…
}
]
}
}What it refuses
- 400 Bad Request - Invalid request parameters or validation errors
- 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
scope_missing: "This key cannot read Sales." The key may not reach this operation.scope_missingwhen the key does not hold Sales;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. - 429
Get sales channel performance
GET /api/v1/sales/performance/channels
Scopes: sales:read
Revenue per sales channel over a window: each channel with its total revenue, the difference against the previous period of the same length, the change as a percentage, and a dated series at the requested granularity.
| Required | In | What it is |
|---|---|---|
start_date | query | Start date for the current period (ISO date format YYYY-MM-DD) |
end_date | query | End date for the current period (ISO date format YYYY-MM-DD). Maximum range is 1 year from start_date. |
| Optional | In | What it is |
|---|---|---|
sales_channel_ids | query | Comma-separated list of sales channel IDs to filter. If not provided, returns all channels. |
granularity | query | Time granularity for the data points (day, week, or month) |
Tightly-Version | header | The date train to answer on. |
bash
curl "https://api.app.tightly.io/api/v1/sales/performance/channels?start_date=<start_date>&end_date=<end_date>" \
-H "Authorization: Bearer $TIGHTLY_API_KEY"What it answers
json
{
"data": {
"channels": [
{
"change_percentage": 12.5,
"channel_name": "Amazon",
"data": [
{
"date": "2025-11-01",
"revenue": 45000
},
{
"date": "2025-11-02",
"revenue": 52000
}
],
"period_difference": 34000.25,
"sales_channel_id": "ch_123",
"total_revenue": 453000.5
},
{
"change_percentage": 8.3,
"channel_name": "Shopify",
"data": [
{
"date": "2025-11-01",
"revenue": 32000
},
{
"date": "2025-11-02",
"revenue": 35000
}
],
"period_difference": 54000,
"sales_channel_id": "ch_456",
"total_revenue": 324000
}
]
},
"message": {
…
}
}What it refuses
- 400 Bad Request - Invalid parameters (e.g., date range exceeds 1 year, invalid date format)
- 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
scope_missing: "This key cannot read Sales." The key may not reach this operation.scope_missingwhen the key does not hold Sales;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. - 429
Get sales table
GET /api/v1/sales/table
Scopes: sales:read
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.
| Optional | In | What it is |
|---|---|---|
limit | query | Pagination limit |
offset | query | Pagination offset |
filter_args | query | JSON array of filter conditions. |
sort_args | query | Comma-separated list of sort arguments (e.g., "+sales_amount,-variant_title"). |
search | query | Search term to filter sales |
type | query | View type - 'variants' shows individual variants, 'products' shows grouped by product. |
export | query | If true, returns an export URL instead of paginated table data |
comparison_date_gte | query | Start of comparison date range. |
comparison_date_lte | query | End of comparison date range. See comparison_date_gte. |
Tightly-Version | header | The date train to answer on. |
bash
curl "https://api.app.tightly.io/api/v1/sales/table" \
-H "Authorization: Bearer $TIGHTLY_API_KEY"What it answers
json
{
"data": {
"filtered_max_size": 4120,
"offset": 0,
"rows": [
{
"category": "Knitwear",
"discounts": 2140,
"gross_sales": 41880,
"lost_sales_units": 40,
"missed_revenue": 1180,
"net_items_sold": 1280,
"net_sales": 37760,
"price_sensitivity": "MODERATE",
"price_sensitivity_absence": null,
"price_sensitivity_absence_word": null,
"price_sensitivity_computed_at": "2026-09-01T06:31:00Z",
"product_id": "4410092",
"product_title": "Terry Crew",
"return_rate": 0.048,
"returned_units": 62,
"returns": 1980,
"sell_through_rate": 0.86,
"shopify_tags": [
"core"
],
"sku": "TB-CREW-BLK-M",
"taxes": 0,
"total_sales": 37760,
"variant_id": "44100920011",
"variant_title": "Black / M"
}
],
"size": 1
},
"message": {
"desc": "",
"service": "sales",
"severity": "INFO"
}
…
}What it refuses
- 400 Bad Request - Invalid request parameters or validation errors
- 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
scope_missing: "This key cannot read Sales." The key may not reach this operation.scope_missingwhen the key does not hold Sales;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. - 429
Get sales velocity event
GET /api/v1/sales/velocity/events/{event_id}
Scopes: sales:read
One sales velocity event: its name and description, its window, the adjustment it applies and that adjustment's configuration, the channels it covers, its status and how many items and variants it holds.
| Required | In | What it is |
|---|---|---|
event_id | path | Unique identifier of the sales velocity event |
| Optional | In | What it is |
|---|---|---|
Tightly-Version | header | The date train to answer on. |
bash
curl "https://api.app.tightly.io/api/v1/sales/velocity/events/<event_id>" \
-H "Authorization: Bearer $TIGHTLY_API_KEY"What it answers
json
{
"data": {
"adjustment_config": {
"uplift_percent": 45
},
"adjustment_type": "uplift_percent",
"awaiting_input": false,
"created_at": "2026-08-02T11:20:00+00:00",
"description": "Site-wide promotion",
"end_date": "2026-11-30",
"id": 412,
"is_active": true,
"is_overridden": false,
"items_count": 184,
"name": "Black Friday 2026",
"sales_channel_id": "61240442",
"sales_channels": [
{
"id": "61240442",
"name": "Wholesale"
}
],
"start_date": "2026-11-27",
"status": "scheduled",
"suggested_event_key": null,
"variants_count": 184
},
"message": {
"desc": "",
"service": "sales_velocity_events",
"severity": "INFO"
}
}What it refuses
- 400 Bad Request - Invalid event ID format
- 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
scope_missing: "This key cannot read Sales." The key may not reach this operation.scope_missingwhen the key does not hold Sales;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. - 404 Not Found - Sales velocity event with specified ID does not exist
- 429
Get event analytics
GET /api/v1/sales/velocity/events/{event_id}/analytics
Scopes: sales:read
How one sales velocity event performed: baseline (the forecast without it), target (the forecast adjusted for it) and actual units, revenue, profit and return on investment, with an accuracy figure.
| Required | In | What it is |
|---|---|---|
event_id | path | Unique identifier of the sales velocity event |
| Optional | In | What it is |
|---|---|---|
group_by | query | Grouping dimension. Omit for a single summary row, or pass "date" for a row per day. |
Tightly-Version | header | The date train to answer on. |
bash
curl "https://api.app.tightly.io/api/v1/sales/velocity/events/<event_id>/analytics" \
-H "Authorization: Bearer $TIGHTLY_API_KEY"What it answers
json
{
"data": {
"group_by": "date",
"rows": [
{
"accuracy": 78,
"actual_profit": 180,
"actual_revenue": 450,
"actual_roi": 66.67,
"actual_unit": 9,
"baseline_profit": 210,
"baseline_revenue": 525,
"baseline_unit": 10.5,
"group_by_col": "2025-12-01",
"target_profit": 231,
"target_revenue": 577.5,
"target_roi": 66.67,
"target_unit": 11.55
}
]
},
"message": {
"desc": "",
"service": "sales_velocity_events",
"severity": "SUCCESS"
}
}What it refuses
- 400 Bad Request - Invalid group_by value
- 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
scope_missing: "This key cannot read Sales." The key may not reach this operation.scope_missingwhen the key does not hold Sales;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. - 404 Not Found - Sales velocity event with specified ID does not exist
- 429
Get suggested events
GET /api/v1/sales/velocity/events/suggested
Scopes: sales:read
The events this organisation's industry is offered for a year, such as Black Friday: each with the real event window, a recommended preparation window (suggested_start_date and suggested_end_date), the lift range to expect, and whether it has already been dismissed.
| Optional | In | What it is |
|---|---|---|
year | query | Year to retrieve suggested events for. Defaults to the current year. |
Tightly-Version | header | The date train to answer on. |
bash
curl "https://api.app.tightly.io/api/v1/sales/velocity/events/suggested" \
-H "Authorization: Bearer $TIGHTLY_API_KEY"What it answers
json
{
"data": {
"events": [
{
"adjustment_max": 50,
"adjustment_min": 30,
"end_date": "2026-11-27",
"event_key": "black_friday_2026",
"is_dismissed": false,
"name": "Black Friday",
"start_date": "2026-11-27",
"suggested_end_date": "2026-11-27",
"suggested_start_date": "2026-11-13"
},
{
"adjustment_max": 40,
"adjustment_min": 25,
"end_date": "2026-11-30",
"event_key": "cyber_monday_2026",
"is_dismissed": false,
"name": "Cyber Monday",
"start_date": "2026-11-30",
"suggested_end_date": "2026-11-30",
"suggested_start_date": "2026-11-27"
}
]
},
"message": {
"desc": "",
"service": "sales_velocity_events",
"severity": "INFO"
}
}What it refuses
- 400 Bad Request - Invalid parameters
- 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
scope_missing: "This key cannot read Sales." The key may not reach this operation.scope_missingwhen the key does not hold Sales;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. - 429
Get sales velocity events table
GET /api/v1/sales/velocity/events/table
Scopes: sales:read
A page of sales velocity events with their windows, statuses, adjustment types and item counts. Page with limit and offset, narrow with filter_args and search, order with sort_args.
| Optional | In | What it is |
|---|---|---|
limit | query | Number of rows to retrieve in the response. |
offset | query | Number of rows to skip in the response. |
filter_args | query | JSON array of filter conditions. |
sort_args | query | Comma-separated list of sort arguments. |
search | query | Search keyword to filter event names or descriptions. |
Tightly-Version | header | The date train to answer on. |
bash
curl "https://api.app.tightly.io/api/v1/sales/velocity/events/table" \
-H "Authorization: Bearer $TIGHTLY_API_KEY"What it answers
json
{
"data": {
"awaiting_input_count": 2,
"filtered_max_size": 12,
"max_size": 12,
"offset": 0,
"rows": [
{
"adjustment_type": "uplift_percent",
"awaiting_input": false,
"end_date": "2026-11-30",
"exclude_from_training": false,
"id": 412,
"items_count": 184,
"name": "Black Friday 2026",
"start_date": "2026-11-27",
"status": "scheduled",
"variants_count": 184
}
],
"size": 1,
"status_counts": {
"finished": 7,
"running": 1,
"scheduled": 4
}
},
"message": {
"desc": "",
"service": "sales_velocity_events",
"severity": "INFO"
}
}What it refuses
- 400 Bad Request - Invalid request parameters or validation errors
- 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
scope_missing: "This key cannot read Sales." The key may not reach this operation.scope_missingwhen the key does not hold Sales;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. - 429
Get sales velocity filters
GET /api/v1/sales/velocity/filters
Scopes: sales:read
The values the sales velocity table can be filtered on: locations and suppliers as id and name, events, the modified-status words, tags, and {min, max} ranges for the numeric columns. Send field to page one field's values with limit, offset and search; omit it and every filter comes back at once.
| Optional | In | What it is |
|---|---|---|
field | query | Optional field name to retrieve paginated values (e.g., 'shopify_tags'). Omit to get all filters. |
search | query | Optional search term to filter results (only applicable when field is provided) |
offset | query | Number of items to skip for pagination (only applicable when field is provided) |
limit | query | Maximum number of items to return per page (only applicable when field is provided) |
Tightly-Version | header | The date train to answer on. |
bash
curl "https://api.app.tightly.io/api/v1/sales/velocity/filters" \
-H "Authorization: Bearer $TIGHTLY_API_KEY"What it answers
json
{
"data": {
"event_id": [
{
"id": "412",
"name": "Black Friday 2026"
}
],
"location_id": [
{
"id": "loc_0004",
"name": "London DC"
}
],
"modified_status": [
"all",
"modified",
"unmodified"
],
"sales_velocity_30_days_range": [
0,
18.4
],
"shopify_tags": [
"core",
"seasonal"
],
"supplier": [
{
"supplier_id": "sup_0031",
"supplier_name": "Porto Knits"
}
]
},
"message": {
"desc": "",
"service": "sales_velocity",
"severity": "INFO"
}
}What it refuses
- 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
scope_missing: "This key cannot read Sales." The key may not reach this operation.scope_missingwhen the key does not hold Sales;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. - 429
Get forecast model config
GET /api/v1/sales/velocity/forecast-config
Scopes: sales:read
The forecast model this organisation has selected and the configuration behind it: the model, the minimum sales and minimum weeks with sales a variant needs to be forecast, the base and pattern filters, the time windows, and any model-specific parameters under model_parameters.
| Optional | In | What it is |
|---|---|---|
Tightly-Version | header | The date train to answer on. |
bash
curl "https://api.app.tightly.io/api/v1/sales/velocity/forecast-config" \
-H "Authorization: Bearer $TIGHTLY_API_KEY"What it answers
json
{
"data": {
"min_total_sales": 30,
"min_weeks_with_sales": 8,
"model": "auto_arima"
},
"message": {
"desc": "",
"service": "sales_velocity",
"severity": "INFO"
}
}What it refuses
- 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
scope_missing: "This key cannot read Sales." The key may not reach this operation.scope_missingwhen the key does not hold Sales;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. - 429
Get sales velocity table
GET /api/v1/sales/velocity/table
Scopes: sales:read
A page of sales velocity per variant or product, the rate demand forecasting and days of cover are computed from: total demand and total sales for the window, the forecast model chosen for the row, whether it is seasonal, price, channel, supplier, bundle count and a confidence tier, with confidence_tier_counts for the filtered book.
| Optional | In | What it is |
|---|---|---|
limit | query | Number of rows to retrieve in the response. |
offset | query | Number of rows to skip in the response. |
filter_args | query | JSON array of filter conditions. |
sort_args | query | Comma-separated list of sort arguments. |
search | query | Search keyword to filter product or variant titles. |
export | query | If true, returns an export URL instead of paginated table data |
start_date | query | Start date for sales velocity analysis period |
end_date | query | End date for sales velocity analysis period |
type | query | View type - 'variants' shows individual variants, 'products' shows grouped by product. |
demand_sales_columns | query | Comma-separated list of demand/sales columns to include. |
Tightly-Version | header | The date train to answer on. |
bash
curl "https://api.app.tightly.io/api/v1/sales/velocity/table" \
-H "Authorization: Bearer $TIGHTLY_API_KEY"What it answers
json
{
"data": {
"confidence_tier_counts": {
"high": 1840,
"low": 640,
"medium": 1204,
"new": 436,
"scored": 3684,
"total": 4120
},
"data_source": "ch",
"demand_as_of": "2026-09-04T02:41:00+00:00",
"filtered_max_size": 4120,
"filtered_variants_size": 1284,
"max_size": 4120,
"offset": 0,
"row_grain": "variants",
"rows": [
{
"bundle_count": 2,
"confidence_tier": "high",
"forecast_model": "auto_arima",
"is_modified": false,
"is_seasonal": true,
"price": 79,
"product_id": "4410092",
"product_title": "Terry Crew",
"sales_channel_id": "61240442",
"sales_channel_name": "Wholesale",
"shopify_tags": [
"core"
],
"sku": "TB-CREW-BLK-M",
"supplier_id": "sup_0031",
"supplier_name": "Porto Knits",
"total_demand": 412,
"total_sales": 388,
"typical_miss_pct": 0.14,
"variant_id": "44100920011",
"variant_title": "Black / M"
…
}
]
}
}What it refuses
- 400 Bad Request - Invalid request parameters or validation errors
- 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
scope_missing: "This key cannot read Sales." The key may not reach this operation.scope_missingwhen the key does not hold Sales;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. - 429
Get sales velocity table totals
GET /api/v1/sales/velocity/table/totals
Scopes: sales:read
The sales velocity table's totals over every filtered row, as one entry per period: units sold and revenue, forecast demand and its revenue, effective demand (forecast adjusted for stockouts), the same period a year earlier, and the velocity rate the forecast used.
| Optional | In | What it is |
|---|---|---|
filter_args | query | JSON array of filter conditions. |
search | query | Search keyword to filter by product or variant title |
start_date | query | Start date for the sales velocity analysis period |
end_date | query | End date for the sales velocity analysis period. Must be provided together with start_date. |
include_last_year | query | When true, includes last year comparison data in the totals |
aggregation_mode | query | Time bucket for period columns. Auto-selected based on date range when omitted: daily (≤14 days), weekly (≤90 days), monthly (otherwise). |
Tightly-Version | header | The date train to answer on. |
bash
curl "https://api.app.tightly.io/api/v1/sales/velocity/table/totals" \
-H "Authorization: Bearer $TIGHTLY_API_KEY"What it answers
json
{
"data": {
"data_source": "pg",
"demand_as_of": "2026-09-04T02:41:00+00:00",
"scope": {
"pairs": 4120,
"variants": 1284
},
"scope_unavailable_reason": null,
"totals": [
{
"calculated_demand": 18820,
"effective_demand": 19100,
"last_period_sales": 17600,
"max_date": "2026-09-13",
"min_date": "2026-09-07",
"sales": 18400,
"sales_revenue": 984000,
"week_grid": "MON:2026-09-07"
}
],
"week_grids": {
"MON:2026-09-07": {
"aligned": false,
"days_in_next_planning_week": 1,
"days_in_planning_week": 6,
"planning_fiscal_week": 32,
"planning_fiscal_year": 2026,
"planning_week_end": "2026-09-12",
"planning_week_start": "2026-09-06",
"planning_week_start_dow": "SUN",
"shift_days": 1,
"week_end": "2026-09-13",
"week_start": "2026-09-07",
"week_start_dow": "MON"
}
}
},
"message": {
"desc": ""
…
}
}What it refuses
- 400 Bad Request - invalid parameters
- 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
scope_missing: "This key cannot read Sales." The key may not reach this operation.scope_missingwhen the key does not hold Sales;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. - 429
Get variant bundle contributions
GET /api/v1/sales/velocity/variants/{variant_id}/bundle-contributions
Scopes: sales:read
How much of one variant's demand comes from each bundle that contains it, over a window: one entry per bundle with the bundle's product and variant titles and its contribution, plus total_count and total_demand.
| Required | In | What it is |
|---|---|---|
variant_id | path | The component variant whose bundle contributions are requested. |
sales_channel_id | query | Sales channel to scope the demand data to. |
start_date | query | Start of the date range (inclusive). |
end_date | query | End of the date range (inclusive). |
| Optional | In | What it is |
|---|---|---|
Tightly-Version | header | The date train to answer on. |
bash
curl "https://api.app.tightly.io/api/v1/sales/velocity/variants/<variant_id>/bundle-contributions?sales_channel_id=<sales_channel_id>&start_date=<start_date>&end_date=<end_date>" \
-H "Authorization: Bearer $TIGHTLY_API_KEY"What it answers
json
{
"data": {
"bundle_contributions": [
{
"contribution": 42,
"product_title": "Bundle Product A",
"variant_title": "Bundle A"
},
{
"contribution": 18,
"product_title": "Bundle Product B",
"variant_title": "Bundle Set B"
},
{
"contribution": 8,
"product_title": "Others",
"variant_title": null
}
],
"data_source": "ch",
"total_count": 3,
"total_demand": 60
},
"message": {
"service": "sales_velocity",
"success": true
}
}What it refuses
- 400 Bad Request - Invalid parameters
- 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
scope_missing: "This key cannot read Sales." The key may not reach this operation.scope_missingwhen the key does not hold Sales;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. - 429