# Add line items

> Add the five kinds of line to a quote or sales order (catalog products, your own products and variants, custom line items and fees), link them to jobs, and change them in bulk.

Web page: https://developer.yoprint.com/guides/line-items/

**Read first:** [Products and pricing](https://developer.yoprint.com/how-yoprint-works/products-and-pricing.md), for what a line can sell and how its price is worked out.

A quote or sales order is made of line items. Each line is one product, item or fee, with a price and a quantity, usually inside a job.

`POST /v2/api/store/{team_slug}/v2/sales_order/{order_unique_key}/order_item` [Add a line item to a sales order or quote](https://developer.yoprint.com/reference/sales-order-line-items/v2-order-item-store.md)

## Before you add lines: a job

Lines that are produced together, such as the sizes of one shirt printed with one design, belong in one job. Create the job on the order first:

`POST /v2/api/store/{team_slug}/v2/sales_order/{hash_id}/job` [Add a job to a sales order or quote](https://developer.yoprint.com/reference/jobs/v2-job-store.md)

```bash
curl https://secure.yoprint.com/v2/api/store/your-team/v2/sales_order/V4keXwwxMQyXw7/job \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Accept: application/json" \
  -H "X-Yoprint-Decimal-Format: exact-v1" \
  -H "Content-Type: application/json" \
  -d '{ "name": "Front logo tees" }'
```

The answer is the job, with its id in `data.id`. Use it in each line's `job`. See [Schedule and track production](https://developer.yoprint.com/guides/jobs-and-production.md).

## What are you adding?

Pick the kind of line first. It decides `product.type`, which decides `pricing.type`.

**Diagram:** Not sure the shop already has the product? Look it up first: see Look up products and prices.

1. **A blank from a connected supplier, such as SanMar**: Catalog product → otherwise
2. **One of the shop's own products, in Products**: Your simple product, or one variant of your variant product → otherwise
3. **A charge on its own, such as setup or rush**: Fee → otherwise
4. **Anything else**: Custom line item: a name and a price you write

| You're adding                                     | Kind                 | `product.type` | `pricing.type`            | You name it by                              |
| ------------------------------------------------- | -------------------- | -------------- | ------------------------- | ------------------------------------------- |
| A blank from a supplier's catalog                 | Catalog product      | `db_catalog`   | `catalog_product_pricing` | The catalog product id, such as `smr-10001` |
| One of your own products, with no sizes or colors | Your simple product  | `user_simple`  | `product_cost_service`    | The product's `id`                          |
| A size or color of one of your own products       | Your variant product | `db_variant`   | `product_cost_service`    | The variant's `id`                          |
| A one-off item that isn't in any catalog          | Custom line item     | `custom`       | `custom_product`          | A name you write                            |
| A charge, such as a setup, rush or art fee        | Fee                  | `fee`          | `fee`                     | A name you write                            |

Send the `pricing.type` that matches the `product.type`. YoPrint refuses a mismatch with `422`. In the web app, the Add Product dialog's type filter calls your products "Custom Simple Product" and "Custom Variant Product".

## The shape every line shares

```json
{
  "product": { "type": "...", "properties": { } },
  "pricing": { "type": "...", "...": "..." },
  "quantity": { "type": "unit", "total": { "value": 24, "dp": 0 } },
  "job": { "type": "linked", "id": "HuhbGhG57Z7Qyi" },
  "position": 100000
}
```

| Field      | Rule                                                                                                                                                                                                                      |
| ---------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `product`  | What the line is. `type` from the table above, and `properties` for that kind.                                                                                                                                            |
| `pricing`  | How it's priced. `type` from the table above, and the fields for that kind.                                                                                                                                               |
| `quantity` | Always `"type": "unit"`, with `total` as a [decimal pair](https://developer.yoprint.com/concepts/money-and-quantities.md).                                                                                                |
| `job`      | `{ "type": "linked", "id": "<job id>" }` to put the line in a job, or `{ "type": "standalone" }` for a line outside any job.                                                                                              |
| `position` | An integer: lines are shown by ascending `position`. The web app spaces lines 100000 apart, which leaves room to insert between them. A value outside the allowed range is replaced by the next free position at the end. |

For line items, every amount and quantity is a decimal pair (`value` ÷ 10^`dp`: `{ "value": 1250, "dp": 2 }` is 12.50), and every price is for one piece. Sending a plain number such as `12.5` is refused with `422`. Note that `dp` can vary across fields (line prices are often `dp` 6, quantities `dp` 0, order totals `dp` 4), and other areas of the API (such as payments) use thousandths (`300000` is 300.00). See [Money and quantities](https://developer.yoprint.com/concepts/money-and-quantities.md).

### What comes back

Adding a line answers with the line wrapped in `item`, and the order's new lock versions. Shortened:

```json
{
  "status": 200,
  "success": true,
  "data": {
    "action_performed": "CREATED",
    "has_changes": true,
    "object_type": "order_item",
    "item": {
      "id": "uEm77FEbwZEMvu",
      "order_id": "V4keXwwxMQyXw7",
      "job": { "type": "linked", "id": "HuhbGhG57Z7Qyi" },
      "position": 100000,
      "unit_price": { "value": 12500000, "dp": 6 },
      "subtotal": { "value": 3000000, "dp": 4 }
    }
  }
}
```

The line's id is `data.item.id`, not `data.id`. Keep it: invoice and shipment lines point at the order line by it, and you change or delete the line with it.

### How a line's price is decided

YoPrint checks these in order and uses the first that applies:

**Diagram:** YoPrint uses the first rule that applies.

1. **A fee line**: Charges exactly the amount in `pricing.product.total` → otherwise
2. **"override": true**: Charges exactly the unit price in `pricing.product.total`, as the web app does → otherwise
3. **"customer_supplied": true**: The product price is 0 → otherwise
4. **Calculated price**: The cost (`pricing.catalog`, `pricing.product_cost.total`, or the product's price in Products), marked up by the order's pricing group or a matching pricing rule

The final unit price adds any decoration price from the linked job (or `pricing.service.total` when `service.override` is true). The line subtotal is the unit price multiplied by quantity.

**Why send both price and cost?** Price (`pricing.product`) is what you charge the customer. Cost (`pricing.product_cost` or `pricing.catalog`) is what you pay the supplier or vendor. Even when you override the price, sending the cost ensures YoPrint can accurately calculate order profit and margin in reports.

## A fee

The simplest line: a name and an amount.

```json
{
  "product": { "type": "fee", "properties": { "name": "Screen setup" } },
  "pricing": { "type": "fee", "product": { "total": { "value": 2500, "dp": 2 } } },
  "quantity": { "type": "unit", "total": { "value": 1, "dp": 0 } },
  "job": { "type": "standalone" },
  "position": 500000
}
```

- `product.properties.name` is the fee's name. `description` is optional.
- `pricing.product.total` is the fee's amount, here 25.00.

Your team's saved fees are fee presets, with their names and amounts:

`GET /v1/api/store/{team_slug}/setting/fee_preset` [List fee presets](https://developer.yoprint.com/reference/fee-presets/v1-fee-preset-index.md)

## A custom line item

An item with a name and a price you set:

```json
{
  "product": {
    "type": "custom",
    "properties": { "name": "Screen printed tee", "customer_supplied": false }
  },
  "pricing": {
    "type": "custom_product",
    "product": { "override": true, "total": { "value": 1250, "dp": 2 } },
    "product_cost": { "total": { "value": 400, "dp": 2 } }
  },
  "quantity": { "type": "unit", "total": { "value": 24, "dp": 0 } },
  "job": { "type": "linked", "id": "HuhbGhG57Z7Qyi" },
  "position": 100000
}
```

- `product.properties.name` and `customer_supplied` are required.
- `pricing.product.total` with `override: true` is the price for each piece, here 12.50.
- `pricing.product_cost.total` is optional: your cost for each piece, used for profit and margin.

## Your simple product

One of your own products. Find its `id` with:

`GET /v1/api/store/{team_slug}/product` [List products](https://developer.yoprint.com/reference/products/v1-products-index.md)

```json
{
  "product": {
    "type": "user_simple",
    "properties": { "id": "5MnM49NNFDgB9A", "customer_supplied": false }
  },
  "pricing": {
    "type": "product_cost_service",
    "product": { "override": true, "total": { "value": 1500, "dp": 2 } }
  },
  "quantity": { "type": "unit", "total": { "value": 24, "dp": 0 } },
  "job": { "type": "linked", "id": "HuhbGhG57Z7Qyi" },
  "position": 200000
}
```

- This sets the selling price to 15.00 a piece.
- To let YoPrint price it instead, send `"product": { "override": false }`. YoPrint then starts from the product's price in Products, or from `pricing.product_cost.total` when you send one, and applies the order's pricing group or a matching pricing rule.
- `pricing.product_cost` is optional: `{ "override": true, "total": { "value": 600, "dp": 2 } }` sets your cost, and a total of `0` means "use the product's price".
- A product that has been phased out or deleted can't be added to a new line.

## Your variant product

A size or color of one of your own products. List the product's variants to find the variant's `id`:

`GET /v1/api/store/{team_slug}/product/{hash_id}/variant` [List a product's variants](https://developer.yoprint.com/reference/product-variants/v1-product-variant-index.md)

The body is the same as for your simple product, with `"type": "db_variant"` and the variant's id in `product.properties.id`. The size and color come from the variant. A phased-out or deleted variant can't be added.

## A catalog product

A blank from a supplier catalog. Each catalog product id represents one size and color of a style and starts with the supplier's code and a hyphen, such as `smr-10001`. For example, "24 tees in three sizes" requires three separate lines in the same job.

> **Note:** The public API cannot search supplier catalogs by keyword. You must start from a catalog product id (`smr-10001`) or style id you already have, such as from an existing catalog line or past order.

Adding a catalog line requires **three calls** in order:

1. **Get the style's sizes and colors** (`hydrate_imported_product`) to retrieve all sizes, colors, and variant IDs (`attr_data`).
2. **Get the supplier's cost** (`real_time_data_fetcher_pricing`) to look up live supplier pricing for those IDs. Catalog lines require an upfront supplier cost, but supplier prices change frequently and are not stored in the style definition.
3. **Add the line item** (`order_item` store) with that supplier cost in `pricing.catalog`.

1) **Get the style's sizes and colors.** Send any catalog product id of the style (`db_catalog`), or the style's own id (`db_catalog_style`):

   `POST /v1/api/store/{team_slug}/hydrate_product` [Load a product for a sales order line](https://developer.yoprint.com/reference/products/v1-hydrate-imported-product.md)

   ```json
   { "type": "db_catalog", "id": "smr-10001" }
   ```

   The answer is the whole style. Each row of `attr_data` is one size and color, with its `id`, `size_label`, `color_label`, `provider` and `dscnt` (true when the supplier has discontinued it). The style's own id is in `style_id`.

2) **Get the supplier's cost** for the ids you chose, up to 100 at once:

   `POST /v1/api/store/{team_slug}/real_time_data_fetcher/pricing` [Get live supplier pricing for catalog products](https://developer.yoprint.com/reference/supplier-catalog/v1-real-time-data-fetcher-pricing.md)

   ```json
   { "product": [{ "type": "db_catalog", "id": "smr-10002" }] }
   ```

   The answer has `real_time_pricing`, fetched live from the supplier with your team's connection, and `local_pricing`, YoPrint's stored copy of the price list. Pick the price level you sell from.

3) **Add the line**, with that cost in `pricing.catalog`:

   ```json
   {
     "product": {
       "type": "db_catalog",
       "properties": { "id": "smr-10002", "customer_supplied": false }
     },
     "pricing": {
       "type": "catalog_product_pricing",
       "catalog": {
         "provider": "smr",
         "selected": "piece",
         "date": "2026-11-03",
         "total": { "value": 450, "dp": 2 }
       },
       "product": { "override": false }
     },
     "quantity": { "type": "unit", "total": { "value": 12, "dp": 0 } },
     "job": { "type": "linked", "id": "HuhbGhG57Z7Qyi" },
     "position": 300000
   }
   ```

- `pricing.catalog` is the supplier cost the line starts from. A new catalog line must have it. Without it, a single add answers `422` with "Enter the catalog cost for this line." on `pricing.catalog`, and a bulk create answers `422` `bulk_validation_payload_failed`. All four of its fields are required:
  - `provider`: the supplier code, from the row's `provider` in step 1. It's the part of the id before the hyphen: `smr` for `smr-10002`.
  - `selected`: the price level, one of `piece`, `dozen`, `case`, `account`, `sales`, `custom` or `legacy`.
  - `date`: the date of the price.
  - `total`: the cost for one piece.
- With `"product": { "override": false }`, YoPrint marks the cost up with the order's pricing group or a matching pricing rule. To charge your own price instead, send `"product": { "override": true, "total": { "value": 900, "dp": 2 } }`.
- When the customer supplies the garment, send `"customer_supplied": true`. Then `pricing.catalog` and `pricing.product` are both optional.

## Read the lines back

The answer to each add is the new line. To see all of an order's lines, and the new totals:

`GET /v2/api/store/{team_slug}/v2/sales_order/{order_unique_key}/order_item` [List the line items of a sales order or quote](https://developer.yoprint.com/reference/sales-order-line-items/v2-order-item-index.md)

`GET /v2/api/store/{team_slug}/v2/sales_order/{order_unique_key}` [Get a sales order or quote](https://developer.yoprint.com/reference/sales-orders/v2-order-show.md)

## Change or remove a line

`PUT /v2/api/store/{team_slug}/v2/sales_order/order_item/{order_item_unique_key}` [Update a line item](https://developer.yoprint.com/reference/sales-order-line-items/v2-order-item-update.md)

Send only what changes. A line's kind can't change: to turn a custom line item into a catalog product, delete it and add a new line.

`DELETE /v2/api/store/{team_slug}/v2/sales_order/order_item/{order_item_unique_key}` [Delete a line item](https://developer.yoprint.com/reference/sales-order-line-items/v2-order-item-destroy.md)

## Many lines at once

The bulk operation takes a JSON array of actions on one order. Each action is one of three shapes:

| Field          | `create`                                                  | `update`                                                       | `delete`                  |
| -------------- | --------------------------------------------------------- | -------------------------------------------------------------- | ------------------------- |
| `bid`          | Required: your own label for the action, such as `line-1` | Required                                                       | Required                  |
| `action`       | `"create"`                                                | `"update"`                                                     | `"delete"`                |
| `payload`      | Required: the same body as a single add                   | Required: the same body as a single update (only what changes) | Not sent                  |
| `cart_item_id` | Not sent                                                  | Required: the line's `id`                                      | Required: the line's `id` |

`POST /v2/api/store/{team_slug}/v2/sales_order/{order_unique_key}/order_item/bulk` [Add, change and delete line items in one save](https://developer.yoprint.com/reference/sales-order-line-items/v2-order-item-bulk.md)

```json
[
  {
    "bid": "line-1",
    "action": "create",
    "payload": {
      "product": { "type": "fee", "properties": { "name": "Rush fee" } },
      "pricing": { "type": "fee", "product": { "total": { "value": 5000, "dp": 2 } } },
      "quantity": { "type": "unit", "total": { "value": 1, "dp": 0 } },
      "job": { "type": "standalone" },
      "position": 600000
    }
  },
  { "bid": "line-2", "action": "delete", "cart_item_id": "uEm77FEbwZEMvu" }
]
```

## Pitfalls

- **`quantity.type` is always `unit`.** Sizes come from the product or variant, not from the quantity.
- **Send `override: true` with your price.** Without it, a pricing rule or the pricing group can change the price, silently.
- **`pricing.type` must match `product.type`.** Use the table at the top.
- **Catalog lines need their catalog cost.** Send `pricing.catalog` unless the customer supplies the product.
- **Amounts are pairs.** Plain numbers like `12.5` are refused with `422`. Send `{ "value": 1250, "dp": 2 }`. Remember that other areas, such as payments, use thousandths. See [Money and quantities](https://developer.yoprint.com/concepts/money-and-quantities.md).
- **Each size is its own line.** For a catalog shirt in four sizes, add four lines, one per catalog product id from `attr_data`, in the same job.
- **The kind is fixed.** An update can't change `product.type`.
- **Phased-out products are refused on new lines.** Existing lines keep working, but a draft holding one can't be published until the line is changed.
- **Totals change on every add.** Read the order back for the new totals rather than adding them up yourself.
