---
title: "Add quote items"
method: POST
path: "/quotes/{id}/items"
tags: ["Quotes"]
---

# Add quote items

`POST /quotes/{id}/items`

## Request

- Base URL: `https://joinways.app/api/v1`
- URL: `https://joinways.app/api/v1/quotes/{id}/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 line items. Totals are computed by the server (tax engine, catalog minimum price); `vat` omitted takes the standard rate of the venue's country. Catalog lines tied to a room (or a package day plan) book that room on the event (`rooms`).

**Package**: send ONE line whose `location_item_id` is a catalog item with `is_package: true`, `quantity` the number of package units and `unit_price` the price of one unit (its catalog price; a different price is split between the articles in the catalog's proportions, `0` adds a plain line). Joinways creates the articles, each at its own tax rate (`vat` is ignored): never add them yourself. The line comes back as the package, `items_added` counts it once. A package takes `discount_percent` or `is_offered`, never `discount_amount` nor `is_discount`. **Role permission:** `quotes.manage`.

## Headers

- `Idempotency-Key` string

## Request body

- object
  - `items` QuoteItemCreate[], required
    - `title` string, required
    - `description` string
    - `quantity` number
    - `unit_price` number, required: Price per unit, excluding tax. For a catalog package: the price of one package unit.
    - `vat` number: Tax rate in %. Omitted, the standard rate of the venue's country (e.g. 0 in the US, 20 in France). Ignored for a package: each article keeps its own rate.
    - `location_item_id` string, uuid, nullable: Catalog item to price this line from; an item of the workspace (`400` otherwise). The `unit_price` is checked against the catalog minimum price. An item with `is_package: true` adds the whole package (see Packages).
    - `is_option` boolean
    - `is_discount` boolean: Discount line. Refused on a package.
    - `is_offered` boolean
    - `discount_amount` number: Refused on a package (`package_discount_amount`), use `discount_percent`.
    - `discount_percent` number, nullable: Exclusive with `discount_amount` (set the other to 0).
    - `event_day` integer: 1-based day for multi-day events.
    - `event_day_date` string, date, nullable: Stable day anchor (`YYYY-MM-DD`); preferred over `event_day`.
    - `sort_order` integer

## Response `201`

Items added.

- object
  - `success` boolean
  - `items_added` integer
  - `items` QuoteItem[]
    - `id` string, uuid
    - `quote_id` string, uuid
    - `account_id` string, uuid, nullable
    - `title` string
    - `description` string
    - `quantity` number
    - `unit_price` number: Price per unit, excluding tax. On a package: the price of one package unit (`package.unit_price`).
    - `vat` number, nullable: Tax rate as a percentage (e.g. 20 for 20 %). A package mixes rates: this is only the lowest of them, read `package.tax_rates` and never compute its tax from `vat`.
    - `total` number, nullable: Computed line total, excluding tax, after the line discount. On a package: the sum of its articles (`package.total_excl_tax`).
    - `is_package` boolean: The line is a package; `package` is then present.
    - `parent_item_id` string, uuid, nullable: The package line an article belongs to. `null` on the items returned: the articles of a package are under its `package.articles`, not items of their own.
    - `package` QuoteItemPackage: Only on a package line. Amounts are summed article by article, like the quote totals, so they can differ by a cent from `unit_price × quantity`.
      - `unit_price` number, required: Price of one package unit, excluding tax, before discount.
      - `total_excl_tax` number, required: Sum of the articles, after the package's discount.
      - `total_tax` number, required
      - `total_incl_tax` number, required
      - `tax_rates` number[], required: Distinct tax rates of the articles, ascending, in %.
      - `articles` QuoteItemPackageArticle[], required: What the package is made of, in the order of its composition. Read-only: written by Joinways from the package line. Their ids are refused by every write (`package_article`).
        - `id` string, uuid
        - `location_item_id` string, uuid, nullable: Catalog item of the article.
        - `title` string
        - `description` string, nullable
        - `quantity` number: Package quantity × `package_unit_quantity`.
        - `package_unit_quantity` number: Quantity of the article in one package unit.
        - `unit_price` number: Unit price of the article inside the package, excluding tax.
        - `vat` number: Tax rate of the article, in %.
        - `total` number: Amount of the article, excluding tax, after the package's discount.
    - `location_item_id` string, uuid, nullable: The catalog item this line was priced from, if any.
    - `min_price_applied` number, nullable: Catalog minimum price the line was checked against.
    - `is_option` boolean, nullable
    - `is_discount` boolean, nullable
    - `is_offered` boolean, nullable: Offered (free) line; bypasses the catalog minimum-price check.
    - `discount_amount` number, nullable
    - `discount_percent` number, nullable
    - `event_day` integer: 1-based day this line belongs to (multi-day events).
    - `event_day_date` string, date, nullable
    - `sort_order` integer: Display order within the day.
    - `client_selected_at` string, date-time, nullable: When the client ticked this option on the public quote.
    - `inherited_from_item_id` string, uuid, nullable
    - `ai_rationale` string, nullable
    - `created_at` string, date-time
  - `rooms` object: Rooms booked by catalog lines.
    - `booked` string[]
    - `skipped` string[]
    - `already_booked` string[]
    - `reason` string, nullable: `whole_venue` when the event holds the whole venue.

## 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 quote's state refuses the operation (`conflict`); retrying the same call fails the same way until that state changes (except `amendment_in_flight`, transient). `error.details.reason` (see `QuoteErrorReason`): `quote_locked` (signed, invoiced or paid: amend or cancel instead), `quote_signed`, `quote_cancelled`, `quote_not_cancellable`, `quote_has_active_invoice` (credit the invoice first), `quote_has_amendments` (a quote with a live amendment is frozen), `quote_superseded` (a replaced version is history), `amendment_in_flight` (one amendment at a time), `amendment_nested`, `parent_not_active`, `parent_not_signed`, `parent_fully_invoiced`, `quote_changed` (the status changed meanwhile), `invalid_status`, `not_a_platform_version`, `event_closed` (event lost or cancelled), `event_archived`, `platform_quote_read_only` (a platform quote, `source: platform`, is the platform's document: amounts, discounts, currency and lines are read-only, its status only takes `accepted` or `rejected`), `platform_quote_delete_forbidden` (cancel or refuse it instead), `platform_version_signed` (another version is signed), `quote_conflict` (another live contract must be cancelled or refused first).
- `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)
