Skip to content
Catalog

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:read permission for locations, inventory:read to read stock, and inventory:update to change it.
  • Variants with trackInventory: true and 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"
Response (excerpt)
{
  "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 /v1/inventory/levels 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"]
  }'
Response (excerpt)
[
  {
    "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.

FieldWhat it counts
onHandPhysical units at the location.
committedReserved units, for example for orders.
unavailableUnits that cannot be sold.
availableWhat can be sold: onHand - committed - unavailable.
incomingUnits to receive. They do not count in onHand yet.
versionVersion 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" adds quantity to the current value. Use a negative number to subtract.
  • mode: "set" sets the value. quantity cannot be negative.
  • target: "onHand" changes the physical units.
  • target: "available" changes what can be sold. The API computes onHand by adding committed and unavailable.

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
  }'
Response
{
  "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:

Error response
{
  "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
  }'
Response (excerpt)
{
  "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 /v1/inventory/batch 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.

count.json
{
  "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
Response
{
  "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 /v1/inventory/imports or split into batches of up to 100.
  • Send Idempotency-Key and reuse the same key when retrying the same batch.
  • To sync from your ERP, use set: repeating a set row doesn't add units twice.

The batch always changes onHand

Rows don't accept target. To set available, use POST /v1/inventory/adjust.

Errors

StatuscodeWhat to do
400Inventory.OnHandUnderflow, Inventory.AvailableUnderflowThe change leaves stock negative: check quantity.
404Locations.NotFoundThe location doesn't exist or is inactive.
409Inventory.ChangedRead the level again and retry with the new version.
409Inventory.NotTrackedThe variant doesn't track stock: enable trackInventory.

Next steps

  • ERP integration: token, products, and orders from your system.
  • Events: receive catalog.stock.changed on every stock change.
  • Inventory: every stock operation.

Last updated:

On this page