Inventory
Read and adjust the stock of each variant across several locations with the Admin API.
Track the stock of the "Polo de algodón" at Almacén Lima and Tienda Miraflores: read each level, adjust it without overwriting other changes, and update many levels in one batch.
Requirements
- An Admin token in
ECOMIQ_TOKEN. See Quickstart. - The
settings:readpermission for locations,inventory:readto read stock, andinventory:updateto change it. - Variants with
trackInventory: trueand at least one active location.
List the locations
Request the active locations: only they accept stock adjustments. Store the
id of each one next to its code.
curl "https://api.ecomiq.pe/api/v1/fulfillment/locations?isActive=true&limit=50" \
-H "Authorization: Bearer $ECOMIQ_TOKEN"{
"data": [
{
"id": "7204558912004325378",
"name": "Almacén Lima",
"code": "ALM-LIMA",
"type": "warehouse",
"isActive": true,
"isDefault": true
},
{
"id": "7204558912004325380",
"name": "Tienda Miraflores",
"code": "TDA-MIRAFLORES",
"type": "store",
"isActive": true,
"isDefault": false
}
],
"hasNextPage": false,
"nextCursor": null
}If hasNextPage is true, request the next page with nextCursor. See
Pagination.
Read stock per location
POST / returns one level per variant and location.
locationIds is optional: without it, you get every location with a level.
This example reads SKU POL-001-AZ-M of the Polo de algodón.
curl -X POST https://api.ecomiq.pe/api/v1/inventory/levels \
-H "Authorization: Bearer $ECOMIQ_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"variantIds": ["7204558912004325377"],
"locationIds": ["7204558912004325378", "7204558912004325380"]
}'[
{
"id": "7204558912004325390",
"variantId": "7204558912004325377",
"locationId": "7204558912004325378",
"onHand": 12,
"committed": 2,
"unavailable": 0,
"available": 10,
"incoming": 0,
"version": 4
},
{
"id": "7204558912004325391",
"variantId": "7204558912004325377",
"locationId": "7204558912004325380",
"onHand": 3,
"committed": 0,
"unavailable": 0,
"available": 3,
"incoming": 0,
"version": 2
}
]If a location is missing, the variant has no level there yet.
| Field | What it counts |
|---|---|
onHand | Physical units at the location. |
committed | Reserved units, for example for orders. |
unavailable | Units that cannot be sold. |
available | What can be sold: onHand - committed - unavailable. |
incoming | Units to receive. They do not count in onHand yet. |
version | Version of the level. It goes up with every change. |
Adjust one level
Send the variant, the location, and the version you read in
expectedVersion. If the variant has no level at that location, send 0: the
API creates the level.
mode: "adjust"addsquantityto the current value. Use a negative number to subtract.mode: "set"sets the value.quantitycannot be negative.target: "onHand"changes the physical units.target: "available"changes what can be sold. The API computesonHandby addingcommittedandunavailable.
This example records five units received at Almacén Lima.
curl -X POST https://api.ecomiq.pe/api/v1/inventory/adjust \
-H "Authorization: Bearer $ECOMIQ_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"variantId": "7204558912004325377",
"locationId": "7204558912004325378",
"target": "onHand",
"mode": "adjust",
"quantity": 5,
"reason": "restock",
"note": "Ingreso de cinco unidades.",
"expectedVersion": 4
}'{
"id": "7204558912004325390",
"variantId": "7204558912004325377",
"locationId": "7204558912004325378",
"onHand": 17,
"committed": 2,
"unavailable": 0,
"available": 15,
"incoming": 0,
"version": 5,
"createdOn": "2026-09-12T14:05:00Z",
"updatedOn": "2026-10-04T15:30:00Z"
}The response returns the new version.
reason is kept in the
movement history.
Values and fields are in
Set or adjust inventory stock.
Resolve a conflict
Every change raises the version, including an order reservation. With a stale
version, the API responds 409 Inventory.Changed and applies nothing.
In this example you read version 2 at Tienda Miraflores. Then an order
reserved one unit and the version moved to 3. Your adjustment with 2 gets:
{
"title": "Conflict",
"status": 409,
"detail": "Inventory changed. Reload and retry.",
"code": "Inventory.Changed"
}Read the level again and retry with the new version:
# 1. Read the current version: it is now 3
curl -X POST https://api.ecomiq.pe/api/v1/inventory/levels \
-H "Authorization: Bearer $ECOMIQ_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"variantIds": ["7204558912004325377"],
"locationIds": ["7204558912004325380"]
}'
# 2. Retry with that version
curl -X POST https://api.ecomiq.pe/api/v1/inventory/adjust \
-H "Authorization: Bearer $ECOMIQ_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"variantId": "7204558912004325377",
"locationId": "7204558912004325380",
"target": "onHand",
"mode": "adjust",
"quantity": -1,
"reason": "damage",
"note": "Una unidad con falla.",
"expectedVersion": 3
}'{
"locationId": "7204558912004325380",
"onHand": 2,
"committed": 1,
"available": 1,
"version": 4
}With set, check that the value is still right before you retry. If a request
got no response, read the level before repeating an adjust: the first attempt
may have been applied.
Update in bulk
POST / accepts up to 100 rows. Each row has a
variant, a location, mode, quantity, and expectedVersion. reason and
note apply to the whole batch.
This count sets three levels. The last row is POL-001-AZ-L, with no level at
Tienda Miraflores: that is why it sends expectedVersion: 0.
{
"items": [
{
"variantId": "7204558912004325377",
"locationId": "7204558912004325378",
"mode": "set",
"quantity": 16,
"expectedVersion": 5
},
{
"variantId": "7204558912004325377",
"locationId": "7204558912004325380",
"mode": "set",
"quantity": 3,
"expectedVersion": 4
},
{
"variantId": "7204558912004325379",
"locationId": "7204558912004325380",
"mode": "set",
"quantity": 6,
"expectedVersion": 0
}
],
"reason": "inventoryCount",
"note": "Conteo de fin de mes."
}curl -X POST https://api.ecomiq.pe/api/v1/inventory/batch \
-H "Authorization: Bearer $ECOMIQ_TOKEN" \
-H "Content-Type: application/json" \
--data @count.json{
"results": [
{
"index": 0,
"id": "7204558912004325377",
"status": "updated",
"errors": []
},
{
"index": 1,
"id": "7204558912004325377",
"status": "updated",
"errors": []
},
{
"index": 2,
"id": "7204558912004325379",
"status": "failed",
"errors": [
{
"field": null,
"code": "Inventory.Changed",
"message": "Inventory changed. Reload and retry."
}
]
}
]
}The API responds 200 with each row's result in results[], in input order.
Each row is applied on its own: one with a stale version or an inactive location
fails and the rest continue. Read the levels of the failed rows again and
resend them with their new version.
- For more than 100 rows, upload a file to
POST /or split into batches of up to 100.v1/ inventory/ imports - Send
Idempotency-Keyand reuse the same key when retrying the same batch. - To sync from your ERP, use
set: repeating asetrow doesn't add units twice.
The batch always changes onHand
Rows don't accept target. To set available, use
POST /.
Errors
| Status | code | What to do |
|---|---|---|
400 | Inventory.OnHandUnderflow, Inventory.AvailableUnderflow | The change leaves stock negative: check quantity. |
404 | Locations.NotFound | The location doesn't exist or is inactive. |
409 | Inventory.Changed | Read the level again and retry with the new version. |
409 | Inventory.NotTracked | The variant doesn't track stock: enable trackInventory. |
Next steps
- ERP integration: token, products, and orders from your system.
- Events: receive
catalog.stock.changedon every stock change. - Inventory: every stock operation.
Last updated: