# Produits

> Listez, recherchez, lisez et créez ou mettez à jour des produits, avec des prix de liste ou les prix de n'importe quel client.

Un produit a un SKU, un prix de liste, et des informations supplémentaires facultatives comme marque, catégorie, statut de stock et détails supplémentaires. Les prix sont toujours calculés pour vous : voir [Prix](/docs/api/prices).

Les produits peuvent avoir aussi `familyId` et `orderStep` (par défaut 1). Une lecture d'un seul produit inclut des métadonnées `family` facultatives avec son nom, les noms des attributs de variante et les préréglages. Un prix configuré contient `orderingFields`, `configurationRequired`, `definitionVersion` et un prix client facultatif `quantityBreaks`. Les champs de tri sont des entrées de l'acheteur ; `details` restent des faits sur le produit.

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

Cette opération en lecture seule accepte `{ "customer": "ACME01", "qty": 3, "configuration": { "length": 2.75, "reference": "Kitchen" } }`. `customer` est facultatif pour la tarification par liste du personnel. Les clés en lecture seule peuvent l'utiliser. Elle retourne `data.productId`, `qty`, `fields` validés, `definitionVersion` et `price` avec `unit`, `line` et la spécification choisie. Les champs requis manquants renvoient Configurez pour le prix ; les combinaisons invalides sont refusées. Elle ne crée pas de panier, de commande ou de paiement. Utilisez `price.line` plutôt que de multiplier le prix unitaire.

Les imports acceptent des `family`, `orderStep` et `orderingDefinition` facultatifs. Une définition utilise le schéma d'OpenAPI, nécessite le Pro et les autorisations de tarification globale, et retourne `ruleDrafts` pour révision via le processus de changement de prix existant. Une structure omise laisse les paramètres existants inchangés. Voir [configuration du catalogue de commerce](/docs/guides/trade-catalogues).

## L'objet produit

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

| Champ                   | Type           | Description                                                                                                                                                                                                                                                                                                                                                                                                                                                       |
| ----------------------- | -------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `id`                    | chaîne         | Identifiant du produit.                                                                                                                                                                                                                                                                                                                                                                                                                                           |
| `sku`                   | chaîne         | Votre code de stock. Unique dans l'espace de travail, indépendamment de la casse.                                                                                                                                                                                                                                                                                                                                                                                 |
| `name`                  | chaîne         | Nom du produit.                                                                                                                                                                                                                                                                                                                                                                                                                                                   |
| `description`           | chaîne ou null | Description plus longue.                                                                                                                                                                                                                                                                                                                                                                                                                                          |
| `brandId`, `categoryId` | chaîne ou null | Voir [marques et catégories](#brands-and-categories).                                                                                                                                                                                                                                                                                                                                                                                                             |
| `manufacturerRef`       | chaîne ou null | Numéro de pièce du fabricant. Recherchable comme le SKU.                                                                                                                                                                                                                                                                                                                                                                                                          |
| `unitLabel`             | chaîne         | Ce qu’est une unité, par exemple « Chaque » ou « Boîte de 100 ».                                                                                                                                                                                                                                                                                                                                                                                                  |
| `minOrderQty`           | entier         | Quantité minimale que les acheteurs peuvent commander.                                                                                                                                                                                                                                                                                                                                                                                                            |
| `availability`          | chaîne         | `not_tracked`, `in_stock`, `low_stock`, `out_of_stock` ou `made_to_order`. Une étiquette uniquement pour les acheteurs : chaque produit peut être commandé. L’envoi sans `stockQty` efface tout compteur stocké (vous définissez manuellement le statut). Avec un `stockQty`, il est calculé pour vous : 0 ou moins est `out_of_stock`, jusqu’au seuil bas est `low_stock`, au-dessus de ce seuil `in_stock`. `made_to_order` n’est jamais modifié par le nombre. |
| `stockQty`              | entier ou null | Unités en stock, ou `null` si vous ne comptez pas ce produit. Lorsqu’il est défini, `availability` lui est associé. Omis lorsque vous demandez en tant que client (`customer`).                                                                                                                                                                                                                                                                                   |
| `lowStockAt`            | entier ou null | Niveau de stock propre à ce produit, ou `null` pour utiliser le niveau de l’espace de travail. Omis lorsque vous demandez en tant que client.                                                                                                                                                                                                                                                                                                                     |
| `leadTimeNote`          | chaîne ou null | Par exemple « 3–5 jours ouvrables ».                                                                                                                                                                                                                                                                                                                                                                                                                              |
| `dealText`              | chaîne ou null | Une note affichée à côté du prix, telle que « min 12 btls 45 ». Son titre est l’étiquette de l’offre de l’espace de travail (Paramètres, Affichage du catalogue).                                                                                                                                                                                                                                                                                                 |
| `active`                | booléen        | Les produits inactifs sont masqués des acheteurs.                                                                                                                                                                                                                                                                                                                                                                                                                 |
| `visibility`            | chaîne         | `all`, ou `selected` lorsqu’uniquement certains clients peuvent le voir.                                                                                                                                                                                                                                                                                                                                                                                          |
| `details`               | objet          | Colonnes supplémentaires provenant des importations, comme `{ "Finish": "Zinc" }`.                                                                                                                                                                                                                                                                                                                                                                                |
| `price`                 | objet ou null  | Le prix à la quantité 1 (ou `qty`). Voir [Prix](/docs/api/prices#the-price-object). `unit` est `null` lorsqu’un produit n’a pas encore de prix.                                                                                                                                                                                                                                                                                                                   |

Les comptes de stock sont à votre charge pour les maintenir à jour : les commandes ne modifient jamais `stockQty`, envoyez-le donc depuis votre système de stock chaque fois qu’il change. Les clients ne voient que le statut `availability`, jamais le nombre. À zéro, un produit peut toujours être commandé ; il est simplement étiqueté comme épuisé.

`GET /products/{id}` inclut également les noms `brand` et `category`.

Les images ne sont pas renvoyées par l’API. Vous pouvez en définir une avec `imageUrl` lors de la création ou de la mise à jour.

## Liste ou recherche de produits

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

Sans `q`, les produits sont listés par nom. Avec `q`, les résultats s’affichent dans cet ordre :

1. SKU exact ou référence du fabricant
2. SKU ou référence du fabricant commençant par `q`
3. recherches par mots-clés sur le nom, la marque, la catégorie et la description (3 caractères ou plus ; les requêtes plus courtes correspondent au début du nom)

Passez `customer` pour définir le prix de chaque ligne pour ce client et ne retournez que les produits que ce client peut voir (actifs, et soit pour tout le monde, soit sélectionnés pour lui). C’est exactement ce que le client voit dans leur portail de commerce.

| Nom            | Type              | Obligatoire | Description                                                                                                                                              |
| -------------- | ----------------- | ----------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `q`            | chaîne            | non         | SKU, référence fabricant, nom, marque ou catégorie. Jusqu'à 200 caractères.                                                                              |
| `category`     | chaîne            | non         | Identifiant de catégorie.                                                                                                                                |
| `brand`        | chaîne            | non         | Identifiant de marque.                                                                                                                                   |
| `availability` | chaîne            | non         | Une des valeurs d'indisponibilité.                                                                                                                       |
| `active`       | `true` ou `false` | non         | `true` retourne uniquement les produits actifs. La valeur par défaut est `true` lorsqu'`customer` est définie ; sinon, tous les produits sont retournés. |
| `customer`     | chaîne            | non         | Identifiant du client ou code de compte.                                                                                                                 |
| `limit`        | entier            | non         | 1 à 50. Valeur par défaut 50.                                                                                                                            |
| `offset`       | entier            | non         | Lignes à ignorer. Valeur par défaut 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"
}
```

## Obtenir un produit

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

| Nom        | Dans    | Type   | Obligatoire | Description                                                                                       |
| ---------- | ------- | ------ | ----------- | ------------------------------------------------------------------------------------------------- |
| `id`       | chemin  | chaîne | oui         | Identifiant du produit.                                                                           |
| `customer` | requête | chaîne | non         | Identifiant du client ou code de compte. Retourne `404` si ce client ne peut pas voir le produit. |
| `qty`      | requête | entier | non         | Quantité à facturer, de 1 à 1 000 000. Valeur par défaut 1. `price.line` est `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"
}
```

Pour rechercher un produit par SKU, utilisez `GET /products?q=HX-8510` : une correspondance exacte de SKU est toujours prioritaire. Ou utilisez [`GET /prices`](/docs/api/prices), qui prend un SKU.

## Créer ou mettre à jour des produits

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

Envoyez jusqu'à **100 produits** par appel. Chacun est associé à un produit existant par SKU, en ignorant la casse. S'il n'y a pas de correspondance, un nouveau produit est créé. Les marques et les catégories sont associées par nom et créées si elles n'existent pas.

### Comment les prix sont gérés

| Le produit…                        | Ce qui se passe avec le prix                                                                                                                                                                                                                |
| ---------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| est nouveau                        | Devient actif à `listPrice` immédiatement.                                                                                                                                                                                                  |
| existe mais n’a pas encore de prix | Devient actif à `listPrice` immédiatement.                                                                                                                                                                                                  |
| existe au même prix                | Rien ne se passe.                                                                                                                                                                                                                           |
| existe à un prix différent         | Rien ne change pour l'instant. Le nouveau prix est placé dans un **changement de prix en brouillon** pour cette liste. `priceChangeId` est le brouillon pour la liste de `listPrice`. `priceChangesByList` a une entrée par liste modifiée. |

Les changements de prix en brouillon sont comment TradeCatalog empêche une erreur de frappe d'atteindre les clients. Le propriétaire ou le personnel examine le brouillon dans l'application (**Changements de prix**), vérifie les plus grandes variations et les avertissements, choisit une date et si les clients doivent être informés par courriel, puis publie. Vous pouvez prévisualiser un brouillon avec [`GET /price-changes/{id}`](/docs/api/price-changes#preview-a-price-change).

**Une synchronisation, un brouillon par liste:** Un appel qui modifie les prix démarre un brouillon par liste, appelé « Import API (nom de clé) » pour le plan Standard et avec le nom de la liste lorsqu'on en envoie plus d'une. Pour garder une synchronisation entière dans ces brouillons, envoyez `priceChangeId` (la liste pour `listPrice`) et `drafts` (les autres listes) depuis le premier appel avec chaque appel ultérieur. Chaque appel doit toujours être un brouillon : une fois publié, annulé ou supprimé, laissez cet identifiant de côté pour en démarrer un nouveau.

### Corps

| Champ                        | Type           | Requis | Description                                                                                                                                                                                                                                                                                                                                                                                                           |
| ---------------------------- | -------------- | ------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `products`                   | tableau        | oui    | 1 à 100 produits. Les SKU doivent être uniques dans l'appel.                                                                                                                                                                                                                                                                                                                                                          |
| `priceChangeId`              | chaîne         | non    | Un brouillon pour la liste `listPrice` d'un appel antérieur. Les prix modifiés pour cette liste y sont ajoutés. 404 si elle n'existe pas, 400 si ce n'est plus un brouillon.                                                                                                                                                                                                                                          |
| `drafts`                     | tableau        | non    | Jusqu'à 30 entrées `{ list, priceChangeId }` afin que les autres listes continuent leurs propres brouillons.                                                                                                                                                                                                                                                                                                          |
| `products[].sku`             | chaîne         | oui    | Jusqu'à 64 caractères.                                                                                                                                                                                                                                                                                                                                                                                                |
| `products[].name`            | chaîne         | oui    | Jusqu'à 200 caractères.                                                                                                                                                                                                                                                                                                                                                                                               |
| `products[].listPrice`       | entier ou null | oui    | Unités mineures, de 0 à 1 000 000 000. `null` est le prix sur demande : un nouveau produit n’a pas de prix, un produit existant conserve le sien.                                                                                                                                                                                                                                                                     |
| `products[].priceList`       | chaîne ou null | non    | Nom d'une liste de prix existante pour `listPrice`. Omettre ou `null` est Standard. Un nom inconnu est rejeté.                                                                                                                                                                                                                                                                                                        |
| `products[].listPrices`      | tableau        | non    | Autres listes, comme `{ list, price }`. `price` est des unités mineures, ou `null` pour laisser cette liste inchangée. Un nom inconnu est rejeté.                                                                                                                                                                                                                                                                     |
| `products[].unitLabel`       | chaîne         | oui    | Jusqu'à 60 caractères, par exemple « Chaque ».                                                                                                                                                                                                                                                                                                                                                                        |
| `products[].brand`           | chaîne         | non    | Nom de marque, jusqu'à 120 caractères. **Effacé si omis.**                                                                                                                                                                                                                                                                                                                                                            |
| `products[].category`        | chaîne         | non    | Nom de catégorie, jusqu'à 120 caractères. **Effacé si omis.**                                                                                                                                                                                                                                                                                                                                                         |
| `products[].manufacturerRef` | chaîne         | non    | Jusqu'à 120 caractères. **Effacé si omis.**                                                                                                                                                                                                                                                                                                                                                                           |
| `products[].description`     | chaîne         | non    | Jusqu'à 4 000 caractères. **Effacé si omis.**                                                                                                                                                                                                                                                                                                                                                                         |
| `products[].availability`    | chaîne         | non    | Une des valeurs d'indisponibilité. Conservé si omis ; les nouveaux produits commencent par `not_tracked`.                                                                                                                                                                                                                                                                                                             |
| `products[].stockQty`        | entier ou null | non    | Unités en stock, de -1 000 000 à 1 000 000 000. Définit `availability` (voir l'objet produit) ; un statut que vous envoyez avec est ignoré, sauf `made_to_order`. Conservé si omis, sauf si vous envoyez un `availability` sans lui : alors le compte est effacé et ce statut est utilisé. `null` arrête le comptage ; le `availability` envoyé dans la même ligne s'applique, ou le statut actuel s'il n'y en a pas. |
| `products[].lowStockAt`      | entier ou null | non    | Niveau de stock bas pour ce produit, à partir de 0. `null` utilise le niveau de l'espace de travail (Paramètres, 5 au départ). Conservé si omis.                                                                                                                                                                                                                                                                      |
| `products[].leadTimeNote`    | chaîne         | non    | Jusqu'à 200 caractères. Conservé si omis.                                                                                                                                                                                                                                                                                                                                                                             |
| `products[].dealText`        | chaîne         | non    | Jusqu'à 200 caractères, affichés à côté du prix. Conservé si omis.                                                                                                                                                                                                                                                                                                                                                    |
| `products[].details`         | objet          | non    | Jusqu'à 30 colonnes supplémentaires : noms jusqu'à 60 caractères, valeurs jusqu'à 500. Remplace toutes les détails lors de l'envoi ; conservé si omis.                                                                                                                                                                                                                                                                |
| `products[].imageUrl`        | chaîne         | non    | Une URL d'image `https`. Copiée dans TradeCatalog en arrière-plan lorsqu'elle change. Conservé si omis.                                                                                                                                                                                                                                                                                                               |
| `products[].onlyFor`         | tableau        | non    | Jusqu'à 200 codes de compte ou adresses e-mail de contact. Rend le produit visible **uniquement** pour ces clients, remplaçant toute liste précédente. Conservé si omis.                                                                                                                                                                                                                                              |

**Envoyez chaque champ qui vous intéresse:** `brand`, `category`, `manufacturerRef` et `description` sont écrits tels qu'envoyés, donc laisser l'un d'eux vide le vide. Envoyez le produit complet à chaque fois, comme une ligne de feuille de calcul.

Ces éléments ne peuvent pas encore être modifiés via l'API : rendre un produit inactif, `minOrderQty`, supprimer une image, effacer une note de délai de livraison, et rendre à nouveau visible un produit réservé aux clients. Faites-le dans l'application.

```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": []
  }
}
```

| Champ                | Description                                                                                         |
| -------------------- | --------------------------------------------------------------------------------------------------- |
| `created`            | Nouveaux produits.                                                                                  |
| `updated`            | Produits existants correspondant au SKU.                                                            |
| `priceChanges`       | Nombre de prix intégrés dans un brouillon, sur chaque liste de l'appel.                             |
| `priceChangeId`      | Le brouillon pour la liste `listPrice`, ou `null` si cette liste n'avait pas de modifications.      |
| `priceChangesByList` | Un `{ list, priceChangeId }` pour chaque liste ayant eu un changement de prix dans cet appel.       |
| `unknownCustomers`   | Valeurs dans `onlyFor` qui n'ont pas trouvé de client (jusqu'à 50). Ces clients n'ont pas eu accès. |

Les modifications apparaissent dans le journal d'activité sous le nom de la clé, et les recherches sont mises à jour immédiatement.

## Marques et catégories

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

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

Les deux retournent toutes les lignes, triées par nom, sans pagination. Utilisez les identifiants avec les filtres `brand` et `category` sur `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" }
  ]
}
```

Pour créer une marque ou une catégorie, donnez-lui un nom sur un produit dans `PUT /products`.
