> ## Documentation Index
> Fetch the complete documentation index at: https://docs.alura.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Inventory and variations

> Replace a listing's whole inventory, or change single variants by their property values

Set prices, stock and SKUs per variant, add or remove variation values, or change one variant without touching the rest. A listing with variations (sizes, colors) has one **product** per combination of values. Each product has a SKU and one **offering**: its price, quantity, whether it is enabled, and its processing profile. Two REST writes change them:

* **Replace inventory** (`PUT /v1/workspace/listings/{id}/inventory`) sends the whole inventory.
* **Patch offerings** (`PATCH /v1/workspace/listings/{id}/inventory`) changes a few variants and leaves the rest as Etsy holds them.

For a listing without variations, Update a listing (`PATCH /v1/workspace/listings/{id}`) already sets its price, quantity and SKU. With `apply_to: "all_variants"` it also sets one price or quantity on every variant at once, over REST and with `update_my_listing` in an AI assistant. A write is charged only when it lands on Etsy, and `dry_run=true` shows the planned write for free. See [API credits](https://docs.alura.io/api-reference/credits-and-limits.md).

## Replace the whole inventory

Replace inventory works like Etsy's own inventory write: it replaces everything. A product you leave out is removed from the listing, and so is a `price_on_property`, `quantity_on_property`, `sku_on_property` or `processing_profile_on_property` you leave out. Read the current inventory first with `GET /v1/workspace/listings/{id}?expand=inventory`, change it, and send all of it back: per product its `sku` and `property_values` (`property_id`, `property_name`, `values`, and `value_ids` and `scale_id` when set), per offering its `price` (the `amount` string), `quantity`, `is_enabled` and `processing_profile_id`, and the four `*_on_property` lists. Leave out the `id`s: Etsy issues new ones.

This replaces a mug's inventory with two sizes, each with its own price, stock and SKU:

```bash theme={null}
curl --fail-with-body -sS -X PUT "https://api.alura.io/v1/workspace/listings/$LISTING_ID/inventory" \
  -H "Authorization: Bearer $ALURA_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{
    "products": [
      {"sku": "MUG-A4", "property_values": [{"property_id": "513", "property_name": "Size", "values": ["A4"]}],
       "offerings": [{"price": "22.00", "quantity": 3, "is_enabled": true, "processing_profile_id": "'"$PROFILE_ID"'"}]},
      {"sku": "MUG-A5", "property_values": [{"property_id": "513", "property_name": "Size", "values": ["A5"]}],
       "offerings": [{"price": "16.50", "quantity": 4, "is_enabled": true, "processing_profile_id": "'"$PROFILE_ID"'"}]}
    ],
    "price_on_property": ["513"],
    "quantity_on_property": ["513"],
    "sku_on_property": ["513"]
  }'
```

* **Add a variation value** by adding its products; **remove one** by leaving its products out. Going back to a single product (`"property_values": []`) removes the variations. Removing a value or a variation removes its variation images on Etsy too, and a revert doesn't bring them back.
* **Every offering of a physical listing needs a `processing_profile_id`.** Without one the call is 400 before anything is sent.
* `property_id` is a category property (`GET /v1/etsy/categories/{id}/properties`), or 513 and 514 for your shop's own variations.

## Change single variants

A patch names each variant by its property values, in `select`, and sends only what changes. This sets the price of a mug's black variant (its price varies by color, so `price_on_property` already names the color property):

```bash cURL theme={null}
# Use a new Idempotency-Key for each write, and the same one to retry it.
# Set KEY to the same value to retry this write.
KEY=${KEY:-inventory-$(date +%s)-$RANDOM}
# A 202 answers the change ("object": "workspace.change") with a Location to poll: don't send the write again.
curl -i --fail-with-body -sS -X PATCH https://api.alura.io/v1/workspace/listings/1834720561/inventory \
  -H "Authorization: Bearer $ALURA_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $KEY" \
  -d '{"offerings": [{"select": {"Primary color": "Black"}, "price": "21.00"}]}'
```

```typescript TypeScript theme={null}
interface Inventory {
  products: { id: string; sku: string | null; property_values: { values: string[] }[]; offerings: { price: { amount: string } | null; quantity: number | null }[] }[] | null;
  change: { id: string; status: string };
}

// One key per write: keep it to retry this write, and the retry answers from the first write instead of writing again.
const idempotencyKey = crypto.randomUUID();
const res = await fetch('https://api.alura.io/v1/workspace/listings/1834720561/inventory', {
  method: 'PATCH',
  headers: { Authorization: `Bearer ${process.env.ALURA_API_KEY}`, 'Content-Type': 'application/json', 'Idempotency-Key': idempotencyKey },
  body: JSON.stringify({ offerings: [{ select: { 'Primary color': 'Black' }, price: '21.00' }] }),
});
if (!res.ok) throw new Error(`${res.status}: ${await res.text()}`);
if (res.status === 202) {
  // Etsy is still answering: the body is the change. Poll it until its outcome is known; don't send the write again.
  const change = (await res.json()) as { id: string; outcome: string };
  console.log(`change ${change.id} ${change.outcome}: poll ${res.headers.get('Location')}`);
} else {
  const inventory = (await res.json()) as Inventory;
  // Etsy issued new product IDs on this write: select variants by their values, never by these IDs.
  for (const product of inventory.products ?? []) {
    const offering = product.offerings[0];
    console.log(`${product.property_values.map((v) => v.values[0]).join('/')} ${product.sku}: ${offering?.price?.amount ?? 'unknown'} x ${offering?.quantity ?? 'unknown'}`);
  }
  console.log(`change ${inventory.change.id} ${inventory.change.status}`);
}
```

```python Python theme={null}
import json
import os
import urllib.request
import uuid

# One key per write: keep it to retry this write, and the retry answers from the first write instead of writing again.
idempotency_key = str(uuid.uuid4())
body = {"offerings": [{"select": {"Primary color": "Black"}, "price": "21.00"}]}
request = urllib.request.Request(
    "https://api.alura.io/v1/workspace/listings/1834720561/inventory",
    data=json.dumps(body).encode(),
    method="PATCH",
    headers={
        "Authorization": f"Bearer {os.environ['ALURA_API_KEY']}",
        "Content-Type": "application/json",
        "Idempotency-Key": idempotency_key,
    },
)
with urllib.request.urlopen(request) as response:
    status = response.status
    location = response.headers.get("Location")
    answer = json.load(response)

if status == 202:
    # Etsy is still answering: the body is the change. Poll it until its outcome is known; don't send the write again.
    print(f"change {answer['id']} {answer['outcome']}: poll {location}")
else:
    # Etsy issued new product IDs on this write: select variants by their values, never by these IDs.
    for product in answer["products"] or []:
        offering = product["offerings"][0] if product["offerings"] else None
        price = offering["price"]["amount"] if offering and offering["price"] else "unknown"
        quantity = offering["quantity"] if offering and offering["quantity"] is not None else "unknown"
        values = "/".join(v["values"][0] for v in product["property_values"])
        print(f"{values} {product['sku']}: {price} x {quantity}")
    print(f"change {answer['change']['id']} {answer['change']['status']}")
```

* **Alura reads the inventory on Etsy right before the write** and applies your patch to that, never to its own copy. An edit you made on Etsy since Alura's last sync is kept.
* **A value that differs between variants needs its property in `*_on_property`.** To give one variant its own price, the property it varies by must be in `price_on_property` (the same for quantity, SKU and processing profile); change that with Replace.
* **A selector must pick exactly one product.** Names and values ignore case and surrounding spaces. Naming one property is enough when its value is unique (`{"Size": "A3"}` on a listing with one A3). A selector that picks no product, more than one, or a product another entry already picked is 400 and names the entry, for example `inventory_selector_no_match, offerings.1`. Nothing is sent.
* `{}` picks the only product of a listing without variations.

## Address variants by their values, not IDs

Etsy can issue new product and offering IDs whenever the inventory is written, by anyone, and does whenever variants are added or removed. Each write's answer carries the current IDs, but don't keep them to address a variant later. Select variants by their property values, which stay the same across writes.

## Retries, concurrency and reverts

* Only one inventory write runs per listing at a time. A second one while the first runs is `409 listing_busy` with `Retry-After`; retry after it.
* **A write can answer 202.** When Etsy is slow to answer, or Alura can't tell yet whether the write landed, the answer is `202` with the change instead of the inventory. Poll the change at `Location` (`GET /v1/workspace/changes/{id}`) until its `outcome` is known, and don't send the write again. A retry with the same `Idempotency-Key` answers from the first write and never writes twice, so send one with every write, as the examples do.
* Revert a change (`POST /v1/workspace/changes/{id}/revert`) restores an inventory change's prices, SKUs and enabled flags. It never restores quantities: the variants keep the quantities Etsy holds now, because restoring an old quantity could sell stock you no longer have. `not_reverted` names the inventory's quantity. A change that added or removed variants is not revertable (`variations_changed`).


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.