Docs

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

El producto

json
{
  "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
}
CampoTipoQué es
iduuidId del ítem en AtiendeFácil.
objecttextoSiempre product.
external_idtexto | nullTu 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.
barcodetexto | nullCó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.
nametextoEl nombre que la IA le dice al cliente. Único en el catálogo sin distinguir mayúsculas.
kindtextoproduct (se vende en un pedido) o service (se agenda como cita).
priceimporte | nullPrecio en la moneda base del negocio (la de currency). null = sin precio: la IA lo nombra pero no dice cuánto cuesta.
currencytextoLa moneda base del negocio (USD o EUR). No se cambia por aquí.
stocknúmero | nullExistencias, 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.
detailstexto | nullDetalle: ingredientes, tallas, qué incluye. La IA lo usa para contestar preguntas.
summarytexto | nullDescripción corta: la que sale en la tarjeta de la web del negocio. null = la tarjeta enseña el detalle.
categorytexto | nullCategoría («Bebidas»). La web del negocio agrupa la carta por ella.
availablebooleanofalse = pausado: la IA lo trata como agotado.
daily_offerbooleanoOferta del día: la IA lo destaca aparte.
discountobjeto | 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_minutesentero | nullSolo servicios: lo que ocupa la cita en la agenda. null = 30 minutos.
applies_deliverybooleanoSi lleva cobro y logística de envío.
fractionalbooleanoSe 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_imagebooleanoSi tiene foto. Se sube desde el panel, la app o con PUT /products/{id}/image; la API no la descarga.
image_hashtexto o nullSHA-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.
schedulelista o nullFranjas 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_feeimporte o nullEnvase por unidad que se suma en el delivery. "0.00" = sin envase; null = como su categoría.
delivery_surchargeimporte o nullRecargo por unidad en el delivery. null = como su categoría.
fulfillmenttexto o nullany, delivery (solo delivery) o pickup (solo retiro). null = como su categoría.
option_groupslista o nullSus 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.
variantslista o nullTamañ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_quantitynúmero o nullMínimo por pedido; max_quantity, el máximo; daily_limit, cuántos se venden como mucho al día. null = sin tope.
created_atfechaCuándo se creó.
updated_atfechaÚltimo cambio.
deleted_atfecha | nullSiempre null en la API; trae la fecha en el webhook product.deleted.

Listar y ver productos

GET/products

Los ítems vivos (no borrados), de la A a la Z por nombre, paginado. Filtros opcionales:

ParámetroQué filtra
external_idEl ítem con ese external_id, exacto. Útil para saber si ya existe.
barcodeEl ítem con ese código de barras, exacto. Es la búsqueda de quien tiene el producto en la mano.
kindproduct o service. Otro valor se ignora.
qBusca 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=trueIncluye también los borrados, con la fecha en deleted_at.
updated_afterLos 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).
GET/products/{id}

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).

CampoValoresPor defecto al crear
nameTexto de 1 a 200 caracteres. Obligatorio al crear y al sincronizar.—
external_idTexto de 1 a 120 caracteres, o null para quitarlo. Obligatorio al sincronizar.null
barcodeTexto 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
kindproduct o service.product
priceNúmero mayor o igual a 0, o null. Se guarda con 2 decimales.null
stockNúmero mayor o igual a 0, o null (no controla stock). Se guarda con 3 decimales como mucho; lo que sobre se redondea.null
detailsTexto de hasta 2.000 caracteres, o null.null
summaryTexto de hasta 160 caracteres, o null. Los saltos de línea se quitan.null
categoryTexto de hasta 60 caracteres, o null. Se guarda con la primera letra en mayúscula.null
availabletrue o false.true
daily_offertrue 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_minutesEntero de 5 a 480, o null. Solo cuenta en servicios: en un product se borra siempre.null
applies_deliverytrue o false.true
fractionaltrue o false. Pensado para productos, no para servicios.false
scheduleHasta 4 franjas { days, from, to }; [] = siempre; null = como su categoría.null
packaging_fee, delivery_surchargeNúmero de 0 a 1000 por unidad, o null (como su categoría). 0 = nada.null
fulfillmentany, delivery, pickup o null (como su categoría).null
option_groupsHasta 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
variantsHasta 12 tamaños { name, price, external_id? }, o null.null
min_quantity, max_quantity, daily_limitNú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
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

POST/products

Crea un ítem. Responde 201 con el producto.

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

json
{
  "external_id": "SRV-CORTE",
  "name": "Corte de cabello",
  "kind": "service",
  "price": 8,
  "duration_minutes": 45,
  "applies_delivery": false
}
PATCH/products/{id}

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
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 }'
DELETE/products/{id}

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.

json
{ "id": "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d", "object": "product", "deleted": true }

Errores

RespuestaCuándo
400 invalid_bodyUn campo no cumple lo de la tabla. El mensaje dice cuál: «price: …».
404 not_foundEl ítem no existe, es de otro negocio o ya está borrado.
409 duplicate_nameOtro ítem ya se llama así (sin distinguir mayúsculas).
409 duplicate_external_idOtro ítem vivo ya tiene ese external_id.
409 duplicate_barcodeOtro ítem vivo ya tiene ese código de barras. El mensaje dice cuál.
409 catalog_fullEl 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).

GET/categories

Todas las categorías: las que tienen reglas y las que usan tus productos sin tenerlas (con id null), con product_count.

PUT/categories

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
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 }] }
    ]
  }'
json
{
  "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 }] }
  ]
}
DELETE/categories?name=Almuerzos

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ónQué hace
PUT /products/{id}/imagePone 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}/imageQuita 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.

ErrorCuándo
400 unsupported_file_typeNo es JPG, PNG, WEBP ni GIF.
400 empty_fileEl archivo llegó vacío.
413 file_too_largePasa de 10 MB.
404 not_foundEl producto no existe o se borró.

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).

¿No encontraste lo que buscabas?

Escríbenos y te responde una persona del equipo.