# Live prices and stock

Change a price, a sale or the stock between two catalogs, and search shows it within seconds, without an index run.

Prices and stock change all day, while the catalog may arrive once a night. `PUT /api/v1/offers` writes such changes into the live search in between. This page changes the price of the pendant lamp `LEUCHTE-KUGEL` (Kugel: globe) and puts it back. Its two variants first:

```sh title="Terminal"
curl -s 'http://127.0.0.1:7800/v1/product?id=LEUCHTE-KUGEL' | jq -c '.product.variants[] | {id, price: .offer.price}'
```

```json title="Answer: 200 · 4.1 ms · release b25a6aaa"
{"id":"LEUCHTE-KUGEL-KLAR","price":149.0}
{"id":"LEUCHTE-KUGEL-RAUCH","price":159.0}
```

The lamp comes in Klarglas (clear glass) and Rauchglas (smoked glass), at 149 € and 159 €.

## Change a price

Send each change as its product, its [variant](/docs/reference/glossary#variant), the [channel](/docs/reference/glossary#channel) and the fields that change. Each change is answered on its own:

```sh title="Terminal"
changed=$(curl -s -X PUT -H "Authorization: Bearer $key" -H 'Content-Type: application/json' $api/offers -d '{"offers": [
  {"product": "LEUCHTE-KUGEL", "variant": "LEUCHTE-KUGEL-KLAR", "channel": "de", "price": 129},
  {"product": "LEUCHTE-KUGEL", "channel": "de", "price": 129}
]}')
revision=$(jq .revision <<< "$changed")
jq -c '.rows[]' <<< "$changed"
```

```json title="Answer: 202 · 23 ms"
{"state":"accepted"}
{"state":"rejected","code":"variant_needed","message":"\"LEUCHTE-KUGEL\" has variants, so name the one whose offer changes in `variant`."}
```

The first change is kept. The second is left out, since a product with variants names the one that changes; a product the live catalog lacks is left out too, as new products come with the catalog. The kept changes make one revision, the answer's `revision`.

No [index run](/docs/reference/glossary#index-run) is queued. The indexer waits a second for changes that follow, compiles the changed products again and replaces their documents in the live indexes. Every answer's [trace](/docs/reference/glossary#trace) names the revision it reads in `live.offers`, so wait until it names this one:

```sh title="Terminal"
for _ in {1..30}; do
  curl -s 'http://127.0.0.1:7800/v1/product?id=LEUCHTE-KUGEL&debug=1' \
    | jq -e ".trace.live.offers >= $revision" >/dev/null && break
  sleep 1
done
curl -s 'http://127.0.0.1:7800/v1/product?id=LEUCHTE-KUGEL' | jq -c '.product.variants[] | {id, price: .offer.price}'
```

```json title="Answer: 200 · 13 ms · release b25a6aaa"
{"id":"LEUCHTE-KUGEL-KLAR","price":129}
{"id":"LEUCHTE-KUGEL-RAUCH","price":159.0}
```

The clear lamp now costs 129 € on its tile and its product page alike. Release and [snapshot](/docs/reference/glossary#snapshot) stay as they were, and with the revision they name what answered. `GET $api/live` names the revision too, with when it arrived, and the panel shows that time as **Prices and stock** on **Overview** and **Releases**.

**Reference:** [`PUT /api/v1/offers`](/docs/reference/management-api#change-offers) · [`rows`](/docs/reference/management-api#change-offers.answer.rows) · [`GET /api/v1/live`](/docs/reference/management-api#live)

## Put it back

A change replaces only the fields it names, so the old price is one more change away:

```sh title="Terminal"
revision=$(curl -s -X PUT -H "Authorization: Bearer $key" -H 'Content-Type: application/json' $api/offers \
  -d '{"offers": [{"product": "LEUCHTE-KUGEL", "variant": "LEUCHTE-KUGEL-KLAR", "channel": "de", "price": 149}]}' \
  | jq .revision)
for _ in {1..30}; do
  curl -s 'http://127.0.0.1:7800/v1/product?id=LEUCHTE-KUGEL&debug=1' \
    | jq -e ".trace.live.offers >= $revision" >/dev/null && break
  sleep 1
done
curl -s 'http://127.0.0.1:7800/v1/product?id=LEUCHTE-KUGEL' | jq -c '.product.variants[] | {id, price: .offer.price}'
```

```json title="Answer: 200 · 6.5 ms · release b25a6aaa"
{"id":"LEUCHTE-KUGEL-KLAR","price":149}
{"id":"LEUCHTE-KUGEL-RAUCH","price":159.0}
```

Both lamps cost what the catalog says again.

## Sales, stock and availability

A change may name any field of an offer, and the fields it leaves out stay as they are. This one puts the smoked lamp on sale for a weekend and sells out the clear one:

```sh title="Terminal"
curl -s -X PUT -H "Authorization: Bearer $key" -H 'Content-Type: application/json' $api/offers -d '{"offers": [
  {"product": "LEUCHTE-KUGEL", "variant": "LEUCHTE-KUGEL-RAUCH", "channel": "de", "sale_price": 139,
   "sale_window": {"from": "2026-11-27T00:00:00+01:00", "until": "2026-11-30T00:00:00+01:00"}},
  {"product": "LEUCHTE-KUGEL", "variant": "LEUCHTE-KUGEL-KLAR", "channel": "de", "availability": "out_of_stock", "stock": 0}
]}'
```

`end_sale: true` ends a sale, and `delivery_days` changes the delivery time. A sale window that opens or closes rebuilds the search at that moment, as it does for a sale the catalog sets. Prices are what search shows; the checkout decides what the shopper pays.

**Reference:** [`sale_price`](/docs/reference/management-api#change-offers.offers.sale_price) · [`end_sale`](/docs/reference/management-api#change-offers.offers.end_sale) · [`availability`](/docs/reference/management-api#change-offers.offers.availability)

## When the next catalog arrives

A change applies to every catalog that first arrived before it. Another catalog supersedes it, since the shop says what holds now:

| What comes next                    | The change                                    |
| ---------------------------------- | --------------------------------------------- |
| The same catalog, sent again       | Stays                                         |
| Another catalog                    | Goes, and the catalog's offer holds           |
| A rebuild, such as after a publish | Stays, as does every change since its catalog |

A change sent while a new catalog's index run builds applies to that catalog too.

<Callout type="warn">
    Another catalog undoes every change sent before it. Keep the shop's export current, so it carries the new prices
    too.
</Callout>

Until the next index run, what the run derived from the whole catalog stays as it was: the dictionary's counts, derived price buckets and how many values each facet counts.

## Limits

- **1,000 changes per call.** A larger batch is refused whole with a `422`, so send a large sync in calls of 1,000, back to back.
- **One write a second.** Changes that arrive close together become one write, so a burst batches itself.
- **A catalog first.** Before any catalog is live, the call answers `409`.

**Reference:** [`offers_per_call`](/docs/reference/limits#offers_per_call) · [`409`](/docs/reference/management-api#change-offers.409)

## Next

- [Variants](/docs/variants): what a variant's offer holds.
- [Channels](/docs/channels): the channels and currencies an offer belongs to.
- [Importing and mapping](/docs/importing): send the whole catalog.
