Appearance
Products
19 reads, no writes, on train 2026-11. Scopes: products:read.
| Operation | Method | Scope | Path |
|---|---|---|---|
| Every value the product reads can be filtered by | GET | products:read | /api/v1/organizations/{organization_id}/products/filters |
| What the catalogue is worth on hand | GET | products:read | /api/v1/organizations/{organization_id}/products/overview |
| The subcategories under one category | GET | products:read | /api/v1/product-subcategories |
| One product with its stock and the ranges its variants span | GET | products:read | /api/v1/product/{product_id} |
| The values a product's own rows can be filtered by | GET | products:read | /api/v1/product/{product_id}/filters |
| What is still on order for a product | GET | products:read | /api/v1/product/{product_id}/incoming-pos |
| What is still on order for a product | GET | products:read | /api/v1/product/{product_id}/incoming-pos/drawer |
| Every variant of one product | GET | products:read | /api/v1/product/{product_id}/variants |
| What the scoped products are missing for planning | GET | products:read | /api/v1/products/completeness |
| A page of the product catalogue | GET | products:read | /api/v1/products/table |
| One variant with its stock and its suppliers | GET | products:read | /api/v1/variants/{variant_id} |
| One variant's stock and cover | GET | products:read | /api/v1/variants/{variant_id}/drawer-overview |
| One variant's own timeline of changes | GET | products:read | /api/v1/variants/{variant_id}/events |
| What is still on order for one variant | GET | products:read | /api/v1/variants/{variant_id}/incoming-pos |
| How this variant's sales have moved when its price moved | GET | products:read | /api/v1/variants/{variant_id}/price-sensitivity |
| The custom fields this organisation keeps on its variants | GET | products:read | /api/v1/variants/custom-fields |
| One custom field definition | GET | products:read | /api/v1/variants/custom-fields/{custom_field_id} |
| What changed on the catalogue | GET | products:read | /api/v1/variants/events |
| A page of variants | GET | products:read | /api/v1/variants/table |
Every value the product reads can be filtered by
GET /api/v1/organizations/{organization_id}/products/filters
Scopes: products:read
The values that exist in this organisation's catalogue, so a filter can be built from what is there rather than guessed: category, supplier, vendor, product_status, tags, shopify_tags, class (the performance categories), location, missing_fields, metafields, custom_fields and country_of_origin (ISO 3166-1 alpha-2).
| Required | In | What it is |
|---|---|---|
organization_id | path | The organisation. |
| Optional | In | What it is |
|---|---|---|
field | query | Return one paginated list instead of every filter. |
search | query | Narrow the paginated list. Ignored when field is absent. |
offset | query | Values to skip. Ignored when field is absent. |
limit | query | Values to return. Ignored when field is absent. |
Tightly-Version | header | The date train to answer on. |
bash
curl "https://api.app.tightly.io/api/v1/organizations/<organization_id>/products/filters" \
-H "Authorization: Bearer $TIGHTLY_API_KEY"What it answers
json
{
"data": {
"category": [
"Outerwear",
"Knitwear"
],
"class": [
"Best sellers",
"Slow movers"
],
"country_of_origin": [
"PT",
"IT"
],
"custom_fields": [
"Fabric composition"
],
"location": [
{
"id": "61240442",
"name": "Rotterdam DC"
}
],
"metafields": [
"custom.fabric"
],
"missing_fields": [
"hs_code"
],
"product_status": [
"ACTIVE",
"ARCHIVED",
"DRAFT",
"UNLISTED"
],
"shopify_tags": [
"aw26"
],
"supplier": [
{
…
}
]
}
}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, because telling a caller which is which maps the surface for them. - 403
scope_missing: "This key cannot read Products." The key may not reach this operation.scope_missingwhen the key does not hold Products;organization_mismatchwhen the path names an organisation that is not the key's, which is refused rather than substituted;ip_not_allowedwhen the caller's address is outside the key's allowlist.plan_excludeswhen the organisation's plan does not include the public API itself, which is sold with Essentials and is asked of every key minted under that gate. A key minted before the gate existed is never refused by it, whatever the plan holds. Products is ungated, so that is the only plan reason served here. - 429
What the catalogue is worth on hand
GET /api/v1/organizations/{organization_id}/products/overview
Scopes: products:read
Three figures for the catalogue, each as a card: current_inventory_cost (stock on hand at cost), incoming_stock_cost (what is on order, at cost) and expected_revenue (the same stock at its selling price).
| Required | In | What it is |
|---|---|---|
organization_id | path | The organisation. |
| Optional | In | What it is |
|---|---|---|
filter_args | query | A JSON array of {key, operation, value} narrowing the figures. |
search | query | Free text, applied to the same set the products table would match. |
Tightly-Version | header | The date train to answer on. |
bash
curl "https://api.app.tightly.io/api/v1/organizations/<organization_id>/products/overview" \
-H "Authorization: Bearer $TIGHTLY_API_KEY"What it answers
json
{
"data": {
"cards": [
{
"change": 0.0717,
"higher_better": true,
"info": "Stock on hand valued at unit cost.",
"key": "current_inventory_cost",
"previous_value": 1198400,
"unit": "USD",
"value": 1284300
},
{
"change": -0.2149,
"higher_better": true,
"info": "Open orders valued at unit cost.",
"key": "incoming_stock_cost",
"previous_value": 402100,
"unit": "USD",
"value": 315700
},
{
"change": 0.0698,
"higher_better": true,
"info": "Stock on hand valued at its selling price.",
"key": "expected_revenue",
"previous_value": 3188200,
"unit": "USD",
"value": 3410800
}
]
},
"message": {
"desc": "OK",
"service": "inventory",
"severity": "INFO"
}
}What it refuses
- 400 filter_args is not valid JSON, or names a key this read does not accept.
- 401
key_invalid: "The API key is not valid." No usable key: malformed, unknown, revoked, expired or stopped. One refusal covers all five, because telling a caller which is which maps the surface for them. - 403
scope_missing: "This key cannot read Products." The key may not reach this operation.scope_missingwhen the key does not hold Products;organization_mismatchwhen the path names an organisation that is not the key's, which is refused rather than substituted;ip_not_allowedwhen the caller's address is outside the key's allowlist.plan_excludeswhen the organisation's plan does not include the public API itself, which is sold with Essentials and is asked of every key minted under that gate. A key minted before the gate existed is never refused by it, whatever the plan holds. Products is ungated, so that is the only plan reason served here. - 429
The subcategories under one category
GET /api/v1/product-subcategories
Scopes: products:read
The active subcategories belonging to one category, each {subcategory_id, name, category_id, is_active}. Read it to resolve a subcategory name to the id a product carries, or to offer the choices under a category.
| Required | In | What it is |
|---|---|---|
category | query | The category name or category_id whose subcategories to retrieve. |
| Optional | In | What it is |
|---|---|---|
Tightly-Version | header | The date train to answer on. |
bash
curl "https://api.app.tightly.io/api/v1/product-subcategories?category=<category>" \
-H "Authorization: Bearer $TIGHTLY_API_KEY"What it answers
json
{
"data": [
{
"category_id": "cat_outerwear",
"is_active": true,
"name": "Shirt jackets",
"subcategory_id": "sub_shirt_jackets"
},
{
"category_id": "cat_outerwear",
"is_active": true,
"name": "Parkas",
"subcategory_id": "sub_parkas"
}
],
"message": {
"desc": "The subcategories on file",
"service": "inventory",
"severity": "SUCCESS"
}
}What it refuses
- 400 The category query parameter is missing.
- 401
key_invalid: "The API key is not valid." No usable key: malformed, unknown, revoked, expired or stopped. One refusal covers all five, because telling a caller which is which maps the surface for them. - 403
scope_missing: "This key cannot read Products." The key may not reach this operation.scope_missingwhen the key does not hold Products;ip_not_allowedwhen the caller's address is outside the key's allowlist.plan_excludeswhen the organisation's plan does not include the public API itself, which is sold with Essentials and is asked of every key minted under that gate. A key minted before the gate existed is never refused by it, whatever the plan holds. Products is ungated, so that is the only plan reason served here. - 404 No category in this organisation carries that name or id.
- 429
One product with its stock and the ranges its variants span
GET /api/v1/product/{product_id}
Scopes: products:read
One product as the catalogue holds it: title, description, vendor, images, category, subcategory and family, status and published date, country of origin, HS code, unit of measure, and the sales channels it is sold through.
| Required | In | What it is |
|---|---|---|
product_id | path | The product this reads. |
| Optional | In | What it is |
|---|---|---|
location_id | query | Narrow in_stock, incoming_stock and inventory_value to one warehouse. |
Tightly-Version | header | The date train to answer on. |
bash
curl "https://api.app.tightly.io/api/v1/product/<product_id>" \
-H "Authorization: Bearer $TIGHTLY_API_KEY"What it answers
json
{
"data": {
"category": "Outerwear",
"category_id": "cat_outerwear",
"country_of_origin": "PT",
"currency": "USD",
"description": "A brushed wool overshirt cut for layering.",
"family": "Tops",
"family_id": "fam_tops",
"gallery_images": [
"https://cdn.tightly.io/p/7412095483953.jpg"
],
"hs_code": "620331",
"image_url": "https://cdn.tightly.io/p/7412095483953.jpg",
"in_current_season_plan": true,
"in_stock": 1840,
"incoming_stock": 600,
"inventory_value": 71760,
"is_managed": true,
"is_seasonal": true,
"last_updated_at": "2026-09-01T11:42:00+00:00",
"last_updated_by": "ana.ruiz@example.com",
"launch_date": "2026-02-14",
"lead_time": {
"max": 60,
"min": 45
},
"min_order_quantity": {
"max": 120,
"min": 120
},
"num_variants": 8,
"options": {
"Colour": [
"Charcoal",
"Sand"
],
"Size": [
"S",
"M"
…
]
}
}
}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, because telling a caller which is which maps the surface for them. - 403
scope_missing: "This key cannot read Products." The key may not reach this operation.scope_missingwhen the key does not hold Products;ip_not_allowedwhen the caller's address is outside the key's allowlist.plan_excludeswhen the organisation's plan does not include the public API itself, which is sold with Essentials and is asked of every key minted under that gate. A key minted before the gate existed is never refused by it, whatever the plan holds. Products is ungated, so that is the only plan reason served here. - 404 No product in this organisation carries that id.
- 429
The values a product's own rows can be filtered by
GET /api/v1/product/{product_id}/filters
Scopes: products:read
The filter values that exist for one product: categories, the category names its variants carry, and locations, the warehouses it is stocked in. Read it to populate a picker before narrowing the product's own reads, rather than sending a value the catalogue does not hold and getting an empty page back.
| Required | In | What it is |
|---|---|---|
product_id | path | The product whose filter values this reads. |
| Optional | In | What it is |
|---|---|---|
Tightly-Version | header | The date train to answer on. |
bash
curl "https://api.app.tightly.io/api/v1/product/<product_id>/filters" \
-H "Authorization: Bearer $TIGHTLY_API_KEY"What it answers
json
{
"data": {
"categories": [
"Outerwear"
],
"locations": [
"Rotterdam DC",
"New Jersey DC"
]
},
"message": {
"desc": "OK",
"service": "product",
"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, because telling a caller which is which maps the surface for them. - 403
scope_missing: "This key cannot read Products." The key may not reach this operation.scope_missingwhen the key does not hold Products;ip_not_allowedwhen the caller's address is outside the key's allowlist.plan_excludeswhen the organisation's plan does not include the public API itself, which is sold with Essentials and is asked of every key minted under that gate. A key minted before the gate existed is never refused by it, whatever the plan holds. Products is ungated, so that is the only plan reason served here. - 404 No product in this organisation carries that id.
- 429
What is still on order for a product
GET /api/v1/product/{product_id}/incoming-pos
Scopes: products:read
Open orders carrying any variant of one product, grouped under the variant rather than under the order: one entry per variant that has something inbound, each with po_count, to_count (transfer orders), proposal_count and the orders themselves.
| Required | In | What it is |
|---|---|---|
product_id | path | The product whose inbound orders this reads. |
| Optional | In | What it is |
|---|---|---|
Tightly-Version | header | The date train to answer on. |
bash
curl "https://api.app.tightly.io/api/v1/product/<product_id>/incoming-pos" \
-H "Authorization: Bearer $TIGHTLY_API_KEY"What it answers
json
{
"data": {
"purchase_orders_grouped_by_variant": [
{
"po_count": 1,
"proposal_count": 0,
"purchase_orders": [
{
"delivered_quantity": 0,
"deliveries": null,
"expected_delivery_date": "2026-10-12",
"has_delivery_delay": false,
"id": "48120",
"is_proposal": false,
"line_items": [
{
"quantity": 240,
"variant_id": "42318902722609",
"variant_title": "Charcoal / S"
}
],
"location_id": "61240442",
"location_name": "Rotterdam DC",
"order_name": "PO-00048120",
"order_type": "PURCHASE",
"ordered_quantity": 240,
"status": "FullyConfirmed",
"supplier_id": "sup_1180",
"supplier_name": "Atelier Norte",
"supplier_signals": []
}
],
"to_count": 0,
"total_delivered": 0,
"total_ordered": 240,
"variant_id": "42318902722609",
"variant_name": "Charcoal / S"
}
]
}
…
}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, because telling a caller which is which maps the surface for them. - 403
scope_missing: "This key cannot read Products." The key may not reach this operation.scope_missingwhen the key does not hold Products;ip_not_allowedwhen the caller's address is outside the key's allowlist.plan_excludeswhen the organisation's plan does not include the public API itself, which is sold with Essentials and is asked of every key minted under that gate. A key minted before the gate existed is never refused by it, whatever the plan holds. Products is ungated, so that is the only plan reason served here. - 404 No product in this organisation carries that id.
- 429
What is still on order for a product
GET /api/v1/product/{product_id}/incoming-pos/drawer
Scopes: products:read
The same open orders as the per-variant read, grouped the other way: by order_type, in the fixed order PURCHASE, TRANSFER, MANUFACTURING, with one entry per order rather than one per variant.
| Required | In | What it is |
|---|---|---|
product_id | path | The product whose inbound orders this reads. |
| Optional | In | What it is |
|---|---|---|
Tightly-Version | header | The date train to answer on. |
bash
curl "https://api.app.tightly.io/api/v1/product/<product_id>/incoming-pos/drawer" \
-H "Authorization: Bearer $TIGHTLY_API_KEY"What it answers
json
{
"data": {
"purchase_orders_grouped_by_type": [
{
"order_type": "PURCHASE",
"purchase_orders": [
{
"delivered_quantity": 0,
"expected_delivery_date": "2026-10-12",
"has_delivery_delay": false,
"id": "48120",
"line_items": [
{
"quantity": 240,
"variant_id": "42318902722609",
"variant_title": "Charcoal / S"
},
{
"quantity": 360,
"variant_id": "42318902755377",
"variant_title": "Charcoal / M"
}
],
"name": "PO-00048120",
"ordered_quantity": 600,
"status": "FullyConfirmed",
"supplier_signals": []
}
]
},
{
"order_type": "TRANSFER",
"purchase_orders": []
}
]
},
"message": {
"desc": "OK",
"service": "product",
"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, because telling a caller which is which maps the surface for them. - 403
scope_missing: "This key cannot read Products." The key may not reach this operation.scope_missingwhen the key does not hold Products;ip_not_allowedwhen the caller's address is outside the key's allowlist.plan_excludeswhen the organisation's plan does not include the public API itself, which is sold with Essentials and is asked of every key minted under that gate. A key minted before the gate existed is never refused by it, whatever the plan holds. Products is ungated, so that is the only plan reason served here. - 404 No product in this organisation carries that id.
- 429
Every variant of one product
GET /api/v1/product/{product_id}/variants
Scopes: products:read
The product's variants, three fields each: variant_id, variant_title and sku. It is the list to walk when you hold a product and need the variant ids underneath it.
| Required | In | What it is |
|---|---|---|
product_id | path | The product whose variants this lists. |
| Optional | In | What it is |
|---|---|---|
Tightly-Version | header | The date train to answer on. |
bash
curl "https://api.app.tightly.io/api/v1/product/<product_id>/variants" \
-H "Authorization: Bearer $TIGHTLY_API_KEY"What it answers
json
{
"data": {
"variants": [
{
"sku": "AWO-CHR-S",
"variant_id": "42318902722609",
"variant_title": "Charcoal / S"
},
{
"sku": "AWO-CHR-M",
"variant_id": "42318902755377",
"variant_title": "Charcoal / M"
},
{
"sku": "AWO-SND-S",
"variant_id": "42318902788145",
"variant_title": "Sand / S"
}
]
},
"message": {
"desc": "OK",
"service": "product",
"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, because telling a caller which is which maps the surface for them. - 403
scope_missing: "This key cannot read Products." The key may not reach this operation.scope_missingwhen the key does not hold Products;ip_not_allowedwhen the caller's address is outside the key's allowlist.plan_excludeswhen the organisation's plan does not include the public API itself, which is sold with Essentials and is asked of every key minted under that gate. A key minted before the gate existed is never refused by it, whatever the plan holds. Products is ungated, so that is the only plan reason served here. - 404 No product in this organisation carries that id.
- 429
What the scoped products are missing for planning
GET /api/v1/products/completeness
Scopes: products:read
A statement about a whole scope rather than a page of it: how many products it holds, how many are planning-complete, and one row per planning attribute saying how many products are missing it.
| Optional | In | What it is |
|---|---|---|
search | query | Free text over product title, SKU and vendor, as on the products table. |
filter_args | query | A JSON array of {key, operation, value}, the same keys the products table accepts. |
Tightly-Version | header | The date train to answer on. |
bash
curl "https://api.app.tightly.io/api/v1/products/completeness" \
-H "Authorization: Bearer $TIGHTLY_API_KEY"What it answers
json
{
"data": {
"complete_products": 268,
"fields": [
{
"affected_pairs": 0,
"field": "category",
"label": "Category",
"missing_products": 0,
"reason": "No category: analog matching and price banding fall back to the whole catalog.",
"review_products": 0
},
{
"affected_pairs": 742,
"field": "price_band",
"label": "Price band",
"missing_products": 61,
"reason": "No price band: suggestions can't band by architecture (good / better / best).",
"review_products": 0
},
{
"affected_pairs": 1109,
"field": "size_curve",
"label": "Size curve",
"missing_products": 88,
"reason": "No size curve: buys will use the bell default (1-2-2-1).",
"review_products": 0
},
{
"affected_pairs": 301,
"field": "selling_window",
"label": "Selling window",
"missing_products": 24,
"reason": "Seasonal without a selling window: replenishment can't hold to the season and plans can't phase.",
"review_products": 0
},
{
"affected_pairs": 144,
"field": "cost",
"label": "Unit cost"
…
}
]
}
}What it refuses
- 400 filter_args is not valid JSON, or names a key the products table does not accept.
- 401
key_invalid: "The API key is not valid." No usable key: malformed, unknown, revoked, expired or stopped. One refusal covers all five, because telling a caller which is which maps the surface for them. - 403
scope_missing: "This key cannot read Products." The key may not reach this operation.scope_missingwhen the key does not hold Products;ip_not_allowedwhen the caller's address is outside the key's allowlist.plan_excludeswhen the organisation's plan does not include the public API itself, which is sold with Essentials and is asked of every key minted under that gate. A key minted before the gate existed is never refused by it, whatever the plan holds. Products is ungated, so that is the only plan reason served here. - 429
A page of the product catalogue
GET /api/v1/products/table
Scopes: products:read
A page of products with the catalogue fields an integrator syncs on: title, vendor, category, num_variants, status, GTIN, description, currency, HS code, country of origin, unit of measure, sales_channels, and last_updated_at and last_updated_by.
| Optional | In | What it is |
|---|---|---|
offset | query | Rows to skip before this page. |
limit | query | Rows in this page. |
search | query | Free text over product title, SKU and vendor. |
filter_args | query | A JSON array of {key, operation, value}. |
sort_args | query | Comma-separated sort columns, - for descending and + or nothing for ascending. |
export | query | Answer a download URL for the filtered set instead of a page of rows. |
Tightly-Version | header | The date train to answer on. |
bash
curl "https://api.app.tightly.io/api/v1/products/table" \
-H "Authorization: Bearer $TIGHTLY_API_KEY"What it answers
json
{
"data": {
"filtered_max_size": 412,
"max_size": 1806,
"offset": 0,
"products_count": 1,
"rows": [
{
"category": "Outerwear",
"category_id": "cat_outerwear",
"country_of_origin": "PT",
"currency": "USD",
"description": "A brushed wool overshirt cut for layering.",
"gallery_images": [
"https://cdn.tightly.io/p/7412095483953.jpg"
],
"gtin": "05012345678900",
"height": null,
"hs_code": "620331",
"is_seasonal": true,
"last_updated_at": "2026-09-01T11:42:00+00:00",
"last_updated_by": "ana.ruiz@example.com",
"length": null,
"num_variants": 8,
"planning_gaps": [],
"price_band": "better",
"price_band_source": "declared",
"product_id": "7412095483953",
"product_status": "ACTIVE",
"product_title": "Alpine Wool Overshirt",
"replenishment_mode": "seasonal",
"sales_channels": [
{
"id": "61240442",
"name": "Online Store"
}
],
"sell_price": {
"max": 145,
"min": 129
…
}
}
]
}
}What it refuses
- 400 offset or limit is outside its range, or filter_args is not valid JSON.
- 401
key_invalid: "The API key is not valid." No usable key: malformed, unknown, revoked, expired or stopped. One refusal covers all five, because telling a caller which is which maps the surface for them. - 403
scope_missing: "This key cannot read Products." The key may not reach this operation.scope_missingwhen the key does not hold Products;ip_not_allowedwhen the caller's address is outside the key's allowlist.plan_excludeswhen the organisation's plan does not include the public API itself, which is sold with Essentials and is asked of every key minted under that gate. A key minted before the gate existed is never refused by it, whatever the plan holds. Products is ungated, so that is the only plan reason served here. - 429
One variant with its stock and its suppliers
GET /api/v1/variants/{variant_id}
Scopes: products:read
One variant as the catalogue holds it, and what is true of it right now: sku, barcode, gtin, lifecycle and its alias status, description and images, vendor, category, sell_price, unit_cost and unit_cost_currency, the physical fields, hs_code, country_of_origin, selected_options, custom_fields, launch_date, and the product_id and product_name it belongs to.
| Required | In | What it is |
|---|---|---|
variant_id | path | The variant this reads. |
| Optional | In | What it is |
|---|---|---|
location_id | query | Narrow in_stock, incoming_stock and inventory_value to one warehouse. |
Tightly-Version | header | The date train to answer on. |
bash
curl "https://api.app.tightly.io/api/v1/variants/<variant_id>" \
-H "Authorization: Bearer $TIGHTLY_API_KEY"What it answers
json
{
"data": {
"barcode": "5012345678900",
"category": "Outerwear",
"category_id": "cat_outerwear",
"country_of_origin": "PT",
"custom_fields": {
"Fabric composition": "80% wool, 20% polyamide"
},
"description": "A brushed wool overshirt cut for layering.",
"gtin": "05012345678900",
"health_cover": [
{
"cover": 38,
"health": "healthy",
"location": "Rotterdam DC"
}
],
"hs_code": "620331",
"image_url": "https://cdn.tightly.io/p/7412095483953.jpg",
"in_stock": 214,
"incoming_stock": 240,
"inventory_value": 7276,
"is_managed": true,
"last_stockout_date": null,
"launch_date": "2026-02-14",
"lifecycle": "seasonal",
"performance_category_by_location": [
{
"capital_quadrant": "Grow",
"location": "Rotterdam DC",
"performance_category": "Best sellers"
}
],
"product_id": "7412095483953",
"product_name": "Alpine Wool Overshirt",
"production_type": "buy_only",
"published_status": "ACTIVE",
"selected_options": {
"Colour": "Charcoal"
…
}
}
}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, because telling a caller which is which maps the surface for them. - 403
scope_missing: "This key cannot read Products." The key may not reach this operation.scope_missingwhen the key does not hold Products;ip_not_allowedwhen the caller's address is outside the key's allowlist.plan_excludeswhen the organisation's plan does not include the public API itself, which is sold with Essentials and is asked of every key minted under that gate. A key minted before the gate existed is never refused by it, whatever the plan holds. Products is ungated, so that is the only plan reason served here. - 404 No variant in this organisation carries that id.
- 429
One variant's stock and cover
GET /api/v1/variants/{variant_id}/drawer-overview
Scopes: products:read
One row per warehouse the variant is held in: location_id and location_name, stock_on_hand, stock_value, incoming_stock, weeks_of_cover, velocity (units a day), health and capital_quadrant.
| Required | In | What it is |
|---|---|---|
variant_id | path | The variant this reads. |
| Optional | In | What it is |
|---|---|---|
Tightly-Version | header | The date train to answer on. |
bash
curl "https://api.app.tightly.io/api/v1/variants/<variant_id>/drawer-overview" \
-H "Authorization: Bearer $TIGHTLY_API_KEY"What it answers
json
{
"data": {
"is_seasonal": true,
"locations": [
{
"capital_quadrant": "Grow",
"health": "healthy",
"incoming_stock": 240,
"location_id": "61240442",
"location_name": "Rotterdam DC",
"stock_on_hand": 214,
"stock_value": 7276,
"velocity": 5.6,
"weeks_of_cover": 5.4
},
{
"capital_quadrant": null,
"health": "critical",
"incoming_stock": 0,
"location_id": "61240443",
"location_name": "New Jersey DC",
"stock_on_hand": 0,
"stock_value": 0,
"velocity": null,
"weeks_of_cover": null
}
],
"predecessors": [
{
"product_title": "Alpine Wool Overshirt (AW25)",
"variant_id": "41180022331904",
"variant_title": "Charcoal / S"
}
],
"successors": []
},
"message": {
"desc": "OK",
"service": "variant",
"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, because telling a caller which is which maps the surface for them. - 403
scope_missing: "This key cannot read Products." The key may not reach this operation.scope_missingwhen the key does not hold Products;ip_not_allowedwhen the caller's address is outside the key's allowlist.plan_excludeswhen the organisation's plan does not include the public API itself, which is sold with Essentials and is asked of every key minted under that gate. A key minted before the gate existed is never refused by it, whatever the plan holds. Products is ungated, so that is the only plan reason served here. - 404 No variant in this organisation carries that id.
- 429
One variant's own timeline of changes
GET /api/v1/variants/{variant_id}/events
Scopes: products:read
Every recorded change to one variant: id, event_type, event_date, previous_value, new_value, a typed metadata object, location_id, created_at and created_by_user_id. It carries no product or SKU fields, because they are the ones you name in the request.
| Required | In | What it is |
|---|---|---|
variant_id | path | The variant whose timeline this reads. |
| Optional | In | What it is |
|---|---|---|
event_type | query | Comma-separated event types to filter by. Omit to return every type. |
start_date | query | Start of the range (ISO 8601 date). |
end_date | query | End of the range (ISO 8601 date). Defaults to today. |
location_id | query | Narrow stock events to one warehouse, matched against metadata.location_changes. |
limit | query | Events in this page. |
offset | query | Events to skip before this page. |
Tightly-Version | header | The date train to answer on. |
bash
curl "https://api.app.tightly.io/api/v1/variants/<variant_id>/events" \
-H "Authorization: Bearer $TIGHTLY_API_KEY"What it answers
json
{
"data": {
"events": [
{
"created_at": "2026-08-19T09:14:00+00:00",
"created_by_user_id": null,
"event_date": "2026-08-19",
"event_type": "price_change",
"id": "evt_9f21c40b",
"location_id": null,
"metadata": {
"currency": "USD"
},
"new_value": "129.00",
"previous_value": "119.00"
},
{
"created_at": "2026-07-02T16:38:00+00:00",
"created_by_user_id": "usr_4471",
"event_date": "2026-07-02",
"event_type": "moq_change",
"id": "evt_9e04a771",
"location_id": null,
"metadata": {
"supplier_id": "sup_1180"
},
"new_value": "120",
"previous_value": "100"
}
],
"filtered_count": 2,
"limit": 8,
"offset": 0,
"total_count": 34
},
"message": {
"desc": "SKU events retrieved",
"service": "variant",
"severity": "SUCCESS"
}
…
}What it refuses
- 400 A date could not be read, or offset or limit is outside its range.
- 401
key_invalid: "The API key is not valid." No usable key: malformed, unknown, revoked, expired or stopped. One refusal covers all five, because telling a caller which is which maps the surface for them. - 403
scope_missing: "This key cannot read Products." The key may not reach this operation.scope_missingwhen the key does not hold Products;ip_not_allowedwhen the caller's address is outside the key's allowlist.plan_excludeswhen the organisation's plan does not include the public API itself, which is sold with Essentials and is asked of every key minted under that gate. A key minted before the gate existed is never refused by it, whatever the plan holds. Products is ungated, so that is the only plan reason served here. - 404 No variant in this organisation carries that id.
- 429
What is still on order for one variant
GET /api/v1/variants/{variant_id}/incoming-pos
Scopes: products:read
The open orders carrying one variant, with the quantities counted for that variant alone.
| Required | In | What it is |
|---|---|---|
variant_id | path | The variant whose inbound orders this reads. |
| Optional | In | What it is |
|---|---|---|
Tightly-Version | header | The date train to answer on. |
bash
curl "https://api.app.tightly.io/api/v1/variants/<variant_id>/incoming-pos" \
-H "Authorization: Bearer $TIGHTLY_API_KEY"What it answers
json
{
"data": {
"purchase_orders": [
{
"delivered_quantity": 0,
"deliveries": null,
"expected_delivery_date": "2026-10-12",
"id": "48120",
"line_items": [
{
"quantity": 240,
"variant_id": "42318902722609",
"variant_title": "Charcoal / S"
}
],
"location_id": "61240442",
"location_name": "Rotterdam DC",
"name": "PO-00048120",
"ordered_quantity": 240,
"status": "FullyConfirmed",
"supplier_id": "sup_1180",
"supplier_name": "Atelier Norte"
}
]
},
"message": {
"desc": "OK",
"service": "variant",
"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, because telling a caller which is which maps the surface for them. - 403
scope_missing: "This key cannot read Products." The key may not reach this operation.scope_missingwhen the key does not hold Products;ip_not_allowedwhen the caller's address is outside the key's allowlist.plan_excludeswhen the organisation's plan does not include the public API itself, which is sold with Essentials and is asked of every key minted under that gate. A key minted before the gate existed is never refused by it, whatever the plan holds. Products is ungated, so that is the only plan reason served here. - 404 No variant in this organisation carries that id.
- 429
How this variant's sales have moved when its price moved
GET /api/v1/variants/{variant_id}/price-sensitivity
Scopes: products:read
What past price changes did to this variant's sales velocity, as a tier with the evidence behind it.
| Required | In | What it is |
|---|---|---|
variant_id | path | The variant this reads. |
| Optional | In | What it is |
|---|---|---|
Tightly-Version | header | The date train to answer on. |
bash
curl "https://api.app.tightly.io/api/v1/variants/<variant_id>/price-sensitivity" \
-H "Authorization: Bearer $TIGHTLY_API_KEY"What it answers
json
{
"data": {
"basis": "measured",
"confidence": "medium",
"data_through": "2026-08-19",
"evidence": "Seven qualifying price changes over 42 weeks.",
"expected_error": 0.04,
"n_events": 7,
"periods_observed": 42,
"price_variation": 0.11,
"reference_basis": null,
"reference_elasticity": null,
"reference_source": null,
"required_price_variation": 0.08,
"shrunk_effect_down": 0.06,
"shrunk_effect_up": -0.11,
"sign_agreement": 0.86,
"summary": "Sales dropped ~11% when price increased. Sales rose ~6% when price decreased.",
"tier": "MODERATE",
"worth_considering": "Price changes modestly affect sales (~11% velocity shift). Medium confidence. Last event analyzed: August 2026."
},
"message": {
"desc": "Price sensitivity classification retrieved",
"service": "variant",
"severity": "SUCCESS"
}
}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, because telling a caller which is which maps the surface for them. - 403
scope_missing: "This key cannot read Products." The key may not reach this operation.scope_missingwhen the key does not hold Products;ip_not_allowedwhen the caller's address is outside the key's allowlist.plan_excludeswhen the organisation's plan does not include the public API itself, which is sold with Essentials and is asked of every key minted under that gate. A key minted before the gate existed is never refused by it, whatever the plan holds. Products is ungated, so that is the only plan reason served here. - 429
The custom fields this organisation keeps on its variants
GET /api/v1/variants/custom-fields
Scopes: products:read
A page of custom field definitions: what this organisation has added to its variants beyond the catalogue's own columns. Each carries id, name, field_type (string, decimal, integer, boolean or date), source, description, is_active, is_editable, is_value_editable, access_type, and its timestamps.
| Optional | In | What it is |
|---|---|---|
offset | query | Definitions to skip before this page. |
limit | query | Definitions in this page. |
search | query | Free text over name and description. |
filter_args | query | A JSON array of {key, operation, value}. |
Tightly-Version | header | The date train to answer on. |
bash
curl "https://api.app.tightly.io/api/v1/variants/custom-fields" \
-H "Authorization: Bearer $TIGHTLY_API_KEY"What it answers
json
{
"data": {
"custom_fields": [
{
"access_type": "read_write",
"bound_to": null,
"created_at": "2026-03-02T10:00:00+00:00",
"description": "The mill's stated composition.",
"field_type": "string",
"grain": "product",
"id": "123",
"is_active": true,
"is_editable": true,
"is_value_editable": true,
"label": "Composition",
"name": "composition",
"source": "tightly",
"updated_at": "2026-08-14T08:21:00+00:00",
"used_in": {
"families": 2,
"saved_filters": 0
}
},
{
"access_type": "read_only",
"bound_to": {
"connection_id": "66b1a0f4c2e91d0007a3b512",
"field_key": "product.custom_field.fabric",
"source_path": "metafields.custom.fabric"
},
"created_at": "2026-01-19T12:04:00+00:00",
"description": "tightly:grain=product; label=Fabric",
"field_type": "string",
"grain": "product",
"id": "141",
"is_active": true,
"is_editable": false,
"is_value_editable": false,
"label": "Fabric",
"name": "fabric"
…
}
]
}
}What it refuses
- 400 offset or limit is outside its range, or filter_args is not valid JSON.
- 401
key_invalid: "The API key is not valid." No usable key: malformed, unknown, revoked, expired or stopped. One refusal covers all five, because telling a caller which is which maps the surface for them. - 403
scope_missing: "This key cannot read Products." The key may not reach this operation.scope_missingwhen the key does not hold Products;ip_not_allowedwhen the caller's address is outside the key's allowlist.plan_excludeswhen the organisation's plan does not include the public API itself, which is sold with Essentials and is asked of every key minted under that gate. A key minted before the gate existed is never refused by it, whatever the plan holds. Products is ungated, so that is the only plan reason served here. - 429
One custom field definition
GET /api/v1/variants/custom-fields/{custom_field_id}
Scopes: products:read
One custom field definition by its id: name, field_type, source, description, is_active, is_editable, is_value_editable, access_type and its timestamps.
| Required | In | What it is |
|---|---|---|
custom_field_id | path | The definition this reads, as a variant's custom_fields map keys it. |
| Optional | In | What it is |
|---|---|---|
Tightly-Version | header | The date train to answer on. |
bash
curl "https://api.app.tightly.io/api/v1/variants/custom-fields/<custom_field_id>" \
-H "Authorization: Bearer $TIGHTLY_API_KEY"What it answers
json
{
"data": {
"custom_field": {
"access_type": "read_write",
"created_at": "2026-03-02T10:00:00+00:00",
"description": "The mill's stated composition.",
"field_type": "string",
"id": "123",
"is_active": true,
"is_editable": true,
"is_value_editable": true,
"name": "Fabric composition",
"source": "tightly",
"updated_at": "2026-08-14T08:21:00+00:00"
}
},
"message": {
"desc": "OK",
"service": "variant",
"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, because telling a caller which is which maps the surface for them. - 403
scope_missing: "This key cannot read Products." The key may not reach this operation.scope_missingwhen the key does not hold Products;ip_not_allowedwhen the caller's address is outside the key's allowlist.plan_excludeswhen the organisation's plan does not include the public API itself, which is sold with Essentials and is asked of every key minted under that gate. A key minted before the gate existed is never refused by it, whatever the plan holds. Products is ungated, so that is the only plan reason served here. - 404 No custom field in this organisation carries that id.
- 429
What changed on the catalogue
GET /api/v1/variants/events
Scopes: products:read
The change log of the catalogue: one entry per recorded change on any variant, each with id, variant_id, event_type, event_date, previous_value, new_value, a typed metadata object, and the product_name, variant_name and sku the change happened to. created_at, created_by_user_id and location_id are null where the change came from a sync rather than a person or a place.
| Optional | In | What it is |
|---|---|---|
event_type | query | Comma-separated event types to filter by. Omit to return every type. |
start_date | query | Start of the range (ISO 8601 date). Unset reads from the beginning of what is kept. |
end_date | query | End of the range (ISO 8601 date). Defaults to today. |
limit | query | Events in this page. |
offset | query | Events to skip before this page. |
Tightly-Version | header | The date train to answer on. |
bash
curl "https://api.app.tightly.io/api/v1/variants/events" \
-H "Authorization: Bearer $TIGHTLY_API_KEY"What it answers
json
{
"data": {
"events": [
{
"created_at": "2026-08-19T09:14:00+00:00",
"created_by_user_id": null,
"event_date": "2026-08-19",
"event_type": "price_change",
"id": "evt_9f21c40b",
"location_id": null,
"metadata": {
"currency": "USD"
},
"new_value": "129.00",
"previous_value": "119.00",
"product_name": "Alpine Wool Overshirt",
"sku": "AWO-CHR-S",
"variant_id": "42318902722609",
"variant_name": "Charcoal / S"
},
{
"created_at": "2026-08-21T02:10:00+00:00",
"created_by_user_id": null,
"event_date": "2026-08-21",
"event_type": "stockout",
"id": "evt_9f21c512",
"location_id": "61240442",
"metadata": {
"location_changes": [
{
"location_id": "61240442",
"new": 0,
"previous": 18
}
]
},
"new_value": "0",
"previous_value": "18",
"product_name": "Alpine Wool Overshirt",
"sku": "AWO-SND-S"
…
}
]
}
}What it refuses
- 400 A date could not be read, or offset or limit is outside its range.
- 401
key_invalid: "The API key is not valid." No usable key: malformed, unknown, revoked, expired or stopped. One refusal covers all five, because telling a caller which is which maps the surface for them. - 403
scope_missing: "This key cannot read Products." The key may not reach this operation.scope_missingwhen the key does not hold Products;ip_not_allowedwhen the caller's address is outside the key's allowlist.plan_excludeswhen the organisation's plan does not include the public API itself, which is sold with Essentials and is asked of every key minted under that gate. A key minted before the gate existed is never refused by it, whatever the plan holds. Products is ungated, so that is the only plan reason served here. - 429
A page of variants
GET /api/v1/variants/table
Scopes: products:read
A page of variants with the fields a catalogue sync needs per SKU: variant_id and its product_id, product and variant title, sku, vendor, category, description, image, product_status, gtin, unit_cost, sell_price, currency, sales_channels, selected_options, the physical fields (weight, length, width, height, uom, weight_unit), hs_code, country_of_origin, and last_updated_at and last_updated_by.
| Optional | In | What it is |
|---|---|---|
offset | query | Rows to skip before this page. |
limit | query | Rows in this page. |
search | query | Free text over variant title, SKU, product title and vendor. |
filter_args | query | A JSON array of {key, operation, value}. |
sort_args | query | Comma-separated sort columns, - for descending and + or nothing for ascending. |
export | query | Answer a download URL for the filtered set instead of a page of rows. |
Tightly-Version | header | The date train to answer on. |
bash
curl "https://api.app.tightly.io/api/v1/variants/table" \
-H "Authorization: Bearer $TIGHTLY_API_KEY"What it answers
json
{
"data": {
"filtered_max_size": 3288,
"max_size": 14204,
"offset": 0,
"rows": [
{
"category": "Outerwear",
"country_of_origin": "PT",
"currency": "USD",
"description": "A brushed wool overshirt cut for layering.",
"gtin": "05012345678900",
"height": null,
"hs_code": "620331",
"image_url": "https://cdn.tightly.io/p/7412095483953.jpg",
"last_updated_at": "2026-09-01T11:42:00+00:00",
"last_updated_by": "ana.ruiz@example.com",
"length": null,
"product_id": "7412095483953",
"product_status": "ACTIVE",
"product_title": "Alpine Wool Overshirt",
"sales_channels": [
{
"id": "61240442",
"name": "Online Store"
}
],
"selected_options": {
"Colour": "Charcoal",
"Size": "S"
},
"sell_price": 129,
"sku": "AWO-CHR-S",
"unit_cost": 34,
"uom": "each",
"variant_id": "42318902722609",
"variant_title": "Charcoal / S",
"vendor": "Veja",
"weight": 0.62,
"weight_unit": "kg"
…
}
]
}
}What it refuses
- 400 offset or limit is outside its range, or filter_args is not valid JSON.
- 401
key_invalid: "The API key is not valid." No usable key: malformed, unknown, revoked, expired or stopped. One refusal covers all five, because telling a caller which is which maps the surface for them. - 403
scope_missing: "This key cannot read Products." The key may not reach this operation.scope_missingwhen the key does not hold Products;ip_not_allowedwhen the caller's address is outside the key's allowlist.plan_excludeswhen the organisation's plan does not include the public API itself, which is sold with Essentials and is asked of every key minted under that gate. A key minted before the gate existed is never refused by it, whatever the plan holds. Products is ungated, so that is the only plan reason served here. - 429