Appearance
Stocktakes
4 reads and 5 writes, on train 2026-11. Scopes: stocktakes:read · stocktakes:write.
| Operation | Method | Scope | Path |
|---|---|---|---|
| Open a stock count over a set of locations and a product selection | POST | stocktakes:write | /api/v1/stocktakes |
| Discard an open count and every line in it | DELETE | stocktakes:write | /api/v1/stocktakes/{stocktake_id} |
| One stock count | GET | stocktakes:read | /api/v1/stocktakes/{stocktake_id} |
| Set counted quantities on the lines of an open count | PATCH | stocktakes:write | /api/v1/stocktakes/{stocktake_id}/counts |
| Apply counted quantities to an open count from an uploaded CSV or Excel file | POST | stocktakes:write | /api/v1/stocktakes/{stocktake_id}/import |
| Close a count, freezing its figures and recording the adjustment | POST | stocktakes:write | /api/v1/stocktakes/{stocktake_id}/post |
| The lines of one count | GET | stocktakes:read | /api/v1/stocktakes/{stocktake_id}/table |
| Every stock count on file | GET | stocktakes:read | /api/v1/stocktakes/table |
| Stock variance measured from posted counts | GET | stocktakes:read | /api/v1/stocktakes/variance/by-supplier |
Open a stock count over a set of locations and a product selection
POST /api/v1/stocktakes
Scopes: stocktakes:write
Opens one count and returns it. The body takes name, counted_on, location_ids (at least one) and then either variant_ids or include_all_variants: true, one or the other, and sending both is refused 400.
| Optional | In | What it is |
|---|---|---|
Tightly-Version | header | The date train to answer on. |
Idempotency-Key | header | A string of your own, up to 255 characters, that makes this write safe to retry. |
bash
curl -X POST "https://api.app.tightly.io/api/v1/stocktakes" \
-H "Authorization: Bearer $TIGHTLY_API_KEY" \
-H "Content-Type: application/json" \
-d @body.jsonWhat to send, as body.json
json
{
"counted_on": "2026-08-10",
"include_all_variants": false,
"location_ids": [
"loc1"
],
"name": "Cycle count of fast movers",
"variant_ids": [
"v1",
"v2"
]
}What it answers
json
{
"data": {
"counted_line_count": 0,
"counted_on": "2026-08-10",
"counted_total": 0,
"id": "1",
"line_count": 24,
"location_names": [
"Collect"
],
"name": "Cycle count of fast movers",
"posted_at": null,
"status": "open",
"system_total": 0,
"variance_total": 0,
"variance_value_total": 0
},
"message": {
"desc": "",
"service": "stocktakes",
"severity": "INFO"
}
}What it refuses
- 400 An empty name; no location; neither
variant_idsnorinclude_all_variants, or both; a location this organisation does not have; a scope resolving to no lines; or one resolving to more than 20,000 lines, refused with the figure, "This selection would create 24,310 lines. Narrow it to 20,000 or fewer." - 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 write Stocktakes." The key may not reach this operation.scope_missingwhen the key holds Stocktakes for reading only, or not at all;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
Discard an open count and every line in it
DELETE /api/v1/stocktakes/{stocktake_id}
Scopes: stocktakes:write
Deletes an open count and its lines outright. Takes no body, and answers 204 with none.
| Required | In | What it is |
|---|---|---|
stocktake_id | path | The open count to discard. |
| Optional | In | What it is |
|---|---|---|
Tightly-Version | header | The date train to answer on. |
Idempotency-Key | header | A string of your own, up to 255 characters, that makes this write safe to retry. |
bash
curl -X DELETE "https://api.app.tightly.io/api/v1/stocktakes/<stocktake_id>" \
-H "Authorization: Bearer $TIGHTLY_API_KEY"What it answers
204, with no body. The count and its lines are gone. No body.
What it refuses
- 400 The count has already been posted, "This count has already been posted and can no longer be changed".
- 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 write Stocktakes." The key may not reach this operation.scope_missingwhen the key holds Stocktakes for reading only, or not at all;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 No count with this id in this organisation.
- 429
One stock count
GET /api/v1/stocktakes/{stocktake_id}
Scopes: stocktakes:read
One count by id, in the shape the table serves it: status, counted_on, the locations it covers, line_count and counted_line_count, and the totals, system_total, counted_total, variance_total and variance_value_total.
| Required | In | What it is |
|---|---|---|
stocktake_id | path | The count's id, as id on every stocktake row. |
| Optional | In | What it is |
|---|---|---|
Tightly-Version | header | The date train to answer on. |
bash
curl "https://api.app.tightly.io/api/v1/stocktakes/<stocktake_id>" \
-H "Authorization: Bearer $TIGHTLY_API_KEY"What it answers
json
{
"data": {
"counted_line_count": 6,
"counted_on": "2026-08-10",
"counted_total": 71,
"id": "1",
"line_count": 24,
"location_names": [
"Collect"
],
"name": "Cycle count of fast movers",
"posted_at": null,
"status": "open",
"system_total": 59,
"variance_total": 12,
"variance_value_total": 288
},
"message": {
"desc": "",
"service": "stocktakes",
"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 Stocktakes." The key may not reach this operation.scope_missingwhen the key does not hold Stocktakes;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 No count with this id in this organisation.
- 429
Set counted quantities on the lines of an open count
PATCH /api/v1/stocktakes/{stocktake_id}/counts
Scopes: stocktakes:write
Writes counted quantities onto lines of an open count. The body is counts, a non-empty array of {variant_id, location_id, counted_quantity}; the pair addresses the line, so a variant counted at two locations is two entries.
| Required | In | What it is |
|---|---|---|
stocktake_id | path | The open count whose lines to write. |
| Optional | In | What it is |
|---|---|---|
Tightly-Version | header | The date train to answer on. |
Idempotency-Key | header | A string of your own, up to 255 characters, that makes this write safe to retry. |
bash
curl -X PATCH "https://api.app.tightly.io/api/v1/stocktakes/<stocktake_id>/counts" \
-H "Authorization: Bearer $TIGHTLY_API_KEY" \
-H "Content-Type: application/json" \
-d @body.jsonWhat to send, as body.json
json
{
"counts": [
{
"counted_quantity": 37,
"location_id": "loc1",
"variant_id": "v1"
},
{
"counted_quantity": 0,
"location_id": "loc1",
"variant_id": "v2"
},
{
"counted_quantity": null,
"location_id": "loc1",
"variant_id": "v3"
}
]
}What it answers
204, with no body. The counts were applied. No body; read the count back for its new totals.
What it refuses
- 400 An empty
countsarray, a negativecounted_quantity, or the count has already been posted, "This count has already been posted and can no longer be changed". - 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 write Stocktakes." The key may not reach this operation.scope_missingwhen the key holds Stocktakes for reading only, or not at all;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 No count with this id in this organisation.
- 429
Apply counted quantities to an open count from an uploaded CSV or Excel file
POST /api/v1/stocktakes/{stocktake_id}/import
Scopes: stocktakes:write
Applies a whole file of counts to an open count. The body is s3_key, a file already uploaded through the shared presigned-URL flow, and mappings, a {our_field: their_header} object in the same shape GET /files/mappings returns. sku and counted_quantity are both required in the mapping; location is optional.
| Required | In | What it is |
|---|---|---|
stocktake_id | path | The open count the file's rows apply to. |
| Optional | In | What it is |
|---|---|---|
Tightly-Version | header | The date train to answer on. |
Idempotency-Key | header | A string of your own, up to 255 characters, that makes this write safe to retry. |
bash
curl -X POST "https://api.app.tightly.io/api/v1/stocktakes/<stocktake_id>/import" \
-H "Authorization: Bearer $TIGHTLY_API_KEY" \
-H "Content-Type: application/json" \
-d @body.jsonWhat to send, as body.json
json
{
"mappings": {
"counted_quantity": "Counted",
"location": "Warehouse",
"sku": "SKU"
},
"s3_key": "uploads/counts.csv"
}What it answers
json
{
"data": {
"skipped": 4,
"unmatched_skus": [
"NOT-IN-COUNT"
],
"updated": 812
},
"message": {
"desc": "",
"service": "stocktakes",
"severity": "INFO"
}
}What it refuses
- 400 A blank
s3_key; a mapping missing the SKU or the counted-quantity column; a file that is empty or cannot be read; or the count has already been posted. - 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 write Stocktakes." The key may not reach this operation.scope_missingwhen the key holds Stocktakes for reading only, or not at all;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 No count with this id in this organisation.
- 429
Close a count, freezing its figures and recording the adjustment
POST /api/v1/stocktakes/{stocktake_id}/post
Scopes: stocktakes:write
Closes the count. Its status becomes posted, posted_at is stamped, and the totals stop moving: from here the count is a record of what was found rather than a working document. Takes no body. The count is identified entirely by its path.
| Required | In | What it is |
|---|---|---|
stocktake_id | path | The open count to close. |
| Optional | In | What it is |
|---|---|---|
Tightly-Version | header | The date train to answer on. |
Idempotency-Key | header | A string of your own, up to 255 characters, that makes this write safe to retry. |
bash
curl -X POST "https://api.app.tightly.io/api/v1/stocktakes/<stocktake_id>/post" \
-H "Authorization: Bearer $TIGHTLY_API_KEY"What it answers
json
{
"data": {
"counted_line_count": 1840,
"counted_on": "2026-06-30",
"counted_total": 41065,
"id": "2",
"line_count": 1840,
"location_names": [
"Collect",
"London 3PL"
],
"name": "Quarter-end full count",
"posted_at": "2026-06-30T18:12:00+00:00",
"status": "posted",
"system_total": 41220,
"variance_total": -155,
"variance_value_total": -3720
},
"message": {
"desc": "",
"service": "stocktakes",
"severity": "INFO"
}
}What it refuses
- 400 Nothing has been counted, "Enter at least one counted quantity before posting", or the count has already been posted.
- 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 write Stocktakes." The key may not reach this operation.scope_missingwhen the key holds Stocktakes for reading only, or not at all;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 No count with this id in this organisation.
- 429
The lines of one count
GET /api/v1/stocktakes/{stocktake_id}/table
Scopes: stocktakes:read
One page of the lines in a count. Each row is one variant at one location: system_quantity (the level snapshotted when the count was created), counted_quantity, variance and variance_value, with sku, product_title, variant_title, location_name and unit_cost for display.
| Required | In | What it is |
|---|---|---|
stocktake_id | path | The count whose lines to read. |
| Optional | In | What it is |
|---|---|---|
search | query | Match against product title and SKU. |
limit | query | How many rows to return. Defaults to 25; the platform cap is 10,000. |
offset | query | How many rows to skip. Page against filtered_max_size, not max_size. |
line_scope | query | Which lines to return. |
variance_only | query | The older spelling of line_scope=differences, honoured where line_scope is not given. Prefer line_scope. |
sort_args | query | Sort fields, comma-separated, - for descending. |
Tightly-Version | header | The date train to answer on. |
bash
curl "https://api.app.tightly.io/api/v1/stocktakes/<stocktake_id>/table" \
-H "Authorization: Bearer $TIGHTLY_API_KEY"What it answers
json
{
"data": {
"filtered_max_size": 24,
"max_size": 24,
"offset": 0,
"rows": [
{
"counted_quantity": 37,
"id": "10",
"location_id": "loc1",
"location_name": "Collect",
"product_title": "Merino Crew",
"sku": "CREW-NAVY-M",
"system_quantity": 40,
"unit_cost": 24,
"variance": -3,
"variance_value": -72,
"variant_id": "v1",
"variant_image": null,
"variant_title": "Navy / M"
}
],
"size": 1
},
"message": {
"desc": "",
"service": "stocktakes",
"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 Stocktakes." The key may not reach this operation.scope_missingwhen the key does not hold Stocktakes;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 No count with this id in this organisation.
- 429
Every stock count on file
GET /api/v1/stocktakes/table
Scopes: stocktakes:read
One page of counts, newest scope first, each row carrying what the count covers and what it found: line_count and counted_line_count, the system_total and counted_total over the counted lines only, and variance_total (which is exactly counted_total - system_total) with variance_value_total beside it.
| Optional | In | What it is |
|---|---|---|
search | query | Match against the count's name. Omitted, every count is in scope. |
limit | query | How many rows to return. Defaults to 10; the platform cap is 10,000. |
offset | query | How many rows to skip. Page against filtered_max_size, not max_size. |
sort_args | query | Sort fields, comma-separated, - for descending and + or nothing for ascending. |
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/stocktakes/table" \
-H "Authorization: Bearer $TIGHTLY_API_KEY"What it answers
json
{
"data": {
"filtered_max_size": 2,
"max_size": 2,
"offset": 0,
"rows": [
{
"counted_line_count": 6,
"counted_on": "2026-08-10",
"counted_total": 71,
"id": "1",
"line_count": 24,
"location_names": [
"Collect"
],
"name": "Cycle count of fast movers",
"posted_at": null,
"status": "open",
"system_total": 59,
"variance_total": 12,
"variance_value_total": 288
},
{
"counted_line_count": 1840,
"counted_on": "2026-06-30",
"counted_total": 41065,
"id": "2",
"line_count": 1840,
"location_names": [
"Collect",
"London 3PL"
],
"name": "Quarter-end full count",
"posted_at": "2026-06-30T18:12:00+00:00",
"status": "posted",
"system_total": 41220,
"variance_total": -155,
"variance_value_total": -3720
}
]
…
}
}What it refuses
- 400
filter_argsis not JSON, names a key other thanstatus, or carries a value that is notopenorposted; orlimitis over 10,000. - 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 Stocktakes." The key may not reach this operation.scope_missingwhen the key does not hold Stocktakes;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
Stock variance measured from posted counts
GET /api/v1/stocktakes/variance/by-supplier
Scopes: stocktakes:read
Reads system_quantity against counted_quantity on POSTED counts only, and puts each counted line to the variant's default supplier. Open counts are never read: an unfinished count is not a measurement.
| Optional | In | What it is |
|---|---|---|
counted_from | query | Include only counts whose counted_on is on or after this date. |
counted_to | query | Include only counts whose counted_on is on or before this date. A counted_from after counted_to is refused 400. |
location_id | query | Limit the read to these locations. |
Tightly-Version | header | The date train to answer on. |
bash
curl "https://api.app.tightly.io/api/v1/stocktakes/variance/by-supplier" \
-H "Authorization: Bearer $TIGHTLY_API_KEY"What it answers
json
{
"data": {
"buckets": [
{
"attribution": "default_supplier",
"attribution_reason": null,
"figures": {
"cost_reason": null,
"counted_line_count": 640,
"currency": "USD",
"currency_reason": null,
"gain_cost": 2304,
"gain_line_count": 21,
"gain_units": 96,
"gain_units_unpriced": 0,
"loss_cost": 19488,
"loss_line_count": 74,
"loss_units": 812,
"loss_units_unpriced": 0,
"matched_line_count": 545,
"variant_count": 210
},
"multi_supplier_line_count": 38,
"supplier_id": "sup_88",
"supplier_name": "Northbound Textiles"
},
{
"attribution": "no_default_supplier",
"attribution_reason": "These products are bought from more than one supplier and none is marked as the main one, so the variance cannot be put to a single supplier.",
"figures": {
"cost_reason": "60 of the 161 units that moved have no unit cost on file, so the value covers only the rest.",
"counted_line_count": 96,
"currency": "USD",
"currency_reason": null,
"gain_cost": 414,
"gain_line_count": 4,
"gain_units": 18,
"gain_units_unpriced": 0,
"loss_cost": 1992,
"loss_line_count": 11
…
}
}
]
}
}What it refuses
- 400 A date could not be read, or
counted_fromis aftercounted_to. - 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 Stocktakes." The key may not reach this operation.scope_missingwhen the key does not hold Stocktakes;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