---
title: "Create a catalog item"
method: POST
path: "/catalog-items"
tags: ["Catalog"]
---

# Create a catalog item

`POST /catalog-items`

## Request

- Base URL: `https://joinways.app/api/v1`
- URL: `https://joinways.app/api/v1/catalog-items`
- 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`)

Adds an item to a venue's catalog. `vat` (or its alias `vat_rate`) omitted takes the standard rate of the venue's country (`400` when the venue has none). `pricing_mode` omitted: `per_person` when `unit_price_per_person` > 0, else `flat`. A plain item needs `unit_price`.

**Package**: send `is_package: true` and `components`, items of the same venue (plain items, each once). Do not send `unit_price`, `unit_price_per_person` or `vat`: the price is the sum of `quantity × unit_price` of the components, written to `unit_price` (or to `unit_price_per_person` when `pricing_mode` is `per_person`), and `vat` the lowest rate of its items. `min_price` must not exceed that price. A component without a tax rate takes the standard rate of the venue's country. The response is the package with its derived price and its `components`. **Role permission:** `quotes.manage` (and a venue the member can see).

## Headers

- `Idempotency-Key` string

## Request body

- CatalogItemCreate: `unit_price` is required for a plain item; a package sends `is_package: true` and `components` instead.
  - `name` string, required
  - `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.
  - `venue_id` string, uuid, required: A venue the member can see.

## Response `201`

The created 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`).
- `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)
