# Prices

> What a customer pays for a SKU at a quantity, and exactly which rule set that price.

TradeCatalog works out every customer price in one place, the same way for the app, the trade portal, MCP and the API. You never calculate prices yourself.

Configured products, measured rates, quantity thresholds, net extras and explicit rounding also use this pricing service. Quote a complete line with `POST /products/{id}/quote`; a missing price may mean `configurationRequired`, while a valid unpriced specification is Price on request. Use the returned line total. See [trade catalog setup](/docs/guides/trade-catalogues) for precedence, net treatment and publication.

## How prices are worked out

The most specific rule wins, and **adjustments never stack**:

1. **Agreed price.** If the customer has an agreed (fixed) price for the product, that’s the price.
2. Otherwise the **list price with one percentage**, the first that applies. A percentage is a discount (taken off) or a markup (added on):
   1. the customer’s percentage for that **product**
   2. the customer’s percentage for the product’s **category** (the nearest parent wins)
   3. the customer’s percentage for the product’s **brand**
   4. the customer’s own percentage on the **whole list**
3. Otherwise the **list price**.

A category discount of 10% and a whole-list markup of 5% give 10% off, not both. The unit is never below zero, and it is rounded half up to a whole minor unit. The line total is `unit × qty`.

List prices and agreed prices are versioned by date. The API always gives the price valid **now**. Future prices from a scheduled price change apply from their date.

## Price lists and catalog access

Standard is the supplier's master catalog. Other price lists can contain products with prices and products awaiting a quote. A missing price on an assigned list does not fall back to Standard.

In the workspace, open **Price lists** and choose a named list. **Assigned customers can see** has two choices:

- **All permitted catalog products** is the default. The list sets prices; assigned buyers can also browse other permitted products as Price on request.
- **Only products on this list** limits every assigned buyer's catalog, searches, product links, downloads and new orders to that list's products. Other product visibility and approved-list restrictions still apply. Customers on other lists keep their existing access.

Each list shows its customer count, priced products, unpriced products and total products. Totals include inactive products, so they can exceed what a buyer currently sees. A zero price counts as priced. A future price counts as unpriced until its start date.

To add products, select them on **Products**, choose **Add to price list**, then copy Standard prices with an optional percentage or select **Add without prices**. Existing members keep their prices. Products without a Standard price are added as Price on request. On a named list's page, saving a SKU with a blank price adds it without a price or clears its current price. **Remove** takes it off that list while keeping the master product and past orders. Resolve any pending price change for that product first.

These controls require Pro and global price-edit rights. Existing lists keep their catalog access until a supplier changes it. API imports into a named list include ordinary product rows with no price as unpriced members; an empty cell in a prices-only import leaves access unchanged.

## The price object

Every product’s `price` has this shape. `GET /prices` returns the same fields at the top level of `data`.

```json
{
  "unit": 1573,
  "line": 15730,
  "list": 1850,
  "discountBp": 1500,
  "adjustment": "discount",
  "source": "brand",
  "label": "15% off your list",
  "remark": null
}
```

| Field        | Type                   | Description                                                                                                                      |
| ------------ | ---------------------- | -------------------------------------------------------------------------------------------------------------------------------- |
| `unit`       | integer or null        | Price for one unit, in minor units. `null` if the product has no price.                                                          |
| `line`       | integer or null        | `unit × qty`.                                                                                                                    |
| `list`       | integer or null        | The list price now.                                                                                                              |
| `discountBp` | integer                | The adjustment applied, in basis points. `0` for agreed and list prices.                                                         |
| `adjustment` | `discount` or `markup` | Which way `discountBp` moves the list price. `discount` when there is no percentage.                                             |
| `source`     | string                 | Which rule set the price: see below.                                                                                             |
| `label`      | string                 | Wording buyers see next to the price.                                                                                            |
| `remark`     | string or null         | The agreed-price note when that customer is allowed to see it. Otherwise `null`. The staff-only internal note is never included. |

| `source`   | Meaning                      | Example `label`     |
| ---------- | ---------------------------- | ------------------- |
| `fixed`    | The customer’s agreed price  | Agreed price        |
| `product`  | Percentage on one product    | 10% off your list   |
| `category` | Percentage on a category     | 12.5% off your list |
| `brand`    | Percentage on a brand        | 15% above your list |
| `account`  | Percentage on the whole list | 10% above your list |
| `list`     | List price, no adjustment    | (empty)             |
| `unpriced` | The product has no price yet | (empty)             |

## Check a customer price

**GET** `/api/v1/prices`

What a customer pays for one SKU at a quantity, and why. Use it to answer “what does Oakfield pay for 10 boxes of HX-8510?”.

| Name       | Type    | Required | Description                  |
| ---------- | ------- | -------- | ---------------------------- |
| `customer` | string  | yes      | Customer id or account code. |
| `sku`      | string  | yes      | The SKU, ignoring case.      |
| `qty`      | integer | no       | 1 to 1,000,000. Default 1.   |

```bash
curl "https://tradecatalog.app/api/v1/prices?customer=OAK01&sku=HX-8510&qty=10" \
  -H "Authorization: Bearer $TRADECATALOG_API_KEY"
```

```js
const params = new URLSearchParams({
  customer: 'OAK01',
  sku: 'HX-8510',
  qty: '10',
})
const res = await fetch(`https://tradecatalog.app/api/v1/prices?${params}`, {
  headers: { Authorization: `Bearer ${process.env.TRADECATALOG_API_KEY}` },
})
const { data: price, currency } = await res.json()
```

```json
{
  "data": {
    "customer": {
      "id": "7d2e4f0a-1b3c-4d5e-8f90-a1b2c3d4e5f6",
      "accountCode": "OAK01",
      "name": "Oakfield Joinery"
    },
    "sku": "HX-8510",
    "name": "Hex bolt M10 x 50 zinc",
    "qty": 10,
    "unit": 1573,
    "line": 15730,
    "list": 1850,
    "discountBp": 1500,
    "source": "brand",
    "label": "15% off your list",
    "remark": null
  },
  "currency": "GBP"
}
```

| Status | Code               | When                                                |
| ------ | ------------------ | --------------------------------------------------- |
| 400    | `validation_error` | `customer` or `sku` missing, or `qty` out of range. |
| 404    | `not_found`        | No such customer, or no product with that SKU.      |

This endpoint checks the price only. It doesn’t check whether the customer can see the product. For that, use [`GET /products/{id}?customer=…`](/docs/api/products#get-one-product).

## Prices for many products

To price a whole page of products for one customer, use `GET /products?customer=OAK01`. Every row’s `price` is that customer’s price, and customer-only products for other customers are left out. See [Products](/docs/api/products#list-or-search-products).

## Changing prices

- **List prices** change through price changes, so customers get notice. Send new prices with [`PUT /products`](/docs/api/products#create-or-update-products); they go into a draft that you publish in the app.
- **Markups, discounts and agreed prices** are set per customer in the app. Read them with [`GET /customers/{id}`](/docs/api/customers#get-one-customer).
- **Agreed prices** for many customers are set with [`PUT /agreed-prices`](/docs/api/prices#set-agreed-prices).
- A customer’s **whole-list percentage** can be set with [`PUT /customers`](/docs/api/customers#create-or-update-customers) (`discountBp`, and optional `adjustment`).

## Set agreed prices

**PUT** `/api/v1/agreed-prices`

Create or replace agreed prices for existing customers and products. Send up to 2,000 rows. Prices are integer minor units. Leave `commit` false to preview: nothing is saved, and each unknown account or SKU comes back as a row error. Set `commit` to true to save. Saving closes the agreed price valid now and inserts the new one from this moment, in batches of 100. Pro only.

| Name          | Type    | Required | Description                                  |
| ------------- | ------- | -------- | -------------------------------------------- |
| `commit`      | boolean | no       | `true` saves. Default `false` previews only. |
| `prices`      | array   | yes      | 1 to 2,000 prices.                           |
| `accountCode` | string  | yes      | Customer account code, ignoring case.        |
| `sku`         | string  | yes      | Product SKU, ignoring case.                  |
| `price`       | integer | yes      | Agreed price in minor units.                 |
| `note`        | string  | no       | Up to 200 characters, shown to staff.        |

```bash
curl -X PUT https://tradecatalog.app/api/v1/agreed-prices \
  -H "Authorization: Bearer $TRADECATALOG_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"commit":false,"prices":[{"accountCode":"OAK01","sku":"HX-8510","price":1250,"note":"Contract"}]}'
```

| Status | Code               | When                                                  |
| ------ | ------------------ | ----------------------------------------------------- |
| 400    | `validation_error` | A price is missing, negative, or over the limit.      |
| 403    | `read_only`        | The workspace is not on Pro, or the key is read-only. |
