---
title: "Update a catalog item"
method: PATCH
path: "/catalog-items/{id}"
tags: ["Catalog"]
---

# Update a catalog item

`PATCH /catalog-items/{id}`

## Request

- Base URL: `https://joinways.app/api/v1`
- URL: `https://joinways.app/api/v1/catalog-items/{id}`
- Auth: one of:
  - HTTP bearer
  - OAuth 2 (authorization code, authorization URL `https://joinways.app/oauth/authorize`, token URL `https://joinways.app/api/oauth/token`, scopes: `mcp.write`)

Updates the given fields. Setting `unit_price_per_person` without `pricing_mode` sets the mode from it. Changing the `vat` of an item brings the `vat` of the packages that contain it in line.

**Package**:
- `components` **replaces** the whole composition (resend every item
  to keep) and derives the price and the tax rate again, as on
  creation; `pricing_mode` may go with it. Sent with `is_package:
  true` on a plain item, it turns the item into a package (`409`
  `package_component` when a package already contains that item).
- Without `components`, the price fields (`unit_price`,
  `unit_price_per_person`, `pricing_mode`) and the tax rate of a
  package are refused (`400` `package_price_read_only`), like next
  to `components`; the other fields can be changed (`min_price` up
  to its price).
- `is_package: false` turns a package back into a plain item: its
  composition is removed and a price is required (`unit_price` or
  `unit_price_per_person`, `400` `price_required` otherwise). Quote
  lines already made of it keep their articles.

**Role permission:** `quotes.manage` (and a venue the member can see).

## Headers

- `Idempotency-Key` string

## Request body

- CatalogItemUpdate
  - `name` string
  - `description` string
  - `category` string
  - `type` string: e.g. Catering, Rental.
  - `unit_label` string: e.g. per person, per hour.
  - `unit_price` number: Excluding tax. Required to create a plain item. Never sent for a package (derived from `components`).
  - `unit_price_per_person` number: Never sent for a package.
  - `cost_price` number: Internal cost.
  - `min_price` number: On a package, at most its price (the sum of its components).
  - `vat` number: Tax rate in %. Omitted on creation, the standard rate of the venue's country. Never sent for a package (the lowest rate of its items).
  - `vat_rate` number: Alias of `vat` (both must agree).
  - `pricing_mode` 'flat' | 'per_person' | 'tiered': Omitted on creation, `per_person` when `unit_price_per_person` > 0, else `flat`.
  - `tags` string[]
  - `is_package` boolean: `true` with `components`: the item is a package. `false` on a package (update): back to a plain item, a price is then required.
  - `components` CatalogItemComponentInput[]: Composition of a package; on update it replaces the former one (order kept). Needs `is_package: true`, except on an item that is already a package.
    - `item_id` string, uuid, required: A plain catalog item of the same venue, once per package: never a package, never the package itself.
    - `quantity` number, required: Count of the item in one package unit.
    - `unit_price` number, required: Price of ONE item inside the package, excluding tax.

## Response `200`

The updated item.

- CatalogItem: A priceable item (catering, service, equipment…) of a venue's catalog. The list returns the fields up to `updated_at` (and `components` on a package); create and update return every field.
  - `id` string, uuid
  - `name` string
  - `category` string, nullable
  - `type` string, nullable
  - `unit_label` string, nullable
  - `pricing_mode` 'flat' | 'per_person' | 'tiered': Which price applies: `flat` = `unit_price` per unit, `per_person` = `unit_price_per_person` × guests, `tiered` = the item's price rules.
  - `unit_price` number, nullable: Price per unit, excluding tax.
  - `unit_price_per_person` number, nullable
  - `cost_price` number, nullable: Internal cost.
  - `min_price` number, nullable: Minimum price a quote line may use.
  - `vat` number, nullable: Tax rate in %.
  - `vat_rate` number, nullable: Same value as `vat`.
  - `taxable` boolean, nullable
  - `is_package` boolean: The item is a package, made of the items in `components`. Its price (`unit_price`, or `unit_price_per_person` when `pricing_mode` is `per_person`) is the sum of `quantity × unit_price` of its components, and its `vat` only the lowest of their rates.
  - `components` CatalogItemComponent[]: Only on a package, in the order of its composition (written with `components` on create / update).
    - `item_id` string, uuid, required: The catalog item (an item of the same venue, never a package).
    - `name` string, nullable
    - `quantity` number, required: Quantity of the item in one package unit.
    - `unit_price` number, required: Unit price of the item inside the package, excluding tax.
    - `vat` number, nullable: Tax rate of the item, in %.
  - `created_at` string, date-time, nullable
  - `updated_at` string, date-time, nullable
  - `description` string, nullable: Create / update only.
  - `tags` string[], nullable: Create / update only.
  - `location_id` string, uuid, nullable: Create / update only.
  - `space_id` string, uuid, nullable: Create / update only.
  - `resolved_unit_price` number: Only when `pax`, `date` or `event_id` is passed. Price for that context, price rules applied.
  - `resolved_min_price` number, nullable: With `resolved_unit_price`. Floor of the applied rule, else of the catalog.
  - `applied_price_rule` object, nullable: With `resolved_unit_price`. The rule applied, or `null` when the catalog price applies.
    - `id` string, uuid
    - `label` string, nullable

## Other responses

- `400`: Validation failed (`invalid_request`). For a body that fails the schema, `details` holds `formErrors` and `fieldErrors`.
- `401`: Missing, invalid, expired or revoked bearer token (`unauthorized`).
- `403`: `forbidden`: the token lacks the scope, its owner's role lacks the permission named by the operation, or the workspace has neither the Pro plan nor a trial (`details.reason` `plan_not_eligible`).
- `404`: Not found in this workspace, or the id of the path is not a UUID (`not_found`).
- `409`: The state of the record refuses the operation (`conflict`). `details.reason` is a stable code.
- `429`: Too many requests (`rate_limited`). `details.reason`: `per-ip`, `per-account-minute`, `per-account-day`, or `email_rate_limited` (email budget). Always carries `Retry-After`: wait that many seconds.

---

[API](https://skmtc.dev/joinways/apis/joinways-public-api.md) · [All operations](https://skmtc.dev/joinways/apis/joinways-public-api/llms.txt) · [OpenAPI document](https://skmtc.dev/joinways/apis/joinways-public-api/revisions/39e84309953e?raw)
