# Conventions

> Response shape, paging, money, percentages, dates, ids, upserts, rate limits and versioning.

## Requests and responses

- Requests and responses are JSON. Send `Content-Type: application/json` with 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`:

```json
{
  "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.

## Paging

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`:

```js
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.

## Money

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:

```js
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`](/docs/api/workspace).

## Percentages

Percentages are **basis points**: hundredths of a percent, as integers.

| Value   | Means |
| ------- | ----- |
| `1250`  | 12.5% |
| `3500`  | 35%   |
| `10000` | 100%  |

## Dates

Dates are ISO 8601 strings in UTC, like `2026-09-27T08:15:00.000Z`. The workspace’s own timezone is `timezone` on [`GET /workspace`](/docs/api/workspace).

## Ids

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:

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

## Creating and updating (PUT)

`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 limits

Each key can make **120 requests a minute**. Over that, you get `429 rate_limited` with `Retry-After: 60`. Wait and try again. [Errors](/docs/api/errors#retrying) has a retry helper.

Batch writes where you can: one `PUT` with 100 products counts as one request.

## Versioning

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](/docs/changelog).
