API: catálogo y sincronización
Productos y servicios por la API: crear, editar, borrar y sincronizar el catálogo entero con precios y stock desde tu sistema.
Revisado el 28 de octubre de 2026
Qué es el catálogo
Es la lista de productos y servicios que la IA ofrece y cobra por WhatsApp: lo que no está en el catálogo, el asistente no lo vende. Por la API se lee, se edita ítem a ítem o se sincroniza (entero o por partes) con tu sistema (tu inventario, tu tienda online, tu punto de venta), de forma que precios, existencias y disponibilidad estén siempre al día sin que nadie los copie a mano.
Un producto creado o cambiado por la API es igual que uno hecho a mano: se ve en el panel y en la app, cuenta para el tope de 5000 ítems y la IA lo usa desde el siguiente mensaje de un cliente.
El producto
{
"id": "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d",
"object": "product",
"external_id": "SKU-0042",
"barcode": "7591234567895",
"name": "Pizza margarita mediana",
"kind": "product",
"price": "9.50",
"currency": "USD",
"stock": 12,
"details": "Salsa de tomate, mozzarella y albahaca. 8 porciones.",
"available": true,
"daily_offer": false,
"discount": { "amount": "1.00", "until": "2026-10-05T03:59:59.000Z" },
"duration_minutes": null,
"applies_delivery": true,
"fractional": false,
"has_image": true,
"image_hash": "9f2c1b7e4a6d0c35e8b1f4a2d7c9e0b3a5f6d8c1e2b4a7f9d0c3e5b6a8f1d2c4",
"schedule": null,
"packaging_fee": null,
"delivery_surcharge": null,
"fulfillment": null,
"option_groups": null,
"variants": null,
"min_quantity": null,
"max_quantity": null,
"daily_limit": null,
"created_at": "2026-08-14T15:20:03.551Z",
"updated_at": "2026-09-29T12:00:41.090Z",
"deleted_at": null
}| Campo | Tipo | Qué es |
|---|---|---|
id | uuid | Id del ítem en AtiendeFácil. |
object | texto | Siempre product. |
external_id | texto | null | Tu id para ese ítem (tu SKU o código), hasta 120 caracteres. Es la llave de la sincronización. Único entre los ítems vivos del negocio. null en lo creado a mano. |
barcode | texto | null | Código de barras (EAN, UPC o uno interno), hasta 80 caracteres. Único entre los ítems vivos del negocio. Se guarda tal cual: 0750 y 750 son dos códigos distintos. null si no tiene. |
name | texto | El nombre que la IA le dice al cliente. Único en el catálogo sin distinguir mayúsculas. |
kind | texto | product (se vende en un pedido) o service (se agenda como cita). |
price | importe | null | Precio en la moneda base del negocio (la de currency). null = sin precio: la IA lo nombra pero no dice cuánto cuesta. |
currency | texto | La moneda base del negocio (USD o EUR). No se cambia por aquí. |
stock | número | null | Existencias, con hasta 3 decimales (12, 2.5). null = no controla stock (nunca se agota). 0 = agotado: la IA no lo vende. Con un número, cada venta lo descuenta. |
details | texto | null | Detalle: ingredientes, tallas, qué incluye. La IA lo usa para contestar preguntas. |
summary | texto | null | Descripción corta: la que sale en la tarjeta de la web del negocio. null = la tarjeta enseña el detalle. |
category | texto | null | Categoría («Bebidas»). La web del negocio agrupa la carta por ella. |
available | booleano | false = pausado: la IA lo trata como agotado. |
daily_offer | booleano | Oferta del día: la IA lo destaca aparte. |
discount | objeto | null | { amount, until }: monto (en moneda base) que se resta al precio hasta esa fecha. Pasada la fecha deja de aplicarse, aunque el objeto siga viniendo hasta que alguien lo quite. |
duration_minutes | entero | null | Solo servicios: lo que ocupa la cita en la agenda. null = 30 minutos. |
applies_delivery | booleano | Si lleva cobro y logística de envío. |
fractional | booleano | Se vende fraccionado (por peso o por medida): el cliente puede pedir cantidades con decimales, como medio kilo o 1,5 metros. Con false, el bot solo acepta unidades enteras y le pide al cliente que ajuste. |
has_image | booleano | Si tiene foto. Se sube desde el panel, la app o con PUT /products/{id}/image; la API no la descarga. |
image_hash | texto o null | SHA-256 del archivo que subiste por la API. Sirve para saber si la foto que tienes ya está puesta, sin volver a subirla. null si no hay foto o si se subió a mano desde el panel o la app. |
schedule | lista o null | Franjas en que se vende: [{ days, from, to }], con days de 0 (domingo) a 6 (sábado) y las horas en «HH:MM» de 24 h, hora de Venezuela. [] = siempre; null = como su categoría. |
packaging_fee | importe o null | Envase por unidad que se suma en el delivery. "0.00" = sin envase; null = como su categoría. |
delivery_surcharge | importe o null | Recargo por unidad en el delivery. null = como su categoría. |
fulfillment | texto o null | any, delivery (solo delivery) o pickup (solo retiro). null = como su categoría. |
option_groups | lista o null | Sus propios grupos de opciones: [{ name, min, max, external_id?, options: [{ name, price, external_id? }] }]. null = los de su categoría; [] = ninguno. El cliente elige de min a max opciones de cada grupo (min 0 = opcional) y el precio de cada una se suma al del producto. external_id es el identificador del grupo o de la opción en tu sistema (opcional, hasta 120 caracteres, sin repetirse en el mismo producto): vuelve en cada línea del pedido con lo que eligió el cliente. |
variants | lista o null | Tamaños con su precio: [{ name, price, external_id? }]. En un pedido, cada tamaño llega como su propia línea («Pizza margarita (Familiar)») con el product_id del producto, y en su variant el tamaño con su external_id. |
min_quantity | número o null | Mínimo por pedido; max_quantity, el máximo; daily_limit, cuántos se venden como mucho al día. null = sin tope. |
created_at | fecha | Cuándo se creó. |
updated_at | fecha | Último cambio. |
deleted_at | fecha | null | Siempre null en la API; trae la fecha en el webhook product.deleted. |
Listar y ver productos
Los ítems vivos (no borrados), de la A a la Z por nombre, paginado. Filtros opcionales:
| Parámetro | Qué filtra |
|---|---|
external_id | El ítem con ese external_id, exacto. Útil para saber si ya existe. |
barcode | El ítem con ese código de barras, exacto. Es la búsqueda de quien tiene el producto en la mano. |
kind | product o service. Otro valor se ignora. |
q | Busca el texto dentro del nombre, sin distinguir mayúsculas. También encuentra el ítem cuyo código de barras es exactamente ese texto. |
include_deleted=true | Incluye también los borrados, con la fecha en deleted_at. |
updated_after | Los que cambiaron desde esa fecha, del más viejo al más nuevo en vez de por nombre. Con include_deleted=true es la forma de sincronizar tu copia, borrados incluidos (ver sincronización incremental). |
Un ítem. Uno borrado responde 404.
Campos que puedes mandar
Los mismos nombres que en la respuesta. Lo que no mandas no se toca (al editar o sincronizar un ítem que ya existe) o toma su valor por defecto (al crearlo).
| Campo | Valores | Por defecto al crear |
|---|---|---|
name | Texto de 1 a 200 caracteres. Obligatorio al crear y al sincronizar. | — |
external_id | Texto de 1 a 120 caracteres, o null para quitarlo. Obligatorio al sincronizar. | null |
barcode | Texto de hasta 80 caracteres: letras, números, punto, guion, guion bajo, barra y espacio. null o vacío para quitarlo. Los espacios de los extremos se recortan. | null |
kind | product o service. | product |
price | Número mayor o igual a 0, o null. Se guarda con 2 decimales. | null |
stock | Número mayor o igual a 0, o null (no controla stock). Se guarda con 3 decimales como mucho; lo que sobre se redondea. | null |
details | Texto de hasta 2.000 caracteres, o null. | null |
summary | Texto de hasta 160 caracteres, o null. Los saltos de línea se quitan. | null |
category | Texto de hasta 60 caracteres, o null. Se guarda con la primera letra en mayúscula. | null |
available | true o false. | true |
daily_offer | true o false. | false |
discount | { "amount": 1, "until": "2026-10-04T23:59:59-04:00" } (monto ≥ 0 y fecha ISO 8601 con zona), o null para quitarlo. | null |
duration_minutes | Entero de 5 a 480, o null. Solo cuenta en servicios: en un product se borra siempre. | null |
applies_delivery | true o false. | true |
fractional | true o false. Pensado para productos, no para servicios. | false |
schedule | Hasta 4 franjas { days, from, to }; [] = siempre; null = como su categoría. | null |
packaging_fee, delivery_surcharge | Número de 0 a 1000 por unidad, o null (como su categoría). 0 = nada. | null |
fulfillment | any, delivery, pickup o null (como su categoría). | null |
option_groups | Hasta 6 grupos de hasta 12 opciones, o null (los de su categoría). max no puede pasar del número de opciones ni min de max. external_id es opcional en grupos y opciones, y no se puede repetir en el mismo producto. | null |
variants | Hasta 12 tamaños { name, price, external_id? }, o null. | null |
min_quantity, max_quantity, daily_limit | Número mayor que 0 (hasta 9999), o null (sin tope). | null |
En un servicio, fulfillment dice dónde se hace (delivery = solo a domicilio, pickup = solo en el local), delivery_surcharge es el recargo a domicilio y cada opción puede traer minutes (lo que alarga la cita). Al leerlo vienen además deposit_percent (su anticipo; null = el general) y employee_ids (quién lo hace; null = cualquiera).
Las reglas de venta pasan las mismas comprobaciones que el editor del panel: lo que allí no se puede guardar responde 400 invalid_body con el motivo en message («En «Contorno» el mínimo no puede ser mayor que el máximo.»).
curl -X PATCH https://api.atiendefacil.com/api/v1/products/a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d \
-H "Authorization: Bearer af_live_TU_CLAVE_AQUI" \
-H "Content-Type: application/json" \
-d '{
"variants": [
{ "name": "Personal", "price": 5, "external_id": "PZ-PER" },
{ "name": "Familiar", "price": 12, "external_id": "PZ-FAM" }
],
"packaging_fee": 0,
"option_groups": [
{ "name": "Extras", "min": 0, "max": 3, "external_id": "GRP-EXTRAS",
"options": [
{ "name": "Queso extra", "price": 1, "external_id": "EXT-QUESO" },
{ "name": "Tocineta", "price": 1.5, "external_id": "EXT-TOCINETA" }
] }
]
}'Crear, editar y borrar
Crea un ítem. Responde 201 con el producto.
curl -X POST https://api.atiendefacil.com/api/v1/products \
-H "Authorization: Bearer af_live_TU_CLAVE_AQUI" \
-H "Content-Type: application/json" \
-d '{
"external_id": "SKU-0042",
"barcode": "7591234567895",
"name": "Pizza margarita mediana",
"price": 9.5,
"stock": 12,
"details": "Salsa de tomate, mozzarella y albahaca. 8 porciones."
}'Un servicio lleva kind y, si quieres, su duración:
{
"external_id": "SRV-CORTE",
"name": "Corte de cabello",
"kind": "service",
"price": 8,
"duration_minutes": 45,
"applies_delivery": false
}Cambia solo los campos que mandas. Responde 200 con el producto. Si lo que mandas es igual a lo que ya tiene, no se escribe nada ni sale ningún webhook.
curl -X PATCH https://api.atiendefacil.com/api/v1/products/a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d \
-H "Authorization: Bearer af_live_TU_CLAVE_AQUI" \
-H "Content-Type: application/json" \
-d '{ "price": 10, "stock": 30, "discount": null }'Lo quita del catálogo, igual que Eliminar del catálogo en el panel: la IA deja de ofrecerlo y los pedidos viejos que lo incluían no cambian. No se puede deshacer por la API. Un ítem ya borrado responde 404.
{ "id": "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d", "object": "product", "deleted": true }Errores
| Respuesta | Cuándo |
|---|---|
| 400 invalid_body | Un campo no cumple lo de la tabla. El mensaje dice cuál: «price: …». |
| 404 not_found | El ítem no existe, es de otro negocio o ya está borrado. |
| 409 duplicate_name | Otro ítem ya se llama así (sin distinguir mayúsculas). |
| 409 duplicate_external_id | Otro ítem vivo ya tiene ese external_id. |
| 409 duplicate_barcode | Otro ítem vivo ya tiene ese código de barras. El mensaje dice cuál. |
| 409 catalog_full | El catálogo ya tiene 5000 ítems. |
Categorías y sus reglas
Lo mismo que la tarjeta Categorías del panel: horario, envase, recargo, modalidad y grupos de opciones que valen para todos los productos de esa categoría. La categoría de un producto es su campo category; aquí se le ponen reglas por nombre (sin distinguir mayúsculas).
Todas las categorías: las que tienen reglas y las que usan tus productos sin tenerlas (con id null), con product_count.
Crea o reemplaza las reglas de una categoría por su name: lo que no mandas queda vacío. Con previous_name la renombra, también en sus productos. Responde con la categoría.
curl -X PUT https://api.atiendefacil.com/api/v1/categories \
-H "Authorization: Bearer af_live_TU_CLAVE_AQUI" \
-H "Content-Type: application/json" \
-d '{
"name": "Almuerzos",
"schedule": [{ "days": [1, 2, 3, 4, 5], "from": "11:00", "to": "15:00" }],
"packaging_fee": 0.5,
"delivery_surcharge": null,
"fulfillment": null,
"option_groups": [
{ "name": "Contorno", "min": 1, "max": 2,
"options": [{ "name": "Arroz", "price": 0 }, { "name": "Papas fritas", "price": 1 }, { "name": "Ensalada", "price": 0 }] }
]
}'{
"object": "category",
"id": "b2c3d4e5-f6a7-4b8c-9d0e-1f2a3b4c5d6e",
"name": "Almuerzos",
"product_count": 6,
"schedule": [{ "days": [1, 2, 3, 4, 5], "from": "11:00", "to": "15:00" }],
"packaging_fee": 0.5,
"delivery_surcharge": null,
"fulfillment": null,
"option_groups": [
{ "name": "Contorno", "min": 1, "max": 2,
"options": [{ "name": "Arroz", "price": 0 }, { "name": "Papas fritas", "price": 1 }, { "name": "Ensalada", "price": 0 }] }
]
}Quita sus reglas; sus productos siguen en ella. Con &products=clear además los deja sin categoría.
La foto del producto
Es la foto que el asistente le manda al cliente que pregunta cómo es un producto.
| Petición | Qué hace |
|---|---|
PUT /products/{id}/image | Pone o reemplaza la foto. Cuerpo JSON: base64 (el archivo), mime_type o file_name (para saber el formato). JPG, PNG, WEBP o GIF, hasta 10 MB. Se comprime y se guarda en un formato que WhatsApp acepta. Devuelve el producto, con su image_hash. |
DELETE /products/{id}/image | Quita la foto. Si no tenía, no hace nada. Devuelve el producto. |
Para no subir la misma foto en cada sincronización, compara el SHA-256 de tu archivo con el image_hash del producto, que también viene en cada elemento de products[] de la respuesta de PUT /products/sync. Si alguien cambia la foto a mano desde el panel o la app, image_hash pasa a null.
| Error | Cuándo |
|---|---|
| 400 unsupported_file_type | No es JPG, PNG, WEBP ni GIF. |
| 400 empty_file | El archivo llegó vacío. |
| 413 file_too_large | Pasa de 10 MB. |
| 404 not_found | El producto no existe o se borró. |
Sincronizar el catálogo
Mandas tu lista y AtiendeFácil deja esos ítems igual que en tu sistema: crea lo nuevo, actualiza lo que cambió y, si lo pides, borra lo que ya no está. La lista puede ser el catálogo entero o solo una parte (lo que cambió): sin delete_missing, lo que no viene no se toca. Es la forma recomendada de tener precios y existencias al día.
curl -X PUT https://api.atiendefacil.com/api/v1/products/sync \
-H "Authorization: Bearer af_live_TU_CLAVE_AQUI" \
-H "Content-Type: application/json" \
-d @catalogo.jsonCon catalogo.json:
{
"delete_missing": false,
"products": [
{
"external_id": "SKU-0042",
"barcode": "7591234567895",
"name": "Pizza margarita mediana",
"price": 9.5,
"stock": 12,
"available": true
},
{
"external_id": "SKU-0043",
"name": "Pizza pepperoni mediana",
"price": 11,
"stock": 0
},
{
"external_id": "CHA-0019",
"name": "Queso blanco (kg)",
"price": 8,
"stock": 12.5,
"fractional": true,
"details": "Se vende por peso."
}
]
}| Campo | Qué es |
|---|---|
products | Lista de hasta 5000 ítems con los campos de siempre; external_id y name son obligatorios en cada uno. Puede ir vacía. El cuerpo puede pesar hasta 25 MB. |
delete_missing | true: todo lo que hay en el catálogo y no viene en la lista se borra. false (por defecto): no se borra nada. |
delete_external_ids | Lista de external_id a borrar en la misma petición. Borra solo esos: lo que el dueño cargó a mano no se toca. Uno que no existe se ignora; uno que también viene en products es 400 invalid_body. |
Respuesta (200):
{
"created": 1,
"updated": 1,
"unchanged": 1,
"deleted": 0,
"products": [
{ "external_id": "SKU-0042", "id": "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d", "action": "unchanged", "image_hash": "9f2c1b7e4a6d0c35e8b1f4a2d7c9e0b3a5f6d8c1e2b4a7f9d0c3e5b6a8f1d2c4" },
{ "external_id": "SKU-0043", "id": "b2c3d4e5-f6a7-4b8c-9d0e-1f2a3b4c5d6e", "action": "updated", "image_hash": null },
{ "external_id": "CHA-0019", "id": "c3d4e5f6-a7b8-4c9d-8e0f-2a3b4c5d6e7f", "action": "created", "image_hash": null }
],
"deleted_products": []
}| Campo | Qué es |
|---|---|
created, updated, unchanged, deleted | Cuántos ítems se crearon, cambiaron, quedaron igual y se borraron. |
products[] | Uno por cada ítem de tu lista, en el mismo orden: tu external_id, el id en AtiendeFácil y la action (created, updated o unchanged), y el image_hash de la foto que tiene. Guarda el id si lo necesitas. |
deleted_products[] | Lo que se borró por delete_missing o delete_external_ids: id, external_id (null si era un ítem hecho a mano) y name. |
Cómo se empareja cada ítem
Para cada ítem de tu lista, en este orden:
Por
external_idSi hay un ítem vivo con ese external_id, es ese: se actualiza con lo que mandas.
Por código de barras, entre los que no tienen external_id
Si no, y tu ítem trae
barcode, se busca un ítem sin external_id con ese mismo código. Si lo hay, se «adopta»: se le pone tu external_id y se actualiza. Va antes que el nombre porque el código no cambia de un sistema a otro; el nombre, sí.Por nombre, entre los que no tienen external_id
Si no, se busca un ítem sin external_id con el mismo nombre (sin distinguir mayúsculas ni espacios de sobra) y se adopta igual. Estos dos pasos son lo que permite que la primera sincronización de un catálogo que el dueño cargó a mano no lo duplique.
Si no, se crea
Un ítem nuevo, con los valores por defecto para lo que no mandes.
- Un ítem que ya existe solo cambia en los campos que mandas. Si todo es igual, queda
unchangedy no se escribe nada. - Un ítem que se borró antes no se recupera: si vuelve a venir su external_id, se crea uno nuevo, con otro id.
- Dos ítems pueden intercambiarse el nombre, o el código de barras, en la misma sincronización.
- Si un nombre de tu lista ya lo usa otro ítem que se queda en el catálogo (con otro external_id, o sin él y ya se adoptó otro), responde
409 duplicate_name. Renómbralo o usadelete_missing. - Lo mismo con el código de barras: si un
barcodede tu lista ya lo tiene otro ítem que se queda en el catálogo y no vino en la lista, responde409 duplicate_barcode.
Todo o nada
La sincronización corre en una sola transacción: si algo falla (un nombre repetido, el tope de ítems), no se aplica nada y el catálogo queda como estaba. Antes de tocar nada se rechaza con 400 invalid_body una lista que repite un external_id, un nombre o un código de barras.
delete_missing
Úsalo solo si tu sistema es la fuente de verdad del catálogo entero, y avísale al dueño de que lo que cargue a mano se borrará en la siguiente sincronización. La respuesta dice qué se borró:
{
"created": 0,
"updated": 0,
"unchanged": 3,
"deleted": 1,
"products": [ … ],
"deleted_products": [
{ "id": "d4e5f6a7-b8c9-4d0e-9f1a-3b4c5d6e7f8a", "external_id": null, "name": "Pizza hawaiana" }
]
}Deshacer una sincronización
Cada PUT /products/sync que cambie algo deja una copia de seguridad. Si una carga salió mal, el dueño la revierte desde el panel y la app en Ajustes → Catálogo → «Deshacer una sincronización por API»: devuelve el catálogo a como estaba justo antes de esa carga —recupera lo que borró y deshace lo que cambió— sin tocar lo que no entró en ella. Se guardan las últimas sincronizaciones; cada una se puede revertir una sola vez, y lo que el dueño haya tocado después de la carga no se conserva al revertir. Cada carga sale con su fecha y hora y lo que hizo («3 nuevos · 2 cambiados · 1 borrado»); en la app se abre como una pantalla propia, Sincronizaciones por API.
Precio, stock y descuentos
priceva en la moneda base del negocio (mírala enGET /rateo en elcurrencyde cualquier producto). La conversión a bolívares la hace AtiendeFácil con la tasa del negocio.- Manda precios con dos decimales como mucho. Se guardan redondeados a dos; si mandas más, el ítem cuenta como cambiado en cada sincronización.
stock: nullapaga el control de existencias de ese ítem;stock: 0lo marca agotado. Si no mandasstock, no se toca.stockadmite hasta 3 decimales, para lo que se vende por peso o por medida (12.5,0.75). Lo que sobre se redondea.- Que las existencias lleven decimales no basta para que el cliente pueda pedir medio kilo: eso lo decide
fractional: trueen ese producto. Sin él, el bot solo toma unidades enteras. Con él, la cantidad de la línea del pedido (items[].quantity) puede llevar decimales y las existencias se descuentan con ellos. - Cada venta por WhatsApp baja el stock en AtiendeFácil. Si tu sincronización manda existencias, pisa ese número: asegúrate de que tu sistema ya descontó esas ventas (escucha
order.createdoproduct.updated) o deja de mandarstocky que lo lleve AtiendeFácil. discount: nullquita el descuento. Para uno nuevo manda monto y fecha de fin.- Los servicios llevan
kind: "service"y, si quieres,duration_minutes. En modo Pedidos la IA no ofrece servicios.
Cada cuánto sincronizar
- Cuando algo cambie en tu sistema, o cada 5 a 15 minutos si no tienes cómo enterarte. El límite es de 10 sincronizaciones por minuto por clave; no hace falta acercarse.
- Para cambiar uno o dos ítems (un precio, un agotado),
PATCH /products/{id}es más ligero que mandar la lista entera. Para unos cuantos, manda aPUT /products/syncsolo esos, sindelete_missing. - Cada ítem creado, cambiado o borrado dispara su webhook
product.created,product.updatedoproduct.deleted. Los que quedanunchanged, no.
Errores
| Respuesta | Cuándo |
|---|---|
| 400 invalid_body | Un ítem no cumple los campos (el mensaje dice cuál: «products.3.price: …»), la lista pasa de 5000 o repite un external_id, un nombre o un código de barras. |
| 409 duplicate_name | Un nombre de la lista choca con otro ítem que se queda. |
| 409 duplicate_external_id | Otra petición creó ese external_id al mismo tiempo. Reintenta. |
| 409 duplicate_barcode | Un código de barras de la lista ya lo tiene otro ítem que se queda y no vino en la lista (o lo creó otra petición al mismo tiempo). |
| 409 catalog_full | El catálogo quedaría con más de 5000 ítems. El mensaje dice cuántos; usa delete_missing o borra algunos. |
| 413 | El cuerpo pasa de 25 MB. |
| 429 | Más de 10 sincronizaciones en un minuto. |
Enterarte de los cambios del catálogo
El catálogo también cambia desde el panel, la app y con cada venta (el stock). Para que tu sistema se entere, suscríbete a los webhooks product.*. En product.updated viene qué campos cambiaron y, si cambió el stock, el anterior:
{
"object": { "id": "a1b2c3d4-…", "object": "product", "external_id": "SKU-0042", "barcode": "7591234567895", "price": "10.00", "stock": 11, "deleted_at": null, … },
"changes": ["price", "stock"],
"previous_stock": 12,
"source": "api:9f1c2b3a-4d5e-4f60-8a7b-1c2d3e4f5a6b"
}Todos los ejemplos, en Webhooks.
Preguntas frecuentes
¿La primera sincronización duplica lo que el dueño ya tenía cargado?
No, si los códigos de barras o los nombres coinciden: los ítems sin external_id se adoptan por código de barras y, si no, por nombre. Lo que no coincida en ninguno de los dos se crea aparte; revisa la respuesta y, si hace falta, renombra o usa delete_missing.
Mi catálogo tiene miles de productos. ¿Tengo que mandarlos todos cada vez?
No. La primera carga puede ir entera en una sola petición (hasta 5000 ítems y 25 MB). Después basta con mandar lo que cambió, sin delete_missing: lo que no viene en la lista no se toca.
¿Puedo subir fotos por la API?
Sí, con PUT /products/{id}/image. La API no las descarga: solo dice si hay una (has_image) y su huella (image_hash).
¿Puedo cambiar la moneda base por la API?
No. Se elige en Ajustes → Tasa de cambio. Por la API se cambia la tasa, no la moneda (ver Tasa de cambio).
