# Price changes

> List price changes and preview one, including what a customer will pay before and after.

List prices never change silently in TradeCatalog. New list prices go into a **price change**: a draft that staff review, date and publish, optionally emailing every customer their own new prices.

## How price changes are made

- **From the API:** [`PUT /products`](/docs/api/products#create-or-update-products) with a new `listPrice` for an existing product adds it to a draft and returns the draft’s `priceChangeId`.
- **From the app:** spreadsheet imports, and bulk changes like “+4% on the Bolts category”.

Publishing, scheduling and cancelling happen in the app, under **Price changes**. There’s no API for them: a person always checks a price change before customers see it.

## Statuses

| Status      | Meaning                                                   |
| ----------- | --------------------------------------------------------- |
| `draft`     | Not published. No prices have changed.                    |
| `scheduled` | Published with a future date. Prices change on that date. |
| `live`      | In effect.                                                |
| `cancelled` | Cancelled before it took effect.                          |

## The price change object

```json
{
  "id": "c4d5e6f7-1234-4abc-9def-0123456789ab",
  "source": "import",
  "title": "API import (Sage stock sync)",
  "supplierNote": null,
  "status": "draft",
  "effectiveAt": null,
  "notifyCustomers": true,
  "raiseFixedPrices": false,
  "createdBy": "Fq3L0cZ7tN1pW9aYx2Rk",
  "publishedAt": null,
  "cancelledAt": null,
  "createdAt": "2026-09-27T02:00:14.000Z",
  "items": 38
}
```

| Field              | Type           | Description                                                                    |
| ------------------ | -------------- | ------------------------------------------------------------------------------ |
| `source`           | string         | `import` (spreadsheet or API) or `bulk` (a percentage change made in the app). |
| `title`            | string         | Shown to staff. API drafts are titled “API import (key name)”.                 |
| `supplierNote`     | string or null | Message to customers in the price-change email.                                |
| `status`           | string         | See [statuses](#statuses).                                                     |
| `effectiveAt`      | string or null | When the new prices apply.                                                     |
| `notifyCustomers`  | boolean        | Whether customers are emailed their new prices.                                |
| `raiseFixedPrices` | boolean        | Whether agreed prices rise by the same percentage.                             |
| `createdBy`        | string or null | Id of the person who made it. For API drafts, the owner who made the key.      |
| `items`            | integer        | How many products it changes. Only in the list.                                |

## List price changes

**GET** `/api/v1/price-changes`

Newest first. Covers the **100 most recent** price changes.

| Name     | Type    | Required | Description              |
| -------- | ------- | -------- | ------------------------ |
| `limit`  | integer | no       | 1 to 100. Default 100.   |
| `offset` | integer | no       | Rows to skip. Default 0. |

```bash
curl https://tradecatalog.app/api/v1/price-changes \
  -H "Authorization: Bearer $TRADECATALOG_API_KEY"
```

```js
const res = await fetch('https://tradecatalog.app/api/v1/price-changes', {
  headers: { Authorization: `Bearer ${process.env.TRADECATALOG_API_KEY}` },
})
const { data: changes } = await res.json()
const drafts = changes.filter((change) => change.status === 'draft')
```

## Preview a price change

**GET** `/api/v1/price-changes/{id}`

Everything staff see before publishing: how many prices go up and down, the biggest moves, warnings, and how many agreed prices it touches. Pass `customer` to see that customer’s own prices before and after.

| Name       | In    | Type   | Required | Description                  |
| ---------- | ----- | ------ | -------- | ---------------------------- |
| `id`       | path  | string | yes      | Price change id.             |
| `customer` | query | string | no       | Customer id or account code. |

```bash
curl "https://tradecatalog.app/api/v1/price-changes/c4d5e6f7-1234-4abc-9def-0123456789ab?customer=OAK01" \
  -H "Authorization: Bearer $TRADECATALOG_API_KEY"
```

```js
const res = await fetch(
  `https://tradecatalog.app/api/v1/price-changes/${priceChangeId}?customer=OAK01`,
  { headers: { Authorization: `Bearer ${process.env.TRADECATALOG_API_KEY}` } },
)
const { data: preview } = await res.json()
if (preview.warnings.length)
  console.warn('Check these before publishing', preview.warnings)
```

```json
{
  "data": {
    "change": {
      "id": "c4d5e6f7-1234-4abc-9def-0123456789ab",
      "source": "import",
      "title": "API import (Sage stock sync)",
      "supplierNote": null,
      "status": "draft",
      "effectiveAt": null,
      "notifyCustomers": true,
      "raiseFixedPrices": false,
      "createdBy": "Fq3L0cZ7tN1pW9aYx2Rk",
      "publishedAt": null,
      "cancelledAt": null,
      "createdAt": "2026-09-27T02:00:14.000Z"
    },
    "counts": { "total": 38, "up": 36, "down": 2 },
    "biggest": [
      {
        "productId": "3f8a2c1e-9b4d-4e6f-a7c8-1d2e3f4a5b6c",
        "oldPrice": 1850,
        "newPrice": 1950,
        "sku": "HX-8510",
        "name": "Hex bolt M10 x 50 zinc",
        "changeBp": 541
      }
    ],
    "warnings": [],
    "fixedAffected": 3,
    "customer": {
      "name": "Oakfield Joinery",
      "rows": [
        {
          "sku": "HX-8510",
          "name": "Hex bolt M10 x 50 zinc",
          "before": 1573,
          "after": 1658,
          "label": "Brand discount −15%"
        }
      ]
    }
  },
  "currency": "GBP"
}
```

| Field           | Description                                                                                                     |
| --------------- | --------------------------------------------------------------------------------------------------------------- |
| `counts`        | Products in the change, and how many go up and down.                                                            |
| `biggest`       | Up to 20 items with the largest percentage move. `changeBp` is the change in basis points (541 = +5.41%).       |
| `warnings`      | Items that move by more than 30% either way, or go to zero. Usually typos.                                      |
| `fixedAffected` | Agreed prices on these products that are valid now. They only change if the change raises agreed prices.        |
| `customer`      | With `customer`: that customer’s unit prices before and after, for up to the first 50 items. `null` without it. |

`404 not_found` if there’s no such price change in this workspace.
