Keeping stock in sync
Read stock levels and follow stock changes over time without missing or repeating records.
There are two ways to get stock out of Colleqtive, and they answer different questions.
“What is the stock right now?” Use GET /storeproducts/stock. It returns the current
level per product per store.
“What changed since I last looked?” Use GET /products/stock/storeproductlogs. It
returns individual movements: losses, gains, corrections and counts. This is the one to
use for an ongoing sync, because a total tells you nothing about why it moved.
Reading current stock
Section titled “Reading current stock”curl "$BASE_URL/api/v2/public/storeproducts/stock?store_number=001&page_size=100" \ -H "Authorization: Bearer $COLLEQTIVE_BEARER_TOKEN"Two flags change the shape of the response:
include_locations=truebreaks each product down per location. This makes the response much larger, so only ask for it when you need location detail.exclude_customer_order_stock=trueleaves out stock already reserved for customer orders, which gives you what is genuinely available to sell.
For a single product, use GET /stores/{store_number}/products/{product_number}/stock.
Lines with no stock are left out of that response unless you pass
include_zero_lines=true. Pass it when you are reconciling against a full product list,
otherwise products that dropped to zero look like they disappeared.
Following changes over time
Section titled “Following changes over time”The movement log supports two ways of paging forward. Both work. Pick one and stay with it.
By date
Section titled “By date”last_modified_date is a lower bound and it is exclusive. Only movements recorded
strictly after that moment come back. That is deliberate: store the newest timestamp you
have seen, send it back next time, and you will not read the same record twice.
curl "$BASE_URL/api/v2/public/products/stock/storeproductlogs?last_modified_date=2026-08-26T09:00:00&page_size=500" \ -H "Authorization: Bearer $COLLEQTIVE_BEARER_TOKEN"to_modified_date is the matching upper bound, also exclusive. Use both together to
read a fixed window, for example when backfilling one day at a time.
Current movements or archived history
Section titled “Current movements or archived history”is_historic decides which set of records you read. There are two, held in separate
tables, and a movement is in one or the other.
Left out, or false. You read the current movement log. The date filters apply to
when the movement was recorded, and both bounds are exclusive.
Set to true. You read the archived movement history instead. The date filters now
apply to when the stock moved, and both bounds are inclusive.
Two consequences worth planning for:
- A sync that only ever calls the default never sees archived movements. If you need the full picture, you have to read both.
- Because the bounds flip from exclusive to inclusive, passing back the newest timestamp
you saw will return that record a second time when
is_historic=true. Usefrom_idpaging there, or drop the duplicate yourself.
By identifier
Section titled “By identifier”Every movement has an identifier that never changes. from_id is an inclusive lower
bound:
curl "$BASE_URL/api/v2/public/products/stock/storeproductlogs?from_id=184320&page_size=500" \ -H "Authorization: Bearer $COLLEQTIVE_BEARER_TOKEN"Store the highest id you processed and send it as from_id next time. to_id closes
the range, but it is only applied when from_id is also set, and it is ignored when it
is lower than from_id.
Filtering what comes back
Section titled “Filtering what comes back”is_loss and is_gain filter on the class of the reason code rather than on a single
code. Leave them out for no filtering, true to keep only that class, false to exclude
it. This saves you from fetching the reason code table and passing raw codes.
# only losses, for one store, since a known pointcurl "$BASE_URL/api/v2/public/products/stock/storeproductlogs?store_number=001&is_loss=true&from_id=184320" \ -H "Authorization: Bearer $COLLEQTIVE_BEARER_TOKEN"reason_code filters on one specific code when you do need that precision.
article_number matches exactly. A partial product number returns nothing rather
than a partial match.