# Write a whole product in one request

Create or update a product together with its barcodes, tags, suppliers, variant attributes and components in a single atomic request.

Source: https://getolet.com/resources/developers/write-a-product/
Updated: 2026-10-15

A product on the OLET API is one document. `POST /products` and `PATCH /products/{id}` accept the same nested document that `GET /products/{id}` returns, so you can read a product, change it, and send it straight back. Everything in the request is saved together, or nothing is.

## What is in a product document

Besides its own fields, a product carries five collections:

- `codes`: barcodes, each `{type, code}`. `type` is one of `upc`, `isbn`, `itf`, `ean`, `jan` or `custom`.
- `tags`: tags attached to the product, each `{id}` where `id` is the tag's own id. A read also shows the tag `name`.
- `suppliers`: supplier links, each `{supplier_id, is_primary, supplier_code}`. A read also shows `supplier_name`.
- `variant_attributes`: the attributes of a variant parent, each `{name, position}` with `position` 1 to 3.
- `components`: the products inside a composite product, each `{product_id, quantity}`.

Only `name` and `retail_price` are required to create a product. `retail_price` is an integer number of cents. See [money and quantities](/resources/developers/money-and-quantities/).

## How do I create a product with its collections

Send the collections in the same request.

```bash
curl -X POST https://api.oletapp.com/your-store/v1/products \
  -H "Authorization: Bearer olet_live_YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Cold brew concentrate",
    "retail_price": 1299,
    "codes": [{"type": "ean", "code": "4006381333931"}],
    "tags": [{"id": "TAG_ID"}],
    "suppliers": [{"supplier_id": "SUPPLIER_ID", "is_primary": true}]
  }'
```

You need the `catalog:write` scope. If an entry in any collection is invalid, nothing is created, not even the product.

## How do I change a product

`PATCH /products/{id}` changes only the fields you send. The rule for each collection is simple:

- Leave the key out and that collection is untouched.
- Send an array and it replaces the collection. Items are matched by what they are (a code's type, a tag's id, a supplier's id, a component's product, an attribute's position), so a row you re-send keeps its identity.
- Send `[]` and the collection is cleared.

```js
const url = 'https://api.oletapp.com/your-store/v1/products/PRODUCT_ID'
const headers = {
  Authorization: 'Bearer olet_live_YOUR_API_KEY',
  'Content-Type': 'application/json',
}
const { data: product } = await (await fetch(url, { headers })).json()
product.retail_price = 1399
await fetch(url, { method: 'PATCH', headers, body: JSON.stringify(product) })
```

Fields the API owns, such as `id`, `created_at`, `updated_at`, `image_thumb_url`, `is_gift_card` and `variant_options`, are accepted and ignored, which is why the read document goes straight back. A `sku` in a `PATCH` is ignored too, so a SKU is set only when you create the product. Leave `sku` out of a create and OLET generates one that starts with `PRD-`.

## How do I avoid overwriting someone else's change

Send the `version` you read back with your update. If the product changed in the meantime, you get `409 STALE_VERSION`: reload it and apply your change again. `expected_version` works the same way; if you send both, they must agree.

## What are the limits

A product can have up to 10 codes, 20 tags, 10 suppliers and 50 components, and up to 3 variant attributes. Going over a cap answers `422 UNPROCESSABLE`, except variant attributes, where a fourth attribute or a `position` outside 1 to 3 answers `400 VALIDATION_FAILED`. `variant_attributes` can only change while the product has no live variant children; after that, a change answers `409 CONFLICT`, but sending exactly what is already there is accepted.

A tag id that does not exist is a `400`. A SKU already in use is a `409 CONFLICT`. On a store at its product limit, a create answers `402 PRODUCT_LIMIT_REACHED`. See [errors](/resources/developers/errors/).

## How do I add an image

Send `image_url` with an https address. OLET downloads the image once and stores its own copy; the address you sent is never kept. If the download fails, the product is still saved and the response is `422 IMAGE_FETCH_FAILED` with the saved product in `data`. There is no way to remove an image through the API: `null` and an omitted `image_url` both leave the image as it is. Image downloads have their own limit, see [rate limits](/resources/developers/rate-limits/).

## How do I create variants

Create the parent with `has_variants` set to `true` and its `variant_attributes`, for example Size at position 1 and Color at position 2. Then add the children with `POST /products/{id}/variants`. Each child names its own combination of options, as `attribute_id` and `value` pairs, and may carry its own `name`, `sku`, `retail_price` and `active`.

```bash
curl -X POST https://api.oletapp.com/your-store/v1/products/PARENT_ID/variants \
  -H "Authorization: Bearer olet_live_YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"variants": [
    {"options": [{"attribute_id": "SIZE_ATTRIBUTE_ID", "value": "M"}, {"attribute_id": "COLOR_ATTRIBUTE_ID", "value": "Black"}]},
    {"options": [{"attribute_id": "SIZE_ATTRIBUTE_ID", "value": "L"}, {"attribute_id": "COLOR_ATTRIBUTE_ID", "value": "Black"}]}
  ]}'
```

You can send up to 200 variants, and the request is all or nothing. A combination that already exists answers `409 CONFLICT`. The children are ordinary products from then on: read, change and delete them with `/products/{id}`. The call accepts an `Idempotency-Key`, see [idempotency](/resources/developers/idempotency/).

## Where to go next

See [delete and erase](/resources/developers/delete-and-erase/) for removing products and customers, or the full [API reference](/resources/developers/reference/) for every product field.
