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 catalogue setup for precedence, net treatment and publication.
How prices are worked outLink to this section#
The most specific rule wins, and adjustments never stack:
- Agreed price. If the customer has an agreed (fixed) price for the product, that’s the price.
- Otherwise the list price with one percentage, the first that applies. A percentage is a discount (taken off) or a markup (added on):
- the customer’s percentage for that product
- the customer’s percentage for the product’s category (the nearest parent wins)
- the customer’s percentage for the product’s brand
- the customer’s own percentage on the whole list
- 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 catalogue accessLink to this section#
Standard is the supplier's master catalogue. 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 catalogue 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 catalogue, 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 catalogue 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 objectLink to this section#
Every product’s price has this shape. GET /prices returns the same fields at the top level of data.
{
"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 priceLink to this section#
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. |
curl "https://tradecatalog.app/api/v1/prices?customer=OAK01&sku=HX-8510&qty=10" \
-H "Authorization: Bearer $TRADECATALOG_API_KEY"
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()
{
"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=….
Prices for many productsLink to this section#
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.
Changing pricesLink to this section#
- List prices change through price changes, so customers get notice. Send new prices with
PUT /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}. - Agreed prices for many customers are set with
PUT /agreed-prices. - A customer’s whole-list percentage can be set with
PUT /customers(discountBp, and optionaladjustment).
Set agreed pricesLink to this section#
PUT/api/v1/agreed-priceswrite key
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. |
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. |