Requests and responsesLink to this section#
- Requests and responses are JSON. Send
Content-Type: application/jsonwith a body. - Field names are camelCase:
listPrice,accountCode,nextOffset. - Query parameters are plain strings:
?limit=20&status=new.
Every successful response is an object with data:
{
"data": [],
"nextOffset": 50,
"currency": "GBP"
}
| Field | When it’s there | Meaning |
|---|---|---|
data | Always | The object or list you asked for. |
nextOffset | Lists that page | Pass it as offset to get the next page. null on the last page. |
currency | Responses with prices in the workspace currency | ISO 4217 code for every money field in data. |
Orders carry their own currency field, fixed when the order was placed.
PagingLink to this section#
Lists take limit and offset:
| Parameter | Default | Maximum |
|---|---|---|
limit | the maximum | 50 for products, 100 for everything else |
offset | 0 | none |
Keep requesting with offset=nextOffset until nextOffset is null:
let offset = 0
const all = []
while (offset !== null) {
const res = await fetch(
`https://tradecatalog.app/api/v1/products?limit=50&offset=${offset}`,
{
headers: { Authorization: `Bearer ${process.env.TRADECATALOG_API_KEY}` },
},
)
const page = await res.json()
all.push(...page.data)
offset = page.nextOffset
}
On /products, nextOffset is set whenever a page is full. If the catalogue size is an exact multiple of limit, the last request returns an empty data list with nextOffset: null.
Some lists are capped. GET /orders covers the 200 most recent matching orders, and GET /price-changes the 100 most recent.
MoneyLink to this section#
Money is an integer in minor units of the currency, so there’s no rounding error:
| Value | Currency | Means |
|---|---|---|
1234 | GBP | £12.34 |
1850 | EUR | €18.50 |
500 | JPY | ¥500 (no minor unit) |
Divide by 10 to the power of the currency’s decimal places to display it. In JavaScript, Intl.NumberFormat does it for you:
const format = (minor, currency) => {
const f = new Intl.NumberFormat('en-GB', { style: 'currency', currency })
const digits = f.resolvedOptions().maximumFractionDigits
return f.format(minor / 10 ** digits)
}
format(1234, 'GBP') // "£12.34"
Prices are before tax. The workspace’s wording for this (for example “ex VAT”) is taxLabel on GET /workspace.
PercentagesLink to this section#
Percentages are basis points: hundredths of a percent, as integers.
| Value | Means |
|---|---|
1250 | 12.5% |
3500 | 35% |
10000 | 100% |
DatesLink to this section#
Dates are ISO 8601 strings in UTC, like 2026-09-27T08:15:00.000Z. The workspace’s own timezone is timezone on GET /workspace.
IdsLink to this section#
Ids are UUID strings. Wherever an endpoint takes a customer, you can pass the customer’s id or their account code (case-insensitive), whichever your system has:
curl "https://tradecatalog.app/api/v1/prices?customer=OAK01&sku=HX-8510" \
-H "Authorization: Bearer $TRADECATALOG_API_KEY"
Creating and updating (PUT)Link to this section#
PUT /products and PUT /customers create or update in one call, up to 100 rows at a time:
- Products are matched by SKU (case-insensitive).
- Customers are matched by account code, else email, else name.
Both are safe to repeat. Sending the same rows twice gives the same result; the second call only reports them as updated.
A PUT writes the fields you send. Some optional product fields are cleared when you leave them out, others are kept. Each endpoint’s page lists which.
Rate limitsLink to this section#
Each key can make 120 requests a minute. Over that, you get 429 rate_limited with Retry-After: 60. Wait and try again. Errors has a retry helper.
Batch writes where you can: one PUT with 100 products counts as one request.
VersioningLink to this section#
The version is in the path: /api/v1.
- Within v1 we only make additive changes: new endpoints, new optional parameters, new fields in responses. Write your code to ignore fields it doesn’t know.
- A breaking change would get a new version (
/api/v2), with v1 kept running and the change announced in the changelog.