Skip to content

Stocktakes ​

4 reads and 5 writes, on train 2026-11. Scopes: stocktakes:read · stocktakes:write.

OperationMethodScopePath
Open a stock count over a set of locations and a product selectionPOSTstocktakes:write/api/v1/stocktakes
Discard an open count and every line in itDELETEstocktakes:write/api/v1/stocktakes/{stocktake_id}
One stock countGETstocktakes:read/api/v1/stocktakes/{stocktake_id}
Set counted quantities on the lines of an open countPATCHstocktakes:write/api/v1/stocktakes/{stocktake_id}/counts
Apply counted quantities to an open count from an uploaded CSV or Excel filePOSTstocktakes:write/api/v1/stocktakes/{stocktake_id}/import
Close a count, freezing its figures and recording the adjustmentPOSTstocktakes:write/api/v1/stocktakes/{stocktake_id}/post
The lines of one countGETstocktakes:read/api/v1/stocktakes/{stocktake_id}/table
Every stock count on fileGETstocktakes:read/api/v1/stocktakes/table
Stock variance measured from posted countsGETstocktakes: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.

OptionalInWhat it is
Tightly-VersionheaderThe date train to answer on.
Idempotency-KeyheaderA string of your own, up to 255 characters, that makes this write safe to retry.
bash
curl -X POST "https://api.app.tightly.io/api/v1/stocktakes" \
  -H "Authorization: Bearer $TIGHTLY_API_KEY" \
  -H "Content-Type: application/json" \
  -d @body.json

What 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_ids nor include_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_missing when the key holds Stocktakes for reading only, or not at all; organization_mismatch when the path names an organisation that is not the key's; ip_not_allowed when the caller's address is outside the key's allowlist.
  • 429

Try it in the reference

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.

RequiredInWhat it is
stocktake_idpathThe open count to discard.
OptionalInWhat it is
Tightly-VersionheaderThe date train to answer on.
Idempotency-KeyheaderA string of your own, up to 255 characters, that makes this write safe to retry.
bash
curl -X 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_missing when the key holds Stocktakes for reading only, or not at all; organization_mismatch when the path names an organisation that is not the key's; ip_not_allowed when the caller's address is outside the key's allowlist.
  • 404 No count with this id in this organisation.
  • 429

Try it in the reference

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.

RequiredInWhat it is
stocktake_idpathThe count's id, as id on every stocktake row.
OptionalInWhat it is
Tightly-VersionheaderThe 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_missing when the key does not hold Stocktakes; organization_mismatch when the path names an organisation that is not the key's; ip_not_allowed when the caller's address is outside the key's allowlist.
  • 404 No count with this id in this organisation.
  • 429

Try it in the reference

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.

RequiredInWhat it is
stocktake_idpathThe open count whose lines to write.
OptionalInWhat it is
Tightly-VersionheaderThe date train to answer on.
Idempotency-KeyheaderA string of your own, up to 255 characters, that makes this write safe to retry.
bash
curl -X 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.json

What 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 counts array, a negative counted_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_missing when the key holds Stocktakes for reading only, or not at all; organization_mismatch when the path names an organisation that is not the key's; ip_not_allowed when the caller's address is outside the key's allowlist.
  • 404 No count with this id in this organisation.
  • 429

Try it in the reference

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.

RequiredInWhat it is
stocktake_idpathThe open count the file's rows apply to.
OptionalInWhat it is
Tightly-VersionheaderThe date train to answer on.
Idempotency-KeyheaderA string of your own, up to 255 characters, that makes this write safe to retry.
bash
curl -X POST "https://api.app.tightly.io/api/v1/stocktakes/<stocktake_id>/import" \
  -H "Authorization: Bearer $TIGHTLY_API_KEY" \
  -H "Content-Type: application/json" \
  -d @body.json

What 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_missing when the key holds Stocktakes for reading only, or not at all; organization_mismatch when the path names an organisation that is not the key's; ip_not_allowed when the caller's address is outside the key's allowlist.
  • 404 No count with this id in this organisation.
  • 429

Try it in the reference

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.

RequiredInWhat it is
stocktake_idpathThe open count to close.
OptionalInWhat it is
Tightly-VersionheaderThe date train to answer on.
Idempotency-KeyheaderA string of your own, up to 255 characters, that makes this write safe to retry.
bash
curl -X POST "https://api.app.tightly.io/api/v1/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_missing when the key holds Stocktakes for reading only, or not at all; organization_mismatch when the path names an organisation that is not the key's; ip_not_allowed when the caller's address is outside the key's allowlist.
  • 404 No count with this id in this organisation.
  • 429

Try it in the reference

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.

RequiredInWhat it is
stocktake_idpathThe count whose lines to read.
OptionalInWhat it is
searchqueryMatch against product title and SKU.
limitqueryHow many rows to return. Defaults to 25; the platform cap is 10,000.
offsetqueryHow many rows to skip. Page against filtered_max_size, not max_size.
line_scopequeryWhich lines to return.
variance_onlyqueryThe older spelling of line_scope=differences, honoured where line_scope is not given. Prefer line_scope.
sort_argsquerySort fields, comma-separated, - for descending.
Tightly-VersionheaderThe 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_missing when the key does not hold Stocktakes; organization_mismatch when the path names an organisation that is not the key's; ip_not_allowed when the caller's address is outside the key's allowlist.
  • 404 No count with this id in this organisation.
  • 429

Try it in the reference

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.

OptionalInWhat it is
searchqueryMatch against the count's name. Omitted, every count is in scope.
limitqueryHow many rows to return. Defaults to 10; the platform cap is 10,000.
offsetqueryHow many rows to skip. Page against filtered_max_size, not max_size.
sort_argsquerySort fields, comma-separated, - for descending and + or nothing for ascending.
filter_argsqueryA JSON array of {key, operation, value}.
Tightly-VersionheaderThe 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_args is not JSON, names a key other than status, or carries a value that is not open or posted; or limit is 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_missing when the key does not hold Stocktakes; organization_mismatch when the path names an organisation that is not the key's; ip_not_allowed when the caller's address is outside the key's allowlist.
  • 429

Try it in the reference

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.

OptionalInWhat it is
counted_fromqueryInclude only counts whose counted_on is on or after this date.
counted_toqueryInclude only counts whose counted_on is on or before this date. A counted_from after counted_to is refused 400.
location_idqueryLimit the read to these locations.
Tightly-VersionheaderThe 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_from is after counted_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_missing when the key does not hold Stocktakes; organization_mismatch when the path names an organisation that is not the key's; ip_not_allowed when the caller's address is outside the key's allowlist.
  • 429

Try it in the reference

Tightly API, version 2026-11.