# Productos

> Lista, busca, lee y crea o actualiza productos, con precios de lista o cualquier precio del cliente.

Un producto tiene un SKU, un precio de lista y opciones de marca, categoría, estado de stock y detalles adicionales. Los precios siempre se calculan para usted: consulte [Precios](/docs/api/prices).

Los productos también pueden tener `familyId` y `orderStep` (predeterminado 1). Una lectura de un solo producto incluye metadatos opcionales `family` con su nombre, nombres de atributos de variante y configuración predeterminada. Un precio configurado contiene `orderingFields`, `configurationRequired`, `definitionVersion` y un `quantityBreaks` opcional con precio del cliente. Los campos de ordenación son entradas del comprador; `details` permanecen como hechos del producto.

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

Esta operación de solo lectura acepta `{ "customer": "ACME01", "qty": 3, "configuration": { "length": 2.75, "reference": "Kitchen" } }`. `customer` es opcional para el precio de lista de personal. Las claves de solo lectura pueden usarla. Devuelve `data.productId`, `qty`, `fields` validados, `definitionVersion` y `price` con `unit`, `line` y la especificación elegida. Los campos obligatorios faltantes devuelven Configurar para precio; las combinaciones inválidas son rechazadas. No crea un carrito, pedido o pago. Use `price.line` en lugar de multiplicar el precio unitario.

Los imports aceptan `family`, `orderStep` y `orderingDefinition` opcionales. Una definición utiliza el esquema en OpenAPI, requiere el plan Pro y permiso para precios globales, y devuelve `ruleDrafts` para revisión a través del flujo de trabajo existente para cambios de precio. La estructura omitida deja las configuraciones existentes intactas. Vea [configuración del catálogo de comercio](/docs/guides/trade-catalogues).

## El objeto de producto

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

| Campo                   | Tipo          | Descripción                                                                                                                                                                                                                                                                                                                                                                                                                                        |
| ----------------------- | ------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `id`                    | string        | Identificador del producto.                                                                                                                                                                                                                                                                                                                                                                                                                        |
| `sku`                   | string        | Su código de stock. Único en el espacio de trabajo, sin importar mayúsculas o minúsculas.                                                                                                                                                                                                                                                                                                                                                          |
| `name`                  | string        | Nombre del producto.                                                                                                                                                                                                                                                                                                                                                                                                                               |
| `description`           | string o nulo | Descripción más larga.                                                                                                                                                                                                                                                                                                                                                                                                                             |
| `brandId`, `categoryId` | string o nulo | Vea [marcas y categorías](#brands-and-categories).                                                                                                                                                                                                                                                                                                                                                                                                 |
| `manufacturerRef`       | string o nulo | Número de pieza del fabricante. Búsqueda como el SKU.                                                                                                                                                                                                                                                                                                                                                                                              |
| `unitLabel`             | string        | Qué es una unidad, por ejemplo, "Cada" o "Caja de 100".                                                                                                                                                                                                                                                                                                                                                                                            |
| `minOrderQty`           | entero        | Cantidad mínima que los compradores pueden pedir.                                                                                                                                                                                                                                                                                                                                                                                                  |
| `availability`          | string        | `not_tracked`, `in_stock`, `low_stock`, `out_of_stock` o `made_to_order`. Una etiqueta solo para compradores: cada producto puede ser comprado. Enviarlo sin un `stockQty` borra cualquier recuento almacenado (está estableciendo el estado manualmente). Con un `stockQty` se calcula para usted: 0 o menos es `out_of_stock`, hasta el nivel mínimo es `low_stock`, por encima de él es `in_stock`. `made_to_order` nunca cambia por el número. |
| `stockQty`              | entero o nulo | Unidades en mano, o `null` cuando no cuenta este producto. Cuando se establece, `availability` lo sigue. No se incluye cuando lo pide como cliente (`customer`).                                                                                                                                                                                                                                                                                   |
| `lowStockAt`            | entero o nulo | Nivel de bajo stock propio del producto, o `null` para usar el nivel del espacio de trabajo. No se incluye cuando lo pide como cliente.                                                                                                                                                                                                                                                                                                            |
| `leadTimeNote`          | string o nulo | Por ejemplo, "3-5 días hábiles".                                                                                                                                                                                                                                                                                                                                                                                                                   |
| `dealText`              | string o nulo | Una nota mostrada junto al precio, como "mínimo 12 botellas 45". Su título es la etiqueta de oferta del espacio de trabajo (Configuración, Visualización del catálogo).                                                                                                                                                                                                                                                                            |
| `active`                | booleano      | Los productos inactivos están ocultos para los compradores.                                                                                                                                                                                                                                                                                                                                                                                        |
| `visibility`            | string        | `all`, o `selected` cuando solo ciertos clientes pueden verlo.                                                                                                                                                                                                                                                                                                                                                                                     |
| `details`               | objeto        | Columnas adicionales de importaciones, como `{ "Finish": "Zinc" }`.                                                                                                                                                                                                                                                                                                                                                                                |
| `price`                 | objeto o nulo | El precio a cantidad 1 (o `qty`). Vea [Precios](/docs/api/prices#the-price-object). `unit` es `null` cuando el producto no tiene precio aún.                                                                                                                                                                                                                                                                                                       |

Los recuentos de stock son suyos para mantener actualizados: los pedidos nunca cambian `stockQty`, por lo que puede enviarlos desde su sistema de stock cada vez que cambie. Los clientes ven solo el estado `availability`, nunca el número. A cero, un producto aún se puede pedir; simplemente se etiqueta como agotado.

`GET /products/{id}` también incluye los nombres `brand` y `category`.

Las imágenes no se devuelven por la API. Puede establecer una con `imageUrl` al crear o actualizar.

## Listar o buscar productos

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

Sin `q`, los productos se listan por nombre. Con `q`, los resultados vienen en este orden:

1. SKU exacto o referencia del fabricante
2. SKU o referencia del fabricante que comience con `q`
3. coincidencias de texto completo en nombre, marca, categoría y descripción (3 o más caracteres; las búsquedas más cortas coinciden con el comienzo del nombre)

Pase `customer` para calcular el precio de cada fila para ese cliente y devolver solo los productos que ese cliente puede ver (activos, y ya sea para todos o seleccionados para ellos). Ese es exactamente lo que el cliente ve en su portal comercial.

| Nombre         | Tipo             | Requerido | Descripción                                                                                                                                       |
| -------------- | ---------------- | --------- | ------------------------------------------------------------------------------------------------------------------------------------------------- |
| `q`            | string           | no        | SKU, referencia del fabricante, nombre, marca o categoría. Máximo 200 caracteres.                                                                 |
| `category`     | string           | no        | Id de categoría.                                                                                                                                  |
| `brand`        | string           | no        | Id de marca.                                                                                                                                      |
| `availability` | string           | no        | Uno de los valores de disponibilidad.                                                                                                             |
| `active`       | `true` o `false` | no        | `true` devuelve solo productos activos. Por defecto `true` cuando `customer` está establecido; de lo contrario, se devuelven todos los productos. |
| `customer`     | string           | no        | Id de cliente o código de cuenta.                                                                                                                 |
| `limit`        | integer          | no        | 1 a 50. Por defecto 50.                                                                                                                           |
| `offset`       | integer          | no        | Filas a omitir. Por defecto 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"
}
```

## Obtener un producto

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

| Nombre     | En    | Tipo    | Requerido | Descripción                                                                                |
| ---------- | ----- | ------- | --------- | ------------------------------------------------------------------------------------------ |
| `id`       | path  | string  | yes       | Id del producto.                                                                           |
| `customer` | query | string  | no        | Id del cliente o código de cuenta. Devuelve `404` si ese cliente no puede ver el producto. |
| `qty`      | query | integer | no        | Cantidad a precio, 1 a 1.000.000. Por defecto 1. `price.line` es `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"
}
```

Para buscar un producto por SKU, use `GET /products?q=HX-8510`: una coincidencia exacta de SKU siempre es la primera. O use [`GET /prices`](/docs/api/prices), que toma un SKU.

## Crear o actualizar productos

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

Envíe hasta **100 productos** por llamada. Cada uno se compara con un producto existente por SKU, ignorando mayúsculas y minúsculas. Si no hay coincidencia, se crea un nuevo producto. Marcas y categorías se comparan por nombre y se crean si no existen.

### Cómo se manejan los precios de lista

| El producto…                    | Qué le ocurre al precio                                                                                                                                                                                                     |
| ------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| es nuevo                        | Se pone en marcha en `listPrice` inmediatamente.                                                                                                                                                                            |
| existe pero aún no tiene precio | Se pone en marcha en `listPrice` inmediatamente.                                                                                                                                                                            |
| existe al mismo precio          | Nada.                                                                                                                                                                                                                       |
| existe a un precio diferente    | Nada cambia todavía. El nuevo precio entra en un **cambio de precio en borrador** para esa lista. `priceChangeId` es el borrador para la lista de `listPrice`. `priceChangesByList` tiene una entrada por lista que cambió. |

Los cambios de precio en borrador son cómo TradeCatalog evita que un error de tipeo llegue a los clientes. El propietario o el personal revisa el borrador en la aplicación (**Cambios de precio**), revisa los mayores movimientos y advertencias, elige una fecha y si enviar un correo a los clientes, y luego publica. Puede previsualizar un borrador con [`GET /price-changes/{id}`](/docs/api/price-changes#preview-a-price-change).

**Una sincronización, un borrador por lista:** Una llamada que cambia precios inicia un borrador por lista, llamado “Importación de API (nombre de clave)” para Standard y con el nombre de la lista cuando envía más de una. Para mantener una sincronización completa en esos borradores, envíe `priceChangeId` (la lista para `listPrice`) y `drafts` (las otras listas) desde la primera llamada con cada llamada posterior. Cada una aún debe ser un borrador: una vez que se publique, se cancele o se descarte, deje de incluir ese id para iniciar uno nuevo.

### Cuerpo

| Campo                        | Tipo           | Requerido | Descripción                                                                                                                                                                                                                                                                                                                                                                                                               |
| ---------------------------- | -------------- | --------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `products`                   | array          | yes       | 1 a 100 productos. Los SKUs deben ser únicos dentro de la llamada.                                                                                                                                                                                                                                                                                                                                                        |
| `priceChangeId`              | string         | no        | Un borrador para la lista `listPrice` de una llamada anterior. Los precios modificados para esa lista se añaden a éste. 404 si no existe, 400 si ya no es un borrador.                                                                                                                                                                                                                                                    |
| `drafts`                     | array          | no        | Hasta 30 entradas `{ list, priceChangeId }` para que otras listas continúen con sus propios borradores.                                                                                                                                                                                                                                                                                                                   |
| `products[].sku`             | string         | yes       | Máximo 64 caracteres.                                                                                                                                                                                                                                                                                                                                                                                                     |
| `products[].name`            | string         | yes       | Máximo 200 caracteres.                                                                                                                                                                                                                                                                                                                                                                                                    |
| `products[].listPrice`       | integer o null | yes       | Unidades menores, de 0 a 1.000.000.000. `null` es precio bajo petición: un producto nuevo no tiene precio, uno existente mantiene el suyo.                                                                                                                                                                                                                                                                                |
| `products[].priceList`       | string o null  | no        | Nombre de una lista de precios existente para `listPrice`. Omita o `null` es Standard. Un nombre desconocido se rechaza.                                                                                                                                                                                                                                                                                                  |
| `products[].listPrices`      | array          | no        | Otras listas, como `{ list, price }`. `price` es unidades menores, o `null` para dejar esa lista sin cambios. Un nombre desconocido se rechaza.                                                                                                                                                                                                                                                                           |
| `products[].unitLabel`       | string         | yes       | Máximo 60 caracteres, por ejemplo, “Cada”.                                                                                                                                                                                                                                                                                                                                                                                |
| `products[].brand`           | string         | no        | Nombre de marca, máximo 120 caracteres. **Borrado si se omite.**                                                                                                                                                                                                                                                                                                                                                          |
| `products[].category`        | string         | no        | Nombre de categoría, máximo 120 caracteres. **Borrado si se omite.**                                                                                                                                                                                                                                                                                                                                                      |
| `products[].manufacturerRef` | string         | no        | Máximo 120 caracteres. **Borrado si se omite.**                                                                                                                                                                                                                                                                                                                                                                           |
| `products[].description`     | string         | no        | Máximo 4.000 caracteres. **Borrado si se omite.**                                                                                                                                                                                                                                                                                                                                                                         |
| `products[].availability`    | string         | no        | Uno de los valores de disponibilidad. Se mantiene si se omite; los productos nuevos comienzan como `not_tracked`.                                                                                                                                                                                                                                                                                                         |
| `products[].stockQty`        | integer o null | no        | Unidades en stock, de -1.000.000 a 1.000.000.000. Establece `availability` (ver el objeto de producto); un estado que envía junto con él se ignora, excepto `made_to_order`. Se mantiene si se omite, a menos que envíe un `availability` sin él: entonces el recuento se borra y se usa ese estado. `null` detiene el recuento; el `availability` enviado en la misma fila aplica, o el estado actual si no hay ninguno. |
| `products[].lowStockAt`      | integer o null | no        | Nivel de bajo stock para este producto, de 0. `null` usa el nivel del espacio de trabajo (Configuración, 5 para empezar). Se mantiene si se omite.                                                                                                                                                                                                                                                                        |
| `products[].leadTimeNote`    | string         | no        | Máximo 200 caracteres. Se mantiene si se omite.                                                                                                                                                                                                                                                                                                                                                                           |
| `products[].dealText`        | string         | no        | Máximo 200 caracteres, mostrado junto al precio. Se mantiene si se omite.                                                                                                                                                                                                                                                                                                                                                 |
| `products[].details`         | object         | no        | Máximo 30 columnas adicionales: nombres de hasta 60 caracteres, valores de hasta 500. Reemplaza todos los detalles al enviarlos; se mantiene si se omite.                                                                                                                                                                                                                                                                 |
| `products[].imageUrl`        | string         | no        | Una URL de imagen `https`. Se copia en TradeCatalog en segundo plano cuando cambia. Se mantiene si se omite.                                                                                                                                                                                                                                                                                                              |
| `products[].onlyFor`         | array          | no        | Máximo 200 códigos de cuenta o correos electrónicos de contacto. Hace que el producto sea visible **solo** para esos clientes, reemplazando cualquier lista anterior. Se mantiene si se omite.                                                                                                                                                                                                                            |

**Envía todos los campos que te importan:** `brand`, `category`, `manufacturerRef` y `description` se escriben como se envían, por lo que omitir uno lo borra. Envíe siempre el producto completo, como una fila de una hoja de cálculo.

Estos no se pueden cambiar a través de la API aún: hacer un producto inactivo, `minOrderQty`, eliminar una imagen, borrar una nota de plazo de entrega y volver a hacer visible un producto exclusivo para clientes. Hágalo en la aplicación.

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

| Campo                | Descripción                                                                                                   |
| -------------------- | ------------------------------------------------------------------------------------------------------------- |
| `created`            | Productos nuevos.                                                                                             |
| `updated`            | Productos existentes coincidentes por SKU.                                                                    |
| `priceChanges`       | Cuántos precios entraron en un borrador, en cada lista de la llamada.                                         |
| `priceChangeId`      | El borrador para la lista `listPrice`, o `null` si esa lista no tuvo cambios.                                 |
| `priceChangesByList` | Un `{ list, priceChangeId }` por cada lista que tuvo un cambio de precio en esta llamada.                     |
| `unknownCustomers`   | Valores en `onlyFor` que no coincidieron con ningún cliente (hasta 50). A esos clientes no se les dio acceso. |

Los cambios aparecen en el registro de actividad con el nombre de la clave, y las búsquedas se actualizan de inmediato.

## Marcas y categorías

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

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

Ambos devuelven cada fila, ordenadas por nombre, sin paginación. Use los ids con los filtros `brand` y `category` en `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" }
  ]
}
```

Para crear una marca o categoría, nómbrala en un producto en `PUT /products`.
