# Recipe: keep stock levels in sync

Push stock levels from another system into OLET, one product at a time or in a batch.

Source: https://getolet.com/resources/developers/recipe-sync-stock/
Updated: 2026-10-15

Read the current level, then push a correction with `PUT` for one product or `POST /inventory/batch` for many, using `Idempotency-Key` so a retried request never double-applies.

## How do I read current stock

`GET /products/{id}/inventory` returns one row per live outlet the product is stocked at.

```bash
curl https://api.oletapp.com/your-store/v1/products/PRODUCT_ID/inventory \
  -H "Authorization: Bearer olet_live_YOUR_API_KEY"
```

```json
{
  "data": [
    {
      "outlet_id": "...",
      "on_hand": 42,
      "reorder_method": "none",
      "reorder_point": null,
      "reorder_amount": null,
      "reorder_target": null,
      "version": "..."
    }
  ]
}
```

## How do I set one product's stock level

`PUT /products/{id}/inventory/{outlet_id}` sets an absolute level. Send the same `on_hand` twice and the second call changes nothing (`changed: false`), so it is safe to retry without an `Idempotency-Key`, though you may still send one. Without a `reason_id`, the change is recorded under the system reason "Stock set via API". `GET /inventory/adjustment-reasons` lists the reasons you can name instead.

```bash
curl -X PUT https://api.oletapp.com/your-store/v1/products/PRODUCT_ID/inventory/OUTLET_ID \
  -H "Authorization: Bearer olet_live_YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"on_hand": 37}'
```

```json
{
  "data": {
    "changed": true,
    "previous": 42,
    "on_hand": 37,
    "delta": -5,
    "adjustment_id": "...",
    "movement_id": "..."
  }
}
```

## How do I sync many products in one call

`POST /inventory/batch` sets or adjusts up to 100 product/outlet lines in one all-or-nothing request. Each line names exactly one of `set` (absolute) or `adjust` (relative), and exactly one of `product_id` or `sku`. `Idempotency-Key` is required here: a retried batch replayed without one could double-apply a relative `adjust` line.

```bash
curl -X POST https://api.oletapp.com/your-store/v1/inventory/batch \
  -H "Authorization: Bearer olet_live_YOUR_API_KEY" \
  -H "Idempotency-Key: YOUR_UNIQUE_KEY" \
  -H "Content-Type: application/json" \
  -d '{"lines": [
    {"sku": "SKU-001", "outlet_id": "OUTLET_ID", "set": {"on_hand": 37}},
    {"sku": "SKU-002", "outlet_id": "OUTLET_ID", "set": {"on_hand": 12}}
  ]}'
```

```json
{
  "data": [
    { "product_id": "...", "sku": "SKU-001", "outlet_id": "OUTLET_ID", "changed": true, "previous": 42, "on_hand": 37, "delta": -5 },
    { "product_id": "...", "sku": "SKU-002", "outlet_id": "OUTLET_ID", "changed": true, "previous": 10, "on_hand": 12, "delta": 2 }
  ]
}
```

A batch with a bad line, such as an unknown `sku`, writes nothing at all: the whole request fails or the whole request succeeds. The same product and outlet pair can appear only once in a batch. A replayed batch marks each line `"replayed": true`.

## How do I find out when stock changes on the OLET side

There is no delta-sync feed for stock levels the way there is for products and sales. Poll `GET /products/{id}/inventory` for the products you care about on whatever interval fits your integration, and treat the returned `on_hand` as the current truth.

## What if I only have a SKU, not a product id

The batch endpoint and `POST /inventory/adjustments` accept `sku` as an alternative to `product_id`, matched case-insensitively against live products only, never against a barcode. `PUT /products/{id}/inventory/{outlet_id}` takes the product id in its path, so use `GET /products?sku=...` to find the id first.

## What can refuse a stock write

- `422 INSUFFICIENT_STOCK` when an adjustment would take stock below zero.
- `422 REASON_NOT_ALLOWED` when the reason is a system reason or is turned off.
- `422 REASON_DIRECTION_MISMATCH` when a `reason_id` on a set does not match whether stock went up or down.
- `422 OUTLET_DELETED` when the outlet has been deleted.

## Where to go next

See [idempotency](/resources/developers/idempotency/) for the full replay rules, or [errors](/resources/developers/errors/) for what `422 INSUFFICIENT_STOCK` and `422 NOT_STOCK_TRACKED` mean.
