# Price changes

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

Bulk changes, scheduling and customer price-change emails require Pro. Free can publish Standard-price spreadsheet imports immediately without emails. Existing agreed prices and published scheduled changes continue to apply when Pro ends.

List prices never change silently in TradeCatalog. New prices go into a **price change** for one price list: a draft that staff review, date and publish. Customers are emailed only when their own price changes. Standard is the list when you don’t name another. One draft never mixes lists.

## 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 canceling 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` | Canceled 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,
  "priceListId": "1a2b3c4d-5e6f-4a7b-8c9d-0e1f2a3b4c5d",
  "priceListName": "Standard",
  "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. That choice is separate from the list.         |
| `priceListId`      | string or null | The list this change writes. Null means Standard, including changes from before lists were named. |
| `priceListName`    | string         | That list’s name. `Standard` when `priceListId` is null.                                          |
| `createdBy`        | string or null | Id of the person who made it. For API drafts, the person 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,
      "priceListId": "1a2b3c4d-5e6f-4a7b-8c9d-0e1f2a3b4c5d",
      "priceListName": "Standard",
      "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, from their own list and then their adjustment, for up to the first 50 items. A customer on another list is unchanged. `null` without it. |

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