Primeros pasos
La API de Yummy Marketplace conecta tu sistema (ERP, POS, tienda propia o CRM) con tu tienda en Yummy: sincronizas stock y precios, recibes tus ventas en tiempo real, concilias tus pagos y atiendes el chat desde tus propias herramientas.
- Pide la activación. Escríbele a tu contacto en Yummy o usa el botón Soporte del Portal de Comercios y pide la integración por API. Al activarla recibes la URL base de la API.
- Recibe tu API Key de pruebas. El equipo de Yummy crea la key con los permisos que necesitas. Las keys de pruebas empiezan con
mk_tst_. - Haz tu primera llamada. Consulta tu stock para ver los identificadores de tus variantes:
curl -X GET "{BASE_URL}/seller-api/v1/stock?limit=10" \
-H "x-api-key: mk_tst_tu_api_key_aqui"
- Configura tu webhook (opcional). Si le das a Yummy la URL de tu servidor, recibes cada venta al momento. El secreto para verificar las firmas se muestra una sola vez, al crear la key.
- Pasa a producción con una key
mk_prd_.
Tu API Key es secreta. Nunca la compartas en código público ni en repositorios, y no la uses en aplicaciones del lado del cliente (frontend o apps móviles). Úsala solo desde tu servidor.
Autenticación
Todas las solicitudes deben incluir tu API Key en el header x-api-key:
x-api-key: mk_tst_aBcDeFgHiJkLmNoPqRsTuVwXyZ123456
Formato de la API Key
| Componente | Valor | Descripción |
|---|---|---|
| Prefijo | mk |
Identifica que es una key de Yummy Marketplace. |
| Entorno | tst / prd |
Indica si es de pruebas o de producción. |
| Secreto | 32 caracteres | Parte secreta de la key. |
Permisos
Cada API Key tiene permisos específicos, asignados por el equipo de Yummy:
| Permiso | Permite |
|---|---|
stock:write |
Sincronizar y actualizar stock (POST /stock/sync). |
stock:read |
Consultar el stock actual (GET /stock). |
price:write |
Actualizar precios (PATCH /sku). |
orders:read |
Consultar órdenes para reconciliación (GET /orders). |
chat:read |
Leer el chat por REST (GET /chat/messages, GET /chat/:chatId/messages) y recibir el webhook chat.message.received. |
chat:write |
Responder en el chat (POST /chat/:chatId/messages). |
Si tu key no tiene el permiso que pide un endpoint, recibes un error 403.
Permisos granulares: una key puede tener solo stock:write y stock:read sin poder tocar precios, solo price:write sin acceso al stock, o solo orders:read. Puedes tener hasta 5 keys con combinaciones distintas para distintos sistemas.
Los permisos no cambian después de crear la key. Al editar una key solo se pueden cambiar su nombre, su
webhookUrly si está activa. Por eso las keys existentes no reciben permisos de chat automáticamente: para usar el chat en un comercio que ya tiene una key, hay que crear una nueva conchat:read/chat:write. Así una key de stock o precios nunca gana acceso al chat sin una acción explícita.
Conceptos clave
Identificadores de variante
| Identificador | Uso | ¿Único? |
|---|---|---|
externalVariantId |
Identificador principal de la variante. Se usa para sincronizar stock y actualizar precios, y aparece en las respuestas y en los webhooks. Si no lo configuraste en tu CSV, se generó a partir de tu SKU. | Sí, por producto |
sku |
Código SKU de la variante. Sirve para sincronizar stock y actualizar precios siempre que sea único entre tus productos. Si se repite, debes usar externalVariantId. |
No necesariamente |
externalId |
ID del producto en tu sistema. Aparece en las respuestas como referencia cruzada. | No necesariamente |
Resolución inteligente de variantes
Los endpoints de escritura (POST /stock/sync y PATCH /sku) aceptan externalVariantId o sku para identificar la variante (al menos uno):
- Si envías
externalVariantId, se busca solo por ese campo. Si no existe entre tus productos, recibesSELLER_API_VARIANT_NOT_FOUND, aunque también hayas enviado unskuválido: no se hace una segunda búsqueda por SKU. - Si envías solo
sku, se busca por SKU entre los productos de tu comercio:- 1 resultado: se usa esa variante (el caso más común si no configuraste
externalVariantId). - 0 resultados:
SELLER_API_VARIANT_NOT_FOUND(también si la variante es de otro comercio). - 2 o más resultados:
SELLER_API_VARIANT_AMBIGUOUS_SKU, con un mensaje que te pide usarexternalVariantId.
- 1 resultado: se usa esa variante (el caso más común si no configuraste
¿Cómo sé mi
externalVariantId? ConsultaGET /stock: cada variante de la respuesta trae suexternalVariantId. Si no configuraste uno a mano, es igual a tu SKU.
Sincronizar stock
/seller-api/v1/stock/syncPermiso: stock:write
Envía una o más actualizaciones de stock en un solo request. Cada item se procesa por separado: si uno falla, los demás se procesan normalmente.
Headers
| Header | Valor | Requerido |
|---|---|---|
x-api-key |
Tu API Key | Sí |
Content-Type |
application/json |
Sí |
Request
{
"items": [
{
"externalVariantId": "CAMISA-AZUL-M",
"branchExternalId": "SUC-001",
"stock": 25,
"operation": "set"
}
]
}
También puedes identificar la variante por SKU:
{
"items": [
{
"sku": "CAMISA-AZUL-M",
"branchExternalId": "SUC-001",
"stock": 25,
"operation": "set"
}
]
}
| Campo | Tipo | Requerido | Descripción |
|---|---|---|---|
items |
Array | Sí | Lista de actualizaciones (mín. 1, máx. 100). |
items[].externalVariantId |
String | No* | Identificador externo de la variante. Máx. 200 caracteres. Tiene prioridad sobre sku. |
items[].sku |
String | No* | SKU de la variante. Máx. 100 caracteres. Si se repite entre productos, devuelve SELLER_API_VARIANT_AMBIGUOUS_SKU. |
items[].branchExternalId |
String | No** | ID de la sucursal en tu sistema. Máx. 100 caracteres. |
items[].branchId |
String | No** | ID interno de Yummy para la sucursal. |
items[].stock |
Integer | Sí | Cantidad de unidades. Debe ser ≥ 0. |
items[].operation |
String | Sí | set, increment o decrement. |
* Debes enviar al menos uno de externalVariantId o sku. Si envías los dos, se usa externalVariantId.
** Puedes identificar la sucursal con branchExternalId (tu código) o branchId (ID de Yummy). Si no envías ninguno, se usa la sucursal principal del comercio.
Operaciones
| Operación | Qué hace | Cuándo usarla |
|---|---|---|
set |
Establece el stock en la cantidad exacta. Sobrescribe el valor anterior. | Sincronización diaria completa desde tu ERP: "tengo 25 unidades, punto". |
increment |
Suma la cantidad al stock actual. | Recepción de mercancía o devolución: "llegaron 10 unidades más". |
decrement |
Resta la cantidad del stock actual. Si no alcanza, lo deja en 0 y devuelve un warning. | Venta en tienda física u otro canal: "se vendieron 5 en el POS". |
Atomicidad: todas las operaciones son atómicas. Si dos sistemas envían actualizaciones al mismo tiempo, no se pisan: cada una ve el stock que dejó la anterior.
Respuesta (200)
{
"batchId": "batch_a1b2c3d4",
"status": "completed",
"results": {
"total": 3,
"succeeded": 2,
"failed": 1,
"warnings": 1,
"items": [
{
"externalVariantId": "CAMISA-AZUL-M",
"externalId": "PROD-001",
"variantId": "665a1b2c3d4e5f...",
"postId": "664b2c3d4e5f6a...",
"sku": "CAMISA-AZUL-M",
"status": "ok",
"previousStock": 10,
"newStock": 25
},
{
"externalVariantId": "PANTALON-NEGRO-L",
"sku": "PANTALON-NEGRO-L",
"status": "warning",
"previousStock": 5,
"newStock": 0,
"warning": "INSUFFICIENT_STOCK: requested -99999 but only 5 available. Stock set to 0."
},
{
"externalVariantId": "ID-INEXISTENTE",
"status": "error",
"errorCode": "SELLER_API_VARIANT_NOT_FOUND",
"errorMessage": "Variant not found"
}
]
}
}
Estados por item
| Status | Significado | Campos adicionales |
|---|---|---|
ok |
Stock actualizado correctamente. | previousStock, newStock, externalVariantId, externalId, variantId, postId, sku |
warning |
Se procesó con una advertencia (ej.: el stock quedó en 0). | previousStock, newStock, warning |
error |
No se pudo procesar este item. | errorCode, errorMessage |
Consultar stock
/seller-api/v1/stockPermiso: stock:read
Consulta el stock actual de tus productos en Yummy. Solo ves el stock de tus propios productos.
Parámetros de consulta
| Parámetro | Tipo | Requerido | Descripción |
|---|---|---|---|
externalVariantId |
String | No | Filtra por identificador externo de variante. Máx. 200 caracteres. |
sku |
String | No | Filtra por SKU. Máx. 100 caracteres. Puede devolver varios resultados si el SKU se repite. |
externalId |
String | No | Filtra por ID externo de producto. Máx. 200 caracteres. |
branchExternalId |
String | No | Filtra por ID externo de sucursal. |
branchId |
String | No | Filtra por ID interno de sucursal. |
postId |
String | No | Filtra por ID de producto/publicación. |
page |
Integer | No | Número de página (por defecto 1). |
limit |
Integer | No | Resultados por página, de 1 a 100 (por defecto 20). |
GET /seller-api/v1/stock?sku=CAMISA-AZUL-M&limit=10
Respuesta (200)
{
"items": [
{
"externalVariantId": "CAMISA-AZUL-M",
"externalId": "PROD-001",
"sku": "CAMISA-AZUL-M",
"variantId": "665a1b2c3d4e5f...",
"postId": "664b2c3d4e5f6a...",
"price": 25.00,
"branches": [
{
"branchId": "663c3d4e5f6a7b...",
"branchName": "Sucursal Centro",
"branchExternalId": "SUC-001",
"stock": 25
},
{
"branchId": "663d4e5f6a7b8c...",
"branchName": "Sucursal Este",
"branchExternalId": "SUC-002",
"stock": 12
}
]
}
],
"pagination": {
"currentPage": 1,
"totalPages": 1,
"totalCount": 1,
"limit": 10
}
}
Actualizar precios
/seller-api/v1/skuPermiso: price:write
Actualiza el precio de una o más variantes. Cada item se procesa por separado. Los headers son los mismos que en Sincronizar stock.
Request
{
"items": [
{
"externalVariantId": "CAMISA-AZUL-M",
"price": 29.99
}
]
}
También puedes identificar la variante por SKU:
{
"items": [
{
"sku": "CAMISA-AZUL-M",
"price": 29.99
}
]
}
| Campo | Tipo | Requerido | Descripción |
|---|---|---|---|
items |
Array | Sí | Lista de actualizaciones (mín. 1, máx. 100). |
items[].externalVariantId |
String | No* | Identificador externo de la variante. Máx. 200 caracteres. Tiene prioridad sobre sku. |
items[].sku |
String | No* | SKU de la variante. Máx. 100 caracteres. Error si el SKU es ambiguo. |
items[].price |
Number | Sí | Nuevo precio de la variante en USD. Debe ser ≥ 0. |
* Debes enviar al menos uno de externalVariantId o sku. Si envías los dos, se usa externalVariantId.
Respuesta (200)
{
"batchId": "batch_a1b2c3d4",
"status": "completed",
"results": {
"total": 2,
"succeeded": 1,
"failed": 1,
"items": [
{
"externalVariantId": "CAMISA-AZUL-M",
"externalId": "PROD-001",
"variantId": "665a1b2c3d4e5f...",
"postId": "664b2c3d4e5f6a...",
"sku": "CAMISA-AZUL-M",
"status": "ok",
"previousPrice": 25.00,
"newPrice": 29.99
},
{
"externalVariantId": "ID-INEXISTENTE",
"status": "error",
"errorCode": "SELLER_API_VARIANT_NOT_FOUND",
"errorMessage": "Variant not found"
}
]
}
}
Nota de diseño:
/skues el punto de extensión para futuras actualizaciones de datos de la variante (descripción, atributos, etc.). El precio vive en la variante: es único por variante, sin importar la sucursal.
Consultar órdenes
/seller-api/v1/ordersPermiso: orders:read
Consulta las órdenes aprobadas de tu comercio en un rango de fechas. Devuelve el mismo formato que el webhook order.items.sold, así puedes usar el mismo parser para los dos flujos.
¿Para qué sirve? Si tu webhook falla (timeout, servidor caído, error de red), consulta este endpoint para recuperar las órdenes que no recibiste y reconciliar tu sistema.
Parámetros de consulta
| Parámetro | Tipo | Requerido | Descripción |
|---|---|---|---|
startDate |
String (ISO 8601) | Sí | Inicio del rango (ej.: 2026-07-01). |
endDate |
String (ISO 8601) | Sí | Fin del rango (ej.: 2026-07-08). |
page |
Integer | No | Número de página (por defecto 1). |
limit |
Integer | No | Resultados por página, de 1 a 100 (por defecto 20). |
Máximo 7 días por consulta. El rango entre
startDateyendDateno puede pasar de 7 días. Si necesitas más, haz varias consultas con rangos consecutivos.
GET /seller-api/v1/orders?startDate=2026-07-01&endDate=2026-07-08&page=1&limit=20
Respuesta (200)
{
"orders": [
{
"orderId": "667a1b2c-3d4e-5f6a-7b8c-9d0e1f2a3b4c",
"timestamp": "2026-07-08T15:45:00.000Z",
"buyer": {
"buyerId": "usr_88776655",
"fullName": "Juan Pérez",
"firstName": "Juan",
"lastName": "Pérez",
"email": "juan.perez@email.com",
"phone": "+584121234567",
"documentType": "V",
"documentNumber": "12345678",
"documentVerified": true,
"fiscalAddress": null
},
"shippingAddress": {
"recipientName": "Juan Pérez",
"street": "Av. Francisco de Miranda, Edif. Galipán, Piso 4",
"city": "Caracas",
"state": "Distrito Capital",
"zipCode": ""
},
"shipping": {
"couponUrl": "https://example.com/coupon/abc123",
"guideNumber": "123456789",
"shippingMethod": "standard"
},
"payment": {
"paymentMethod": "pago-movil",
"usedCashea": false,
"paymentIntent": {
"bank": "0102",
"phone": "+584121234567",
"transactionPhone": "+584121234567",
"dni": "V12345678",
"documentType": "V",
"documentNumber": "12345678"
}
},
"disbursement": {
"status": "paid",
"reference": "20260722150502",
"currency": "VES",
"amount": 2275.0,
"transactionId": "9fdfb05d-06b3-53a9-bf96-d3fcc0e0fbc2",
"paidAt": "2026-07-22T21:05:02.070Z"
},
"financials": {
"baseCurrency": "USD",
"exchangeRate": 45.5,
"subtotal": 50.0,
"subtotalBs": 2275.0,
"shippingCost": 5.0,
"shippingCostBs": 227.5,
"freeShipping": false,
"tax": 0,
"discount": 0,
"total": 55.0,
"totalBs": 2502.5,
"deductions": [
{ "type": "shipping", "amount": 5.0, "amountBs": 227.5 }
],
"payout": 50.0,
"payoutBs": 2275.0
},
"items": [
{
"externalVariantId": "ZAPATO-NEGRO-42",
"externalId": "PROD-001",
"sku": "ZAPATO-NEGRO-42",
"variantId": "665a1b2c3d4e5f...",
"branchId": "663c3d4e5f6a7b...",
"branchExternalId": "SUC-001",
"quantity": 2,
"unitPrice": 25.0,
"totalPrice": 50.0,
"totalPriceBs": 2275.0,
"stockAfter": 12
}
]
}
],
"pagination": {
"currentPage": 1,
"totalPages": 3,
"totalCount": 52,
"limit": 20
}
}
Campo
disbursement: viene enGET /ordersy en el webhookorder.disbursement.completed, pero no enorder.items.sold, porque al momento de la venta el desembolso todavía no se ha procesado. Trae la referencia bancaria del pago móvil enviado a tu comercio: úsala para conciliar con tu estado de cuenta. Sustatuspuede serpending,paid,failed,processingocancelled.
Cada objeto de orders tiene la misma estructura que el payload del webhook order.items.sold, más el campo disbursement. La única otra diferencia es que las órdenes de este endpoint no traen el campo event (solo lo llevan los webhooks).
Chat y CRM
Atiende el chat entre compradores y tu comercio desde tu propio CRM, sin depender del Portal ni de la app. Funciona con dos piezas, como en Stripe, GitHub o Shopify:
- El webhook
chat.message.receivedes la señal en tiempo real, con el mejor esfuerzo posible. - Los endpoints REST con cursor son la red de seguridad: si el webhook falla, recuperas lo que faltó con
?since={cursor}.
El cursor es el
messageId, no la fecha. Está ordenado y siempre crece, así que nunca pierdes ni duplicas mensajes aunque dos tengan la misma hora. Eltimestampes solo informativo. Para saber quién escribió, usadirection:inboundes del comprador youtboundes de tu comercio.
Listar mensajes
/seller-api/v1/chat/messagesPermiso: chat:read
Lista todos los mensajes de tu comercio (todos sus chats) en orden de cursor. Sirve para ponerte al día después de una caída del webhook.
| Parámetro | Tipo | Requerido | Descripción |
|---|---|---|---|
since |
String (24 caracteres hex) | No | Devuelve los mensajes con messageId mayor que since. Omítelo para empezar desde el inicio. |
limit |
Integer | No | Mensajes por página, de 1 a 100 (por defecto 50). |
{
"messages": [
{
"id": "665a1b2c3d4e5f6a7b8c9d0e",
"chatId": "664b2c3d4e5f6a7b8c9d0e1f",
"content": "Hola, sigue disponible el zapato talla 42?",
"files": [],
"from": "usr_88776655",
"to": "cmr_11223344",
"direction": "inbound",
"isBot": false,
"timestamp": "2026-09-01T15:45:00.000Z",
"cursor": "665a1b2c3d4e5f6a7b8c9d0e"
}
],
"nextCursor": null
}
nextCursor es el messageId del último mensaje solo cuando la página viene llena (puede haber más); es null cuando ya no quedan mensajes. Pagina repitiendo con ?since={nextCursor} mientras no sea null.
| Campo | Tipo | Descripción |
|---|---|---|
id |
String | ID del mensaje (igual a cursor). |
chatId |
String | Chat al que pertenece. |
content |
String | Texto plano. "" si el mensaje era solo de archivos o es un mensaje histórico sin texto. |
files |
Array | Adjuntos [{ "uri", "type" }]. [] si no hay. |
from / to |
String | Autor y destinatario del mensaje. |
direction |
String | inbound (del comprador) u outbound (de tu comercio). |
isBot |
Boolean | true si es un mensaje automático (envío MRW, tracking QR, recordatorios). |
timestamp |
String (ISO 8601) | Fecha de creación (informativa). |
cursor |
String | Cursor de este mensaje (igual a id). |
Historial de un chat
/seller-api/v1/chat/:chatId/messagesPermiso: chat:read
Igual que el anterior, pero de un solo chat. Misma respuesta y misma paginación por cursor; chatId va en la ruta y since / limit funcionan igual.
Si el
chatIdno existe o no es de tu comercio, recibes404 SELLER_API_CHAT_NOT_FOUND.
Responder en un chat
/seller-api/v1/chat/:chatId/messagesPermiso: chat:write
Responde en un chat. Usa el mismo flujo que el Portal y la app, así que el comprador recibe la notificación push y tu equipo ve el mensaje en vivo en el Portal.
| Campo | Tipo | Requerido | Descripción |
|---|---|---|---|
content |
String | No* | Texto de la respuesta. Máx. 5000 caracteres. |
files |
Array | No* | De 1 a 10 adjuntos { "uri", "type" } (uri debe ser una URL válida). |
replyTo |
String | No | ID del mensaje al que respondes. |
* Debes enviar al menos uno de content o files.
{
"content": "¡Sí! Disponible. ¿Te lo reservo?",
"replyTo": "665a1b2c3d4e5f6a7b8c9d0e"
}
Respuesta (200): el mensaje creado, con el mismo formato de arriba y direction: "outbound".
La respuesta queda a nombre del comercio. La API Key es quien responde, no una persona de tu equipo. No hay separación por sucursal: la key ve y escribe en todos los chats del comercio.
Errores posibles: 404 SELLER_API_CHAT_NOT_FOUND (chat inexistente o ajeno), 403 CHAT_INACTIVE (chat inactivo o bloqueado, por ejemplo por un pago vencido) y 403 CHAT_RECIPIENT_BLOCKED / CHAT_RECIPIENT_BLOCKED_YOU / CHAT_UNBLOCK_TO_SEND (hay un bloqueo entre las partes).
Mensajes automáticos (bots)
Los mensajes automáticos (estado de envío MRW, tracking QR, recordatorios de pago) son avisos operativos de la app, no conversaciones para tu CRM. Por eso nunca llegan por el webhook chat.message.received, que siempre trae isBot: false. Si tu CRM quiere verlos, los encuentra en GET /chat/messages, marcados con isBot: true.
Webhooks
Si tu comercio tiene configurada una URL de webhook, Yummy le envía una notificación HTTP a tu servidor en cada uno de estos eventos:
| Evento | Cuándo llega |
|---|---|
order.items.sold |
Cuando un comprador paga una orden con productos de tu comercio. |
order.disbursement.completed |
Unos 15 minutos después, cuando el pago a tu comercio se confirma. |
chat.message.received |
Cuando un comprador te escribe (requiere chat:read). |
Evento order.items.sold
Se dispara cuando un comprador completa el pago de una orden que incluye productos de tu comercio.
POST {tu_webhook_url}
Content-Type: application/json
X-Webhook-Id: {orderId}
X-Webhook-Timestamp: {ISO 8601}
X-Webhook-Signature: {firma HMAC-SHA256}
{
"event": "order.items.sold",
"orderId": "667a1b2c3d4e5f6a7b8c9d0e",
"timestamp": "2026-06-17T15:45:00.000Z",
"buyer": {
"buyerId": "usr_88776655",
"fullName": "Juan Pérez",
"firstName": "Juan",
"lastName": "Pérez",
"email": "juan.perez@email.com",
"phone": "+584121234567",
"documentType": "V",
"documentNumber": "12345678",
"documentVerified": true,
"fiscalAddress": null
},
"shippingAddress": {
"recipientName": "Juan Pérez",
"street": "Av. Francisco de Miranda, Edif. Galipán, Piso 4",
"city": "Caracas",
"state": "Distrito Capital",
"zipCode": ""
},
"shipping": {
"couponUrl": "https://example.com/coupon/abc123",
"guideNumber": "123456789",
"shippingMethod": "standard"
},
"payment": {
"paymentMethod": "pago-movil",
"usedCashea": false,
"paymentIntent": {
"bank": "0102",
"phone": "+584121234567",
"transactionPhone": "+584121234567",
"dni": "V12345678",
"documentType": "V",
"documentNumber": "12345678"
}
},
"financials": {
"baseCurrency": "USD",
"exchangeRate": 45.50,
"subtotal": 50.00,
"subtotalBs": 2275.00,
"shippingCost": 5.00,
"shippingCostBs": 227.50,
"freeShipping": false,
"tax": 0,
"discount": 0,
"total": 55.00,
"totalBs": 2502.50,
"deductions": [
{ "type": "shipping", "amount": 5.00, "amountBs": 227.50 }
],
"payout": 50.00,
"payoutBs": 2275.00
},
"items": [
{
"externalVariantId": "ZAPATO-NEGRO-42",
"externalId": "PROD-001",
"sku": "ZAPATO-NEGRO-42",
"variantId": "665a1b2c3d4e5f...",
"branchId": "663c3d4e5f6a7b...",
"branchExternalId": "SUC-001",
"quantity": 2,
"unitPrice": 25.00,
"totalPrice": 50.00,
"totalPriceBs": 2275.00,
"stockAfter": 12
}
]
}
| Campo | Tipo | Descripción |
|---|---|---|
event |
String | Siempre "order.items.sold" en este evento. |
orderId |
String | ID único de la orden en Yummy. |
timestamp |
String (ISO 8601) | Fecha y hora de creación de la orden. Es el mismo valor en order.items.sold, order.disbursement.completed y GET /orders: no cambia según cuándo se envía o se consulta. |
buyer |
Object | Datos del comprador: buyerId, fullName, firstName, lastName, email, phone, documentType (ej. "V"), documentNumber (cédula), documentVerified y fiscalAddress (objeto o null). |
shippingAddress |
Object (opcional) | Dirección de envío: recipientName, street, city, state, zipCode. Solo viene si la orden requiere envío. |
shipping |
Object (opcional) | couponUrl (URL del cupón de envío), guideNumber (número de guía) y shippingMethod (tipo de servicio). Solo viene cuando hay datos del envío. |
payment |
Object | paymentMethod (ej. "pago-movil"), usedCashea, paymentIntent (datos del pago del comprador, opcional) y cashea (solo si usedCashea es true; ver Ventas con Cashea). |
disbursement |
Object (opcional) | Solo en GET /orders y en order.disbursement.completed. Ver Consultar órdenes. |
financials |
Object | Montos de la orden y tu liquidación. Ver Liquidación. |
items |
Array | Productos vendidos de tu comercio. |
items[].externalVariantId |
String | Identificador externo de la variante. |
items[].externalId |
String (opcional) | ID externo del producto en tu sistema. |
items[].sku |
String | SKU de la variante vendida. |
items[].variantId |
String | ID interno de la variante en Yummy. |
items[].branchId |
String | ID interno de la sucursal donde se descontó el stock. |
items[].branchExternalId |
String (opcional) | Tu ID de la sucursal, si lo configuraste. |
items[].quantity |
Integer | Unidades vendidas. |
items[].unitPrice |
Number | Precio unitario en USD al momento de la venta. |
items[].totalPrice |
Number | unitPrice × quantity, en USD. |
items[].totalPriceBs |
Number | totalPrice en bolívares, a la tasa financials.exchangeRate. |
items[].stockAfter |
Integer | Stock que queda en Yummy después de la venta. |
Envío gratis (
freeShipping):shippingCostsiempre muestra el costo real del courier, aunque el envío sea gratis, para que sepas cuánto estás asumiendo con la promoción. SifreeShippingestrue, ese costo no se le cobra al comprador ytotal = subtotal. Si esfalse,total = subtotal + shippingCost.
Datos del comprador para facturar y entregar:
documentNumberprioriza la cédula verificada por Yummy (documentVerified: true). Si no está verificada, es la que declaró el comprador o, como último recurso, la del titular de la cuenta bancaria que usa para cobrar reembolsos, que puede no coincidir con quien retira el pedido. SidocumentVerifiedesfalse, valida la identidad con el documento físico antes de entregar.fiscalAddresspuede llegarnull: Yummy todavía no pide la dirección fiscal a todos los compradores.
Ventas con Cashea
Cuando la venta se pagó con Cashea (payment.usedCashea: true), payment trae un bloque cashea con el detalle de la financiación. Llega igual en order.items.sold, order.disbursement.completed y GET /orders. En ventas sin Cashea el bloque no viene.
financials.subtotal sigue siendo el precio completo del producto. El bloque cashea lo separa en dos partes: la inicial que pagó el comprador (lo que Yummy te deposita con la venta) y el monto que Cashea financia en cuotas.
Ejemplo: venta de $26.99 a tasa 854.4637, inicial de $5.40, $21.59 financiados en 3 cuotas, acuerdo con Cashea al 0% y fee del comprador de 2.1% + IVA.
"payment": {
"paymentMethod": "pago-movil",
"usedCashea": true,
"paymentIntent": {
"bank": "0105",
"phone": "+584141234567",
"transactionPhone": "+584141234567",
"dni": "V12345678",
"documentType": "V",
"documentNumber": "12345678"
},
"cashea": {
"casheaOrderId": 221028073,
"status": "confirmed",
"initialAmount": 5.4,
"initialAmountBs": 4614.1,
"financedAmount": 21.59,
"financedAmountBs": 18447.87,
"sellerFee": 0,
"sellerFeeRate": 0,
"buyerFee": { "rate": 0.021, "amount": 0.57, "ivaRate": 0.16, "iva": 0.09, "total": 0.66 },
"installments": [
{ "installmentNumber": 1, "amount": 7.2, "dueDate": "2026-10-08T15:40:46.696Z" },
{ "installmentNumber": 2, "amount": 7.2, "dueDate": "2026-10-22T15:40:46.696Z" },
{ "installmentNumber": 3, "amount": 7.2, "dueDate": "2026-11-05T15:40:46.696Z" }
]
}
}
| Campo | Tipo | Descripción |
|---|---|---|
casheaOrderId |
Number (opcional) | ID de la orden en Cashea, para conciliar con ellos. Puede faltar en ventas antiguas. |
status |
String (opcional) | Estado de la orden en Cashea: pending, confirmed, cancelled o failed. |
initialAmount / initialAmountBs |
Number | Inicial pagada por el comprador, en USD y en bolívares. Es lo único que Yummy te deposita con la venta. |
financedAmount / financedAmountBs |
Number | subtotal − initialAmount, en USD y en bolívares. Es lo que Cashea financia en cuotas. |
sellerFee / sellerFeeRate |
Number (opcional) | Comisión de Cashea que se descuenta de tu desembolso: monto en USD y tasa como fracción (0.045 = 4,5%). 0 significa un acuerdo al 0%. |
buyerFee |
Object (opcional) | Fee de Cashea que pagó el comprador encima del precio, en USD: rate × subtotal = amount, más iva (ivaRate sobre amount); total = amount + iva. Se cobra con la inicial y no es tuyo: se descuenta de tu desembolso. El IVA de este fee no va en financials.tax, que corresponde al producto. No viene si no se cobró. |
installments |
Array (opcional) | Cuotas tal como las agendó Cashea, sin la inicial: installmentNumber (1..n), amount (USD, redondeado a centavos) y dueDate (ISO 8601). Por el redondeo, la suma puede diferir en un centavo de financedAmount. |
Cuotas: el bloque muestra el cronograma, no si las cuotas ya se cobraron. Las cuotas las gestionas directamente con Cashea. Las ventas antiguas con Cashea sin inicial registrada traen
usedCashea: truesin bloquecashea, y sindeductions,payoutnipayoutBsenfinancials.
Liquidación: de total a payout
financials cierra con tu liquidación. Viene en order.items.sold, order.disbursement.completed y GET /orders, así que desde el primer evento sabes cuánto vas a recibir.
total: lo que se le cobró al comprador.deductions: lo que no llega a tu comercio. Cada línea traetype,amount(USD),amountBsy, en las porcentuales,rateybase. Solo vienen las líneas mayores a 0.payout/payoutBs: lo que Yummy te deposita.payoutBses igual adisbursement.amount.
payout = total − Σ deductions.amount
type |
Qué es |
|---|---|
cashea_financed |
Parte del precio que financia Cashea. No es un cobro: la recibes directamente de Cashea en cuotas y nunca pasa por Yummy. |
cashea_seller_fee |
Comisión por vender con Cashea: rate × base (el precio completo del producto), descontada de la inicial. |
cashea_buyer_fee |
Fee de Cashea que pagó el comprador (payment.cashea.buyerFee.total). Está en total, pero no es tuyo. |
shipping |
Costo del courier. Siempre se descuenta: si lo pagó el comprador, está en total y va al courier; con envío gratis no está en total y lo asume tu comercio. |
El envío no entra en la inicial ni en lo financiado: el comprador lo paga completo junto con la inicial.
Ejemplo: venta con Cashea de $20 a tasa 577.5461, inicial de $6.67, comisión de 3,5% al comercio y fee de 2,1% + IVA al comprador, sin envío.
"financials": {
"baseCurrency": "USD",
"exchangeRate": 577.5461,
"subtotal": 20,
"subtotalBs": 11550.92,
"shippingCost": 0,
"shippingCostBs": 0,
"freeShipping": false,
"casheaBuyerFee": 0.49,
"casheaBuyerFeeBs": 283,
"tax": 0,
"discount": 0,
"total": 20.49,
"totalBs": 11833.92,
"deductions": [
{ "type": "cashea_financed", "amount": 13.33, "amountBs": 7698.69 },
{ "type": "cashea_seller_fee", "rate": 0.035, "base": 20, "amount": 0.7, "amountBs": 404.28 },
{ "type": "cashea_buyer_fee", "amount": 0.49, "amountBs": 283 }
],
"payout": 5.97,
"payoutBs": 3447.95
}
20.49 − 13.33 − 0.70 − 0.49 = 5.97, y payoutBs (3.447,95 Bs) coincide con disbursement.amount.
Redondeo: cada línea en bolívares se redondea por separado, así que la suma en Bs puede diferir en un centavo. En ese caso manda
payoutBs, que es el monto que efectivamente se depositó.
Qué debe hacer tu servidor
- Verificar la firma HMAC-SHA256 (ver Firma de los webhooks).
- Responder con HTTP
200lo más rápido posible. - Procesar la información en segundo plano (descontar en tu ERP, actualizar tu POS, etc.).
Yummy espera la respuesta un máximo de 5 segundos. Si tu servidor tarda más o no responde, el webhook queda registrado como fallido, pero la venta no se cancela. Puedes recuperar las órdenes perdidas con
GET /seller-api/v1/orders.
Ejemplo de receptor en Node.js:
const express = require("express");
const crypto = require("crypto");
const app = express();
app.post("/webhook/yummy", express.json(), (req, res) => {
// 1. Verificar la firma
const signature = req.headers["x-webhook-signature"];
const expected = crypto
.createHmac("sha256", WEBHOOK_SECRET)
.update(JSON.stringify(req.body))
.digest("hex");
if (!crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected))) {
return res.status(401).send("Invalid signature");
}
// 2. Responder de inmediato
res.status(200).json({ received: true });
// 3. Procesar en segundo plano
const { event, orderId, items, financials } = req.body;
if (event === "order.items.sold") {
items.forEach((item) => {
console.log(
`Venta: ${item.quantity}x ${item.sku} @ $${item.unitPrice} — Stock restante: ${item.stockAfter}`
);
});
console.log(`Total: $${financials.total} (Bs ${financials.totalBs})`);
}
if (event === "order.disbursement.completed") {
const { reference, amount, paidAt } = req.body.disbursement;
console.log(`Desembolso de la orden ${orderId} pagado: ref ${reference}, Bs ${amount} (${paidAt})`);
}
});
app.listen(3000);
Evento order.disbursement.completed
Se dispara unos 15 minutos después de la venta, cuando Yummy confirma que el pago móvil a tu comercio fue exitoso. Es un evento informativo para tu contabilidad: te entrega la referencia bancaria del pago que recibiste.
Dos momentos distintos:
order.items.soldllega al momento del pago (descuentas stock y preparas el envío).order.disbursement.completedllega después, cuando tu dinero ya fue depositado (concilias la referencia bancaria). Los dos usan la misma firma, los mismos headers y el mismo formato.
Llega con los mismos headers que order.items.sold y el mismo payload completo de la orden (buyer, shippingAddress, shipping, payment, financials, items), con dos diferencias:
eventvale"order.disbursement.completed".disbursementsiempre viene, constatus: "paid":
"disbursement": {
"status": "paid",
"reference": "20260722150502",
"currency": "VES",
"amount": 2275.0,
"transactionId": "9fdfb05d-06b3-53a9-bf96-d3fcc0e0fbc2",
"paidAt": "2026-07-22T21:05:02.070Z"
}
reference es la referencia bancaria del pago móvil (úsala para conciliar con tu estado de cuenta), currency siempre es "VES", amount es el monto depositado (igual a financials.payoutBs), transactionId es el ID interno de la transacción y paidAt es la fecha del pago.
Aplican las mismas reglas de entrega que
order.items.sold: 5 segundos de timeout, sin reintentos, el fallo del webhook no afecta el desembolso, y se envía a todas las API Keys que tenganwebhookUrl.
Evento chat.message.received
Se dispara cuando un comprador le escribe a tu comercio, si tienes una API Key activa con chat:read y webhookUrl. Es de mejor esfuerzo: si falla, recuperas lo que faltó con GET /chat/messages?since={cursor} (por eso no hay reintentos).
Nunca recibes tus propias respuestas: el webhook solo va al comercio cuando el mensaje es para él, y lo que responde tu CRM va dirigido al comprador.
Los headers son los mismos que en los eventos de orden, salvo que X-Webhook-Id es el messageId (en vez del orderId), para que puedas evitar procesar dos veces el mismo mensaje.
{
"event": "chat.message.received",
"messageId": "665a1b2c3d4e5f6a7b8c9d0e",
"chatId": "664b2c3d4e5f6a7b8c9d0e1f",
"cursor": "665a1b2c3d4e5f6a7b8c9d0e",
"content": "Hola, sigue disponible el zapato talla 42?",
"files": [],
"timestamp": "2026-09-01T15:45:00.000Z",
"from": { "buyerId": "usr_88776655" },
"isBot": false
}
| Campo | Tipo | Descripción |
|---|---|---|
event |
String | Siempre "chat.message.received". |
messageId |
String | ID del mensaje (igual a X-Webhook-Id). |
chatId |
String | Chat al que pertenece. |
cursor |
String | Igual a messageId. Retoma el cursor REST desde aquí: GET /chat/messages?since={cursor}. |
content |
String | Texto plano (nunca viaja cifrado ni queda en logs). "" si es solo de archivos. |
files |
Array | Adjuntos [{ "uri", "type" }]. |
timestamp |
String (ISO 8601) | Fecha del mensaje (informativa; el orden real es por messageId). |
from.buyerId |
String | Autor del mensaje (el comprador). |
isBot |
Boolean | Siempre false en el webhook: los mensajes automáticos no llegan por aquí. |
Ponte al día sin duplicados: guarda el cursor del último webhook que procesaste. Si tu servidor estuvo caído,
GET /seller-api/v1/chat/messages?since={cursor}te devuelve exactamente los mensajes que faltaron.
Aplican las mismas reglas que en los eventos de orden: firma HMAC-SHA256 sobre el body tal como llega, 5 segundos de timeout, sin reintentos, el fallo no afecta el mensaje, y se envía a todas las keys del comercio con chat:read y webhookUrl.
Firma de los webhooks (HMAC-SHA256)
Al crear una API Key con webhookUrl, Yummy genera un webhookSecret de 64 caracteres. Se muestra una sola vez, en la respuesta de creación de la key. Si luego cambia la webhookUrl, se genera un secreto nuevo.
Cada webhook trae estos headers:
| Header | Descripción |
|---|---|
Content-Type |
application/json |
X-Webhook-Id |
ID único del evento: orderId (eventos de orden) o messageId (chat). Úsalo para no procesar dos veces el mismo evento. |
X-Webhook-Timestamp |
Fecha y hora ISO 8601 del envío. |
X-Webhook-Signature |
Firma HMAC-SHA256 del body con tu webhookSecret. |
Verificación de la firma:
const crypto = require("crypto");
function verifyWebhookSignature(rawBody, signature, secret) {
const expected = crypto
.createHmac("sha256", secret)
.update(rawBody)
.digest("hex");
return crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected));
}
Comportamiento de los envíos:
- Timeout de 5 segundos: si tu servidor no responde, el envío queda como fallido.
- Sin reintentos automáticos en la versión actual.
- Un fallo no cancela la venta: el pago se completa igual, sin importar lo que pase con el webhook.
- Varias keys: si tu comercio tiene varias API Keys con
webhookUrl, el evento se envía a todas.
Códigos de error
Errores de autenticación
| HTTP | Código | Qué pasó | Solución |
|---|---|---|---|
| 401 | SELLER_API_KEY_MISSING |
No se envió el header x-api-key. |
Incluye el header con tu API Key. |
| 401 | SELLER_API_KEY_INVALID |
La key no es válida o fue revocada. | Verifica la key. Si fue rotada, usa la nueva. |
| 403 | SELLER_API_KEY_PERMISSION_DENIED |
La key no tiene el permiso que pide el endpoint. | Pide al equipo de Yummy una key con ese permiso. |
Errores de stock (por item)
Aparecen dentro de results.items[] con status: "error":
| Código | Qué pasó | Solución |
|---|---|---|
SELLER_API_VARIANT_NOT_FOUND |
No existe una variante con ese externalVariantId o sku en tu catálogo (o es de otro comercio). |
Verifica que el identificador coincida exactamente con el registrado en Yummy. |
SELLER_API_BRANCH_NOT_FOUND |
No se encontró la sucursal indicada. | Verifica el branchExternalId o el branchId. |
SELLER_API_NO_STOCK_IN_BRANCH |
Solo con decrement: la variante no tiene stock registrado en esa sucursal. Con set o increment el registro se crea. |
Usa set para inicializar el stock en esa sucursal. |
SELLER_API_VARIANT_AMBIGUOUS_SKU |
Hay varias variantes con el mismo SKU en distintos productos. | Usa externalVariantId. Consulta GET /stock para obtener los identificadores. |
SELLER_API_PROCESSING_ERROR |
Error interno al procesar el item. | Reintenta. Si persiste, contacta a soporte. |
Errores de precios (por item)
| Código | Qué pasó | Solución |
|---|---|---|
SELLER_API_VARIANT_NOT_FOUND |
No existe una variante con ese externalVariantId o sku (o es de otro comercio). |
Verifica que el identificador coincida exactamente. |
SELLER_API_VARIANT_AMBIGUOUS_SKU |
Hay varias variantes con el mismo SKU en distintos productos. | Usa externalVariantId. |
SELLER_API_PROCESSING_ERROR |
Error interno al procesar el item. | Reintenta. Si persiste, contacta a soporte. |
Errores de chat
| HTTP | Código | Qué pasó |
|---|---|---|
| 404 | SELLER_API_CHAT_NOT_FOUND |
El chat no existe o no es de tu comercio (en GET y POST /chat/:chatId/messages). |
| 403 | CHAT_INACTIVE |
El chat está inactivo o bloqueado (por ejemplo, por un pago vencido). |
| 403 | CHAT_RECIPIENT_BLOCKED / CHAT_RECIPIENT_BLOCKED_YOU / CHAT_UNBLOCK_TO_SEND |
Hay un bloqueo entre las partes al intentar responder. |
Errores de validación (HTTP 400)
Se devuelven cuando el body o los parámetros no tienen el formato esperado:
- Item sin identificador (ni
externalVariantIdnisku) en operaciones de escritura. - Stock negativo.
- Operación inválida (debe ser
set,incrementodecrement). - Arreglo
itemsvacío o con más de 100 elementos. - Rango de fechas mayor a 7 días en
GET /orders.
Ejemplos completos
Sincronización diaria desde tu ERP
curl -X POST {BASE_URL}/seller-api/v1/stock/sync \
-H "x-api-key: mk_tst_tu_api_key_aqui" \
-H "Content-Type: application/json" \
-d '{
"items": [
{ "externalVariantId": "CAMISA-AZUL-S", "branchExternalId": "SUC-001", "stock": 15, "operation": "set" },
{ "externalVariantId": "CAMISA-AZUL-M", "branchExternalId": "SUC-001", "stock": 25, "operation": "set" },
{ "externalVariantId": "CAMISA-AZUL-L", "branchExternalId": "SUC-001", "stock": 8, "operation": "set" }
]
}'
Venta en tienda física
curl -X POST {BASE_URL}/seller-api/v1/stock/sync \
-H "x-api-key: mk_tst_tu_api_key_aqui" \
-H "Content-Type: application/json" \
-d '{
"items": [
{ "externalVariantId": "ZAPATO-NEGRO-42", "branchExternalId": "SUC-001", "stock": 2, "operation": "decrement" }
]
}'
Actualizar un precio
curl -X PATCH {BASE_URL}/seller-api/v1/sku \
-H "x-api-key: mk_tst_tu_api_key_aqui" \
-H "Content-Type: application/json" \
-d '{
"items": [
{ "externalVariantId": "CAMISA-AZUL-M", "price": 29.99 }
]
}'
Consultar stock por SKU
curl -X GET "{BASE_URL}/seller-api/v1/stock?sku=CAMISA-AZUL-M" \
-H "x-api-key: mk_tst_tu_api_key_aqui"
Reconciliar órdenes
curl -X GET "{BASE_URL}/seller-api/v1/orders?startDate=2026-07-01&endDate=2026-07-08&page=1" \
-H "x-api-key: mk_tst_tu_api_key_aqui"
Leer el chat y responder
# Traer los mensajes nuevos desde el último cursor procesado
curl -X GET "{BASE_URL}/seller-api/v1/chat/messages?since=665a1b2c3d4e5f6a7b8c9d0e&limit=50" \
-H "x-api-key: mk_tst_tu_api_key_aqui"
# Responder en un chat
curl -X POST "{BASE_URL}/seller-api/v1/chat/664b2c3d4e5f6a7b8c9d0e1f/messages" \
-H "x-api-key: mk_tst_tu_api_key_aqui" \
-H "Content-Type: application/json" \
-d '{ "content": "¡Sí! Disponible. ¿Te lo reservo?" }'
Límites
| Concepto | Límite | Nota |
|---|---|---|
| Items por request de stock o precios | 100 | Para inventarios grandes, divide en varios requests. |
| API Keys por comercio | 5 | Suficiente para ERP, POS, app móvil, pruebas, etc. |
Rango de fechas en /orders |
7 días | Divide en consultas consecutivas para rangos mayores. |
| Resultados por página | 100 | Por defecto, 20. |
| Timeout de webhooks | 5 segundos | Tu servidor debe responder dentro de este tiempo. |
| Chat: mensajes por página | 100 | Por defecto, 50. Pagina por cursor con ?since=. |
| Chat: texto por respuesta | 5000 caracteres | En POST /chat/:chatId/messages. |
| Chat: adjuntos por respuesta | 1 a 10 | Cada uno { uri, type }. |
Buenas prácticas
- Usa
setpara sincronizaciones completas. Es la operación más segura cuando tu ERP tiene el número exacto de stock. - Usa
increment/decrementpara cambios en tiempo real: ventas en tu POS, recepciones, devoluciones. - Guarda el
batchIdde cada respuesta. Sirve para rastrear y para pedir soporte. - Revisa los items con
warning. Indican que tu sistema y Yummy no están sincronizados. - Verifica la firma HMAC de todos los webhooks. Te protege de payloads falsos.
- Usa el endpoint de órdenes para reconciliar. Si un webhook falló, consulta
GET /orderscon el rango de fechas afectado. - Nunca expongas tu API Key en código frontend, apps móviles ni repositorios públicos.
- Maneja los errores por item. Si un request de 50 items tiene 2 errores, los otros 48 se procesaron bien: reintenta solo los que fallaron.
- Chat: guarda el cursor y usa REST como red de seguridad. Persiste el cursor del último mensaje procesado y, ante cualquier caída, usa
GET /chat/messages?since={cursor}para recuperar lo que faltó sin duplicados. Los mensajes automáticos (isBot: true) solo aparecen por REST.
¿Dudas técnicas? Escríbele a tu contacto en Yummy y pide comunicación directa con nuestro equipo de desarrollo.
Historial de versiones
| Versión | Fecha | Cambios |
|---|---|---|
| 3.2 | Septiembre 2026 | Liquidación en financials (deductions, payout, payoutBs) y fee del comprador en ventas con Cashea. |
