# Producten

> Lijsten, zoeken, lezen en maken of bijwerken van producten, met lijsprijzen of enige klantprijs.

Een product heeft één SKU, een lijstprijs en optionele merk, categorie, voorraadstatus en extra details. Prijzen worden altijd voor u berekend: zie [Prices](/docs/api/prices).

Producten kunnen ook `familyId` en `orderStep` hebben (standaard 1). Een enkelvoudige productlezing bevat optionele `family` metadata met zijn naam, variantattribuutnamen en instelling. Een geconfigureerde prijs bevat `orderingFields`, `configurationRequired`, `definitionVersion` en optionele klantgeprijsde `quantityBreaks`. Volgordes velden zijn koperinput; `details` blijven productfeiten.

**POST** `/api/v1/products/{id}/quote`

Deze alleen-lezen bewerking accepteert `{ "customer": "ACME01", "qty": 3, "configuration": { "length": 2.75, "reference": "Kitchen" } }`. `customer` is optioneel voor medewerkerslijstprijzen. Alleen-lezen sleutels kunnen het gebruiken. Het retourneert `data.productId`, `qty`, gevalideerde `fields`, `definitionVersion` en `price` met `unit`, `line` en de gekozen specificatie. Ontbrekende verplichte velden retourneren Configureer voor prijs; ongeldige combinaties worden geweigerd. Het maakt geen mandje, bestelling of betaling aan. Gebruik `price.line` in plaats van de eenheidsprijs te vermenigvuldigen.

Imports accepteren optionele `family`, `orderStep` en `orderingDefinition`. Een definitie gebruikt het schema in OpenAPI, vereist Pro en globale prijsrechten, en retourneert `ruleDrafts` voor review via de bestaande prijswijzigingsworkflow. Ontbrekende structuur laat bestaande instellingen onveranderd. Zie [trade catalogue setup](/docs/guides/trade-catalogues).

## Het productobject

```json
{
  "id": "3f8a2c1e-9b4d-4e6f-a7c8-1d2e3f4a5b6c",
  "sku": "HX-8510",
  "name": "Hex bolt M10 x 50 zinc",
  "description": "Grade 8.8, zinc plated, fully threaded.",
  "brandId": "a1b2c3d4-0000-4000-8000-000000000001",
  "categoryId": "a1b2c3d4-0000-4000-8000-000000000002",
  "manufacturerRef": "DIN933-M10-50",
  "unitLabel": "Box of 100",
  "minOrderQty": 1,
  "availability": "in_stock",
  "stockQty": 240,
  "lowStockAt": null,
  "leadTimeNote": null,
  "dealText": null,
  "active": true,
  "visibility": "all",
  "details": { "Finish": "Zinc", "Thread": "M10" },
  "createdAt": "2026-08-14T09:30:00.000Z",
  "updatedAt": "2026-09-20T16:02:11.000Z",
  "price": {
    "unit": 1850,
    "line": 1850,
    "list": 1850,
    "discountBp": 0,
    "source": "list",
    "label": ""
  }
}
```

| Veld                    | Type            | Beschrijving                                                                                                                                                                                                                                                                                                                                                                                                                                   |
| ----------------------- | --------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `id`                    | string          | Product id.                                                                                                                                                                                                                                                                                                                                                                                                                                    |
| `sku`                   | string          | Uw voorraadcode. Uniek in de werkruimte, hoofdletteronafhankelijk.                                                                                                                                                                                                                                                                                                                                                                             |
| `name`                  | string          | Productnaam.                                                                                                                                                                                                                                                                                                                                                                                                                                   |
| `description`           | string of null  | Langere beschrijving.                                                                                                                                                                                                                                                                                                                                                                                                                          |
| `brandId`, `categoryId` | string of null  | Zie [merken en categorieën](#brands-and-categories).                                                                                                                                                                                                                                                                                                                                                                                           |
| `manufacturerRef`       | string of null  | Artikelnummer van de fabrikant. Zoekbaar zoals de SKU.                                                                                                                                                                                                                                                                                                                                                                                         |
| `unitLabel`             | string          | Wat één eenheid is, bijvoorbeeld “Stuk” of “Doos met 100”.                                                                                                                                                                                                                                                                                                                                                                                     |
| `minOrderQty`           | integer         | Kleinst mogelijke hoeveelheid die kopers kunnen bestellen.                                                                                                                                                                                                                                                                                                                                                                                     |
| `availability`          | string          | `not_tracked`, `in_stock`, `low_stock`, `out_of_stock` of `made_to_order`. Een label voor kopers alleen: elk product kan besteld worden. Het verzenden zonder `stockQty` verwijdert elke opgeslagen hoeveelheid (je stelt de status handmatig in). Met een `stockQty` berekent het voor je: 0 of minder is `out_of_stock`, tot het laag niveau is `low_stock`, boven dat is `in_stock`. `made_to_order` wordt nooit veranderd door het aantal. |
| `stockQty`              | integer of null | Aantal op voorraad, of `null` als je dit product niet telt. Wanneer ingesteld, volgt `availability` hierop. Niet opgenomen wanneer je als klant vraagt (`customer`).                                                                                                                                                                                                                                                                           |
| `lowStockAt`            | integer of null | Dit product’s eigen laag voorraadniveau, of `null` om het werkplek niveau te gebruiken. Niet opgenomen wanneer je als klant vraagt.                                                                                                                                                                                                                                                                                                            |
| `leadTimeNote`          | string of null  | Bijv. “3–5 werkdagen”.                                                                                                                                                                                                                                                                                                                                                                                                                         |
| `dealText`              | string of null  | Een opmerking naast de prijs, zoals “min 12 btls 45”. De titel is het deal-label van de werkplek (Instellingen, Catalogusweergave).                                                                                                                                                                                                                                                                                                            |
| `active`                | boolean         | Inactieve producten zijn verborgen voor kopers.                                                                                                                                                                                                                                                                                                                                                                                                |
| `visibility`            | string          | `all`, of `selected` wanneer alleen geselecteerde klanten het kunnen zien.                                                                                                                                                                                                                                                                                                                                                                     |
| `details`               | object          | Extra kolommen van importen, zoals `{ "Finish": "Zinc" }`.                                                                                                                                                                                                                                                                                                                                                                                     |
| `price`                 | object of null  | De prijs bij hoeveelheid 1 (of `qty`). Zie [Prijzen](/docs/api/prices#the-price-object). `unit` is `null` wanneer het product nog geen prijs heeft.                                                                                                                                                                                                                                                                                            |

Stockaantallen zijn van u om up-to-date te houden: bestellingen veranderen nooit `stockQty`, dus stuur het vanuit uw stocksystem wanneer het verandert. Klanten zien alleen de `availability` status, nooit het aantal. Bij nul kan een product nog steeds besteld worden; het is gewoon gemarkeerd als uit voorraad.

`GET /products/{id}` bevat ook de namen `brand` en `category`.

Afbeeldingen worden niet geretourneerd door de API. U kunt er een instellen met `imageUrl` bij het aanmaken of bijwerken.

## Producten lijsten of zoeken

**GET** `/api/v1/products`

Zonder `q` worden producten op naam gelijst. Met `q` komen de resultaten in deze volgorde:

1. exacte SKU of fabrikantreferentie
2. SKU of fabrikantreferentie die begint met `q`
3. volledige tekst matches op naam, merk, categorie en beschrijving (3 of meer tekens; kortere zoekopdrachten matchen het begin van de naam)

Geef `customer` door om elke rij voor die klant te prijzen en retourneer alleen producten die die klant kan zien (actief, en ofwel voor iedereen of gekozen voor hen). Dat is precies wat de klant ziet in hun handelsportaal.

| Naam           | Type              | Verplicht | Beschrijving                                                                                                                              |
| -------------- | ----------------- | --------- | ----------------------------------------------------------------------------------------------------------------------------------------- |
| `q`            | string            | niet      | SKU, fabrikantreferentie, naam, merk of categorie. Maximaal 200 tekens.                                                                   |
| `category`     | string            | niet      | Categorie id.                                                                                                                             |
| `brand`        | string            | niet      | Merk id.                                                                                                                                  |
| `availability` | string            | niet      | Een van de beschikbaarheidswaarden.                                                                                                       |
| `active`       | `true` of `false` | niet      | `true` retourneert alleen actieve producten. Standaard is `true` als `customer` is ingesteld; anders worden alle producten geretourneerd. |
| `customer`     | string            | niet      | Klant id of accountcode.                                                                                                                  |
| `limit`        | integer           | niet      | 1 tot 50. Standaard 50.                                                                                                                   |
| `offset`       | integer           | niet      | Rijen om te overslaan. Standaard 0.                                                                                                       |

```bash
curl "https://tradecatalog.app/api/v1/products?q=hex%20bolt&customer=OAK01&limit=20" \
  -H "Authorization: Bearer $TRADECATALOG_API_KEY"
```

```js
const params = new URLSearchParams({
  q: 'hex bolt',
  customer: 'OAK01',
  limit: '20',
})
const res = await fetch(`https://tradecatalog.app/api/v1/products?${params}`, {
  headers: { Authorization: `Bearer ${process.env.TRADECATALOG_API_KEY}` },
})
const { data: products, nextOffset } = await res.json()
```

```json
{
  "data": [
    {
      "id": "3f8a2c1e-9b4d-4e6f-a7c8-1d2e3f4a5b6c",
      "sku": "HX-8510",
      "name": "Hex bolt M10 x 50 zinc",
      "description": "Grade 8.8, zinc plated, fully threaded.",
      "brandId": "a1b2c3d4-0000-4000-8000-000000000001",
      "categoryId": "a1b2c3d4-0000-4000-8000-000000000002",
      "manufacturerRef": "DIN933-M10-50",
      "unitLabel": "Box of 100",
      "minOrderQty": 1,
      "availability": "in_stock",
      "leadTimeNote": null,
      "active": true,
      "visibility": "all",
      "details": { "Finish": "Zinc", "Thread": "M10" },
      "createdAt": "2026-08-14T09:30:00.000Z",
      "updatedAt": "2026-09-20T16:02:11.000Z",
      "price": {
        "unit": 1573,
        "line": 1573,
        "list": 1850,
        "discountBp": 1500,
        "source": "brand",
        "label": "Brand discount −15%"
      }
    }
  ],
  "nextOffset": null,
  "currency": "GBP"
}
```

## Een product ophalen

**GET** `/api/v1/products/{id}`

| Naam       | In    | Type    | Verplicht | Beschrijving                                                                           |
| ---------- | ----- | ------- | --------- | -------------------------------------------------------------------------------------- |
| `id`       | path  | string  | ja        | Product id.                                                                            |
| `customer` | query | string  | niet      | Klant id of accountcode. Retourneert `404` als die klant het product niet kan zien.    |
| `qty`      | query | integer | niet      | Hoeveelheid om te prijzen, 1 tot 1.000.000. Standaard 1. `price.line` is `unit × qty`. |

```bash
curl "https://tradecatalog.app/api/v1/products/3f8a2c1e-9b4d-4e6f-a7c8-1d2e3f4a5b6c?customer=OAK01&qty=5" \
  -H "Authorization: Bearer $TRADECATALOG_API_KEY"
```

```js
const res = await fetch(
  `https://tradecatalog.app/api/v1/products/${productId}?customer=OAK01&qty=5`,
  { headers: { Authorization: `Bearer ${process.env.TRADECATALOG_API_KEY}` } },
)
const { data: product } = await res.json()
```

```json
{
  "data": {
    "id": "3f8a2c1e-9b4d-4e6f-a7c8-1d2e3f4a5b6c",
    "sku": "HX-8510",
    "name": "Hex bolt M10 x 50 zinc",
    "description": "Grade 8.8, zinc plated, fully threaded.",
    "brandId": "a1b2c3d4-0000-4000-8000-000000000001",
    "categoryId": "a1b2c3d4-0000-4000-8000-000000000002",
    "manufacturerRef": "DIN933-M10-50",
    "unitLabel": "Box of 100",
    "minOrderQty": 1,
    "availability": "in_stock",
    "leadTimeNote": null,
    "active": true,
    "visibility": "all",
    "details": { "Finish": "Zinc", "Thread": "M10" },
    "createdAt": "2026-08-14T09:30:00.000Z",
    "updatedAt": "2026-09-20T16:02:11.000Z",
    "brand": "Northgate",
    "category": "Bolts",
    "price": {
      "unit": 1573,
      "line": 7865,
      "list": 1850,
      "discountBp": 1500,
      "source": "brand",
      "label": "Brand discount −15%"
    }
  },
  "currency": "GBP"
}
```

Gebruik `GET /products?q=HX-8510` om een product op SKU te zoeken: een exacte SKU-match komt altijd eerst. Gebruik ook [`GET /prices`](/docs/api/prices), wat een SKU neemt.

## Producten aanmaken of bijwerken

**PUT** `/api/v1/products`

Stuur maximaal **100 producten** per oproep. Elk wordt matched met een bestaand product via de SKU, ongeacht hoofdletters. Als er geen match is, wordt een nieuw product aangemaakt. Merken en categorieën worden matched op naam en aangemaakt als ze niet bestaan.

### Hoe lijsprijzen worden behandeld

| Het product...                    | Wat gebeurt er met de prijs                                                                                                                                                                                               |
| --------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| is nieuw                          | Gaat live op `listPrice` direct.                                                                                                                                                                                          |
| bestaat maar heeft nog geen prijs | Gaat live op `listPrice` direct.                                                                                                                                                                                          |
| bestaat met dezelfde prijs        | Niets.                                                                                                                                                                                                                    |
| bestaat met een andere prijs      | Niets verandert nog. De nieuwe prijs gaat in een **conceptprijsverandering** voor die lijst. `priceChangeId` is het concept voor de lijst van `listPrice`. `priceChangesByList` heeft één entry per lijst die veranderde. |

Conceptprijsveranderingen zijn hoe TradeCatalog een typfout voorkomt bij klanten. De eigenaar of medewerkers bekijken het concept in de app (**Prijsveranderingen**), controleren de grootste bewegingen en waarschuwingen, kiezen een datum en of klanten moeten worden geëmaild, en publiceren het. U kunt een concept bekijken met [`GET /price-changes/{id}`](/docs/api/price-changes#preview-a-price-change).

**Eén synchronisatie, één concept per lijst:** Een oproep die prijzen verandert start een concept per lijst, genaamd “API import (sleutelnaam)” voor Standard en met de lijstnaam wanneer u meer dan één stuurt. Om een hele synchronisatie in die concepten te houden, stuur `priceChangeId` (de lijst voor `listPrice`) en `drafts` (de andere lijsten) van de eerste oproep met elke latere oproep. Elke moet nog steeds een concept zijn: zodra het gepubliceerd, geannuleerd of weggegooid is, laat dat id weg om een nieuw one te starten.

### Lichaam

| Veld                         | Type                 | Verplicht | Beschrijving                                                                                                                                                                                                                                                                                                                                                                                                                                      |
| ---------------------------- | -------------------- | --------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `products`                   | array                | ja        | 1 tot 100 producten. SKUs moeten uniek zijn binnen de oproep.                                                                                                                                                                                                                                                                                                                                                                                     |
| `priceChangeId`              | string               | nee       | Een concept voor de `listPrice`-lijst van een vorige oproep. Gewijzigde prijzen voor die lijst worden er aan toegevoegd. 404 als het niet bestaat, 400 als het geen concept meer is.                                                                                                                                                                                                                                                              |
| `drafts`                     | array                | nee       | Maximaal 30 `{ list, priceChangeId }`-vermeldingen zodat andere lijsten hun eigen concepten kunnen voortzetten.                                                                                                                                                                                                                                                                                                                                   |
| `products[].sku`             | string               | ja        | Maximaal 64 tekens.                                                                                                                                                                                                                                                                                                                                                                                                                               |
| `products[].name`            | string               | ja        | Maximaal 200 tekens.                                                                                                                                                                                                                                                                                                                                                                                                                              |
| `products[].listPrice`       | integer of null      | ja        | Minder eenheden, 0 tot 1.000.000.000. `null` is prijs op verzoek: een nieuw product heeft geen prijs, een bestaand product behoudt de zijne.                                                                                                                                                                                                                                                                                                      |
| `products[].priceList`       | string of null       | nee       | Naam van een bestaande prijslijst voor `listPrice`. Weglaten of `null` is Standard. Een onbekende naam wordt afgewezen.                                                                                                                                                                                                                                                                                                                           |
| `products[].listPrices`      | array                | nee       | Andere lijsten, zoals `{ list, price }`. `price` is minder eenheden, of `null` om die lijst onveranderd te laten. Een onbekende naam wordt afgewezen.                                                                                                                                                                                                                                                                                             |
| `products[].unitLabel`       | string               | ja        | Maximaal 60 tekens, bijvoorbeeld “Stuk”.                                                                                                                                                                                                                                                                                                                                                                                                          |
| `products[].brand`           | string               | nee       | Merknaam, maximaal 120 tekens. **Gecleard als deze wordt weggelaten.**                                                                                                                                                                                                                                                                                                                                                                            |
| `products[].category`        | string               | nee       | Categorienaam, maximaal 120 tekens. **Gecleard als deze wordt weggelaten.**                                                                                                                                                                                                                                                                                                                                                                       |
| `products[].manufacturerRef` | string               | nee       | Maximaal 120 tekens. **Gecleard als deze wordt weggelaten.**                                                                                                                                                                                                                                                                                                                                                                                      |
| `products[].description`     | string               | nee       | Maximaal 4.000 tekens. **Gewisig als deze ontbreekt.**                                                                                                                                                                                                                                                                                                                                                                                            |
| `products[].availability`    | string               | nee       | Een van de beschikbaarheidswaarden. Behouden als deze ontbreekt; nieuwe producten beginnen als `not_tracked`.                                                                                                                                                                                                                                                                                                                                     |
| `products[].stockQty`        | geheel getal of null | nee       | Voorraadhoeveelheid, van -1.000.000 tot 1.000.000.000. Stelt `availability` in (zie het productobject); een status die u erbij stuurt wordt genegeerd, behalve `made_to_order`. Behouden als deze ontbreekt, tenzij u een `availability` zonder deze stuurt: dan wordt het aantal gewist en wordt die status gebruikt. `null` stopt met tellen; de `availability` die in dezelfde rij wordt gestuurd, geldt, of de huidige status als er geen is. |
| `products[].lowStockAt`      | geheel getal of null | nee       | Laag voorraadniveau voor dit product, van 0. `null` gebruikt het werkruimteniveau (Instellingen, 5 om te beginnen). Behouden als deze ontbreekt.                                                                                                                                                                                                                                                                                                  |
| `products[].leadTimeNote`    | string               | nee       | Maximaal 200 tekens. Behouden als deze ontbreekt.                                                                                                                                                                                                                                                                                                                                                                                                 |
| `products[].dealText`        | string               | nee       | Maximaal 200 tekens, weergegeven naast de prijs. Behouden als deze ontbreekt.                                                                                                                                                                                                                                                                                                                                                                     |
| `products[].details`         | object               | nee       | Maximaal 30 extra kolommen: namen tot 60 tekens, waarden tot 500. Vervangt alle details wanneer gestuurd; behouden als deze ontbreekt.                                                                                                                                                                                                                                                                                                            |
| `products[].imageUrl`        | string               | nee       | Een `https` afbeeldings-URL. Kopieert naar TradeCatalog op de achtergrond wanneer het verandert. Behouden als deze ontbreekt.                                                                                                                                                                                                                                                                                                                     |
| `products[].onlyFor`         | array                | nee       | Maximaal 200 accountcodes of contactmails. Maakt het product zichtbaar **alleen** voor die klanten, in plaats van een vorige lijst. Behouden als deze ontbreekt.                                                                                                                                                                                                                                                                                  |

**Stuur elke veld dat je belangrijk vindt:** `brand`, `category`, `manufacturerRef` en `description` worden zoals verstuurd geschreven, dus als u er een laatste, wordt deze gewist. Stuur het volledige product elke keer, zoals een spreadsheetrij.

Deze kunnen nog niet worden gewijzigd via de API: een product inactief maken, `minOrderQty`, een afbeelding verwijderen, een leadtime-notitie wissen en een klant-alleen product opnieuw zichtbaar maken voor iedereen. Doe dit in de app.

```bash
curl -X PUT https://tradecatalog.app/api/v1/products \
  -H "Authorization: Bearer $TRADECATALOG_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "products": [
      {
        "sku": "HX-8510",
        "name": "Hex bolt M10 x 50 zinc",
        "listPrice": 1950,
        "unitLabel": "Box of 100",
        "brand": "Northgate",
        "category": "Bolts",
        "manufacturerRef": "DIN933-M10-50",
        "availability": "in_stock",
        "details": { "Finish": "Zinc", "Thread": "M10" }
      },
      {
        "sku": "OAK-HNG-75",
        "name": "Oak-finish butt hinge 75mm",
        "listPrice": 695,
        "unitLabel": "Pair",
        "category": "Hinges",
        "onlyFor": ["OAK01"]
      }
    ]
  }'
```

```js
const res = await fetch('https://tradecatalog.app/api/v1/products', {
  method: 'PUT',
  headers: {
    Authorization: `Bearer ${process.env.TRADECATALOG_API_KEY}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({ products }),
})
const { data: result } = await res.json()
if (result.priceChangeId)
  console.log('Review the draft price change in TradeCatalog')
```

```json
{
  "data": {
    "created": 1,
    "updated": 1,
    "priceChanges": 1,
    "priceChangeId": "c4d5e6f7-1234-4abc-9def-0123456789ab",
    "priceChangesByList": [
      {
        "list": "Standard",
        "priceChangeId": "c4d5e6f7-1234-4abc-9def-0123456789ab"
      }
    ],
    "unknownCustomers": []
  }
}
```

| Veld                 | Beschrijving                                                                                        |
| -------------------- | --------------------------------------------------------------------------------------------------- |
| `created`            | Nieuwe producten.                                                                                   |
| `updated`            | Bestaande producten die overeenkomen met SKU.                                                       |
| `priceChanges`       | Hoeveel prijzen zijn opgenomen in een concept, over elk lijst in de oproep.                         |
| `priceChangeId`      | Het concept voor de `listPrice`-lijst, of `null` als die lijst geen wijzigingen had.                |
| `priceChangesByList` | Een `{ list, priceChangeId }` voor elke lijst die in deze oproep een prijswijziging had.            |
| `unknownCustomers`   | Waarden in `onlyFor` die geen klant hebben gevonden (maximaal 50). Die klanten kregen geen toegang. |

Veranderingen verschijnen in de activiteitenlog als de naam van de sleutel, en zoekopdrachten worden direct bijgewerkt.

## Merken en categorieën

**GET** `/api/v1/brands`

**GET** `/api/v1/categories`

Beide retourneren elke rij, gesorteerd op naam, zonder paginering. Gebruik de ids met de filters `brand` en `category` op `GET /products`.

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

```js
const res = await fetch('https://tradecatalog.app/api/v1/categories', {
  headers: { Authorization: `Bearer ${process.env.TRADECATALOG_API_KEY}` },
})
const { data: categories } = await res.json()
```

```json
{
  "data": [
    { "id": "a1b2c3d4-0000-4000-8000-000000000002", "name": "Bolts" },
    { "id": "a1b2c3d4-0000-4000-8000-000000000003", "name": "Hinges" }
  ]
}
```

Om een merk of categorie te maken, noem het op een product in `PUT /products`.
