Webhooks
Un webhook es una URL de tu aplicación que la plataforma llama automáticamente (POST con un cuerpo JSON) cada vez que ocurre un evento: se crea un usuario, se publica un producto, se paga o se cancela un pedido, etc. Así tu sistema se entera en el momento, sin tener que consultar la API periódicamente.
1. Dónde se configuran
Hay tres formas de registrar un webhook. Cada una determina qué eventos y de qué alcance vas a recibir (ver sección 3).
| Quién lo registra | Dónde | Alcance |
|---|---|---|
| Administrador del market | Panel de administración > Desarrolladores > WebHooks | Todo el market: usuarios, productos y pedidos de todas las tiendas. |
| Una tienda (vendedor) | Mi Cuenta > Desarrolladores > WebHooks | Solo lo de esa tienda: sus productos, sus ventas y las preguntas sobre sus productos. |
| Una aplicación del Mercado de Aplicaciones | Automático al instalar la app (ver sección 8) | El del lugar donde se instaló la app: el market completo o una tienda. |
Al crear el webhook indicás un nombre, la URL que va a recibir las notificaciones (debe ser pública y con HTTPS válido) y los eventos a los que te suscribís. Cada webhook tiene un secreto de firma propio que se usa para verificar que las notificaciones vienen de la plataforma (sección 5).
Un webhook solo recibe los eventos a los que está suscripto. Cuando se agregan eventos nuevos a la plataforma (como los de sub-pedidos de la sección 3), los webhooks existentes no los reciben hasta que se los agregue a la suscripción.
2. Eventos disponibles
Los nombres de evento van siempre en plural (purchases.charge, products.create), salvo question.received y seller.created, que se escriben así.
| Evento | Cuándo se dispara | Clave en data | Disponible para |
|---|---|---|---|
| users.created | Se registra un usuario nuevo. | user | Market |
| users.verified | El usuario confirma su email (o se registra con el email ya verificado, por ejemplo con login social). | user | Market |
| seller.created | Un usuario pasa a ser vendedor: se registra directamente como vendedor, o un usuario comprador abre su tienda y pasa a tener rol seller. | user | Market |
| question.received | Un visitante hace una pregunta sobre un producto. | question | Tienda |
| products.set_category | Comienza la carga de un producto desde Mi Cuenta: el vendedor eligió la categoría pero todavía no completó los datos. | item (producto) | Market y tienda |
| products.create | Se dio de alta un producto, ya con sus variantes, imágenes y atributos. Se envía una sola vez por producto, cualquiera sea el origen del alta (ver "Alta de productos" en la sección 3). | item (producto) | Market y tienda |
| products.update | Se modifica un producto. | item (producto) | Market y tienda |
| products.destroy | Se elimina un producto. | item (producto) | Market y tienda |
| products.variants.create | Se crea una variante de un producto. | item (variante) | Market y tienda |
| products.variants.update | Se modifica una variante (precio, stock, SKU, etc.). | item (variante) | Market y tienda |
| products.variants.destroy | Se elimina una variante. | item (variante) | Market y tienda |
| purchases.charge | Se registra el pago del pedido. | purchase | Market y tienda |
| purchases.shipments.deliver_to_other | El pedido se despacha a través de un tercero (pasa a "en camino"). | purchase | Market y tienda |
| purchases.success | El pedido finaliza exitosamente. | purchase | Market y tienda |
| purchases.cancel | Se cancela el pedido. | purchase | Market y tienda |
| purchases.suborders.success | Una tienda finalizó su parte del pedido (por ejemplo, el comprador recibió sus productos), sin que se haya finalizado el pedido completo. | purchase | Market |
| purchases.suborders.cancel | Se canceló la parte del pedido de una tienda (la canceló el vendedor, el administrador o se eliminó la tienda), sin que se haya cancelado el pedido completo. | purchase | Market |
| purchases.finished_with_observations | El pedido terminó con sus tiendas en estados distintos (por ejemplo, una parte se entregó y otra se canceló). El pedido queda en estado "Finalizada con Observaciones". | purchase | Market |
3. Qué recibe cada webhook
Pedidos: pedido completo vs. sub-pedido por tienda
Un pedido puede incluir productos de varias tiendas. Internamente se compone de un pedido (lo que pagó el comprador) y un sub-pedido por cada tienda involucrada. Los eventos purchases.* se envían en dos niveles:
| Webhook | Recibe | Formato de order.id |
|---|---|---|
| Del market (o app instalada en el market) | Siempre el pedido completo, con los ítems de todas las tiendas y la lista suborders con el estado de cada sub-pedido. Recibe los pedidos de todas las tiendas del market. | TO-P--P6599 |
| De una tienda (o app instalada en una tienda) | Solo el sub-pedido de esa tienda, con sus ítems. | TO-PS--P6599-S6603 |
Pedidos con varias tiendas: cómo se entera el market
Cada tienda avanza su sub-pedido por su cuenta: puede finalizarlo o cancelarlo sin que las demás hayan terminado. Los eventos de cada tienda no llegan al webhook del market tal cual. En cambio, el market recibe estos eventos, siempre con el pedido completo:
| Qué pasa | Qué recibe la tienda | Qué recibe el market |
|---|---|---|
| Se paga, cancela o finaliza el pedido completo (por ejemplo, el pago del comprador o una acción del administrador sobre el pedido) | purchases.charge / purchases.cancel / purchases.success de su sub-pedido | El mismo evento, una vez, con el pedido completo |
| Una tienda finaliza su parte | purchases.success | purchases.suborders.success |
| Una tienda cancela su parte | purchases.cancel | purchases.suborders.cancel |
| La última tienda termina y todas finalizaron | — | Además, purchases.success |
| La última tienda termina y todas cancelaron | — | Además, purchases.cancel |
| La última tienda termina y hay estados distintos | — | Además, purchases.finished_with_observations |
En los eventos purchases.suborders.* el campo suborder_changed indica qué sub-pedido cambió, y cada elemento de suborders[] trae su status actual. Con eso podés saber, por ejemplo, qué parte del pago hay que devolver cuando una tienda cancela.
Ejemplo: un pedido con dos tiendas, A y B. La tienda A cancela su parte y después la tienda B entrega la suya. El webhook del market recibe, en este orden:
- purchases.suborders.cancel con suborder_changed = sub-pedido de A.
- purchases.suborders.success con suborder_changed = sub-pedido de B.
- purchases.finished_with_observations.
En un pedido de una sola tienda, cuando la tienda finaliza o cancela su parte, el market recibe el evento purchases.suborders.* y a continuación el purchases.success o purchases.cancel del pedido completo. Si solo te interesa el resultado final del pedido, alcanza con suscribirse a estos dos últimos.
El despacho que hace cada tienda de su parte (purchases.shipments.deliver_to_other) llega solo al webhook de esa tienda. El market recibe ese evento cuando se despacha el pedido completo. Si el market está configurado para que cobre cada tienda (pago por tienda), purchases.charge llega solo al webhook de la tienda.
Productos, variantes y preguntas
- Productos y variantes: los recibe el webhook de la tienda dueña del producto y también los webhooks del market.
- Preguntas (question.received): solo el webhook de la tienda dueña del producto.
- Usuarios (users.*, seller.created): solo los webhooks del market.
Alta de productos
Un producto se puede dar de alta de tres maneras. En todas, products.create se envía una sola vez, cuando el producto ya está completo:
| Origen del alta | Cuándo llega products.create |
|---|---|
| Mi Cuenta (carga manual del vendedor) | Cuando el vendedor guarda el formulario completo. Antes llega products.set_category, al elegir la categoría. |
| API (POST /api/v1/items) | Al terminar el request. Si el request encuentra un producto existente (por ejemplo, por external_id), llega products.update en vez de products.create. |
| Importación de productos (CSV), desde el administrador o desde Mi Cuenta | Cuando termina de procesarse toda la importación, una vez por cada producto nuevo. Un producto con varias variantes ocupa varias filas del archivo, y el aviso espera a que estén todas cargadas. En importaciones grandes puede llegar varios minutos después de iniciada. |
Los products.variants.create de las variantes se envían a medida que se crea cada una, así que normalmente llegan antes que el products.create de su producto. Si procesás el producto completo, alcanza con products.create, que ya incluye todas las variantes en variations[].
Identificadores de pedido
El identificador del pedido (data.purchase.order.id) es siempre un texto, no un número, y es el mismo que devuelve la API de ventas. Usalo para relacionar los distintos eventos de un mismo pedido (por ejemplo, un purchases.cancel con su purchases.charge previo).
- TO-P--P6599: pedido completo número 6599.
- TO-PS--P6599-S6603: sub-pedido 6603 (de una tienda) dentro del pedido 6599.
El tipo también viene por separado en order.type (TO-P o TO-PS). Los identificadores de suborders[].id y suborder_changed usan el mismo formato TO-PS--....
4. Formato del request
Cada notificación es un POST a tu URL con estos headers:
| Header | Valor |
|---|---|
| Content-Type | application/json |
| Signature | Firma HMAC-SHA256 del cuerpo (ver sección 5). |
El cuerpo siempre tiene la misma estructura: el nombre del evento y los datos del objeto involucrado, bajo una clave que depende del tipo (ver tabla de la sección 2).
{
"event": "purchases.charge",
"data": {
"purchase": { ... }
}
}5. Seguridad: verificación de la firma
Cada request incluye el header Signature con un HMAC-SHA256 (en hexadecimal) calculado sobre el cuerpo JSON crudo, tal como llega, usando el secreto de firma del webhook. Verificar esta firma es obligatorio: sin ella, cualquiera podría enviarle a tu URL un pedido "pagado" falso.
- Calculá el HMAC sobre el cuerpo sin parsear. Si primero lo convertís a objeto y lo volvés a serializar, el resultado puede cambiar (espacios, orden, decimales) y la firma no va a coincidir.
- Compará con una función de tiempo constante (hash_equals en PHP, crypto.timingSafeEqual en Node).
- Si la firma no coincide, respondé 401 y descartá el request.
Dónde está el secreto: en la pantalla de edición del webhook. Para los webhooks que crea una aplicación al instalarse, el secreto es el hash de la aplicación.
6. Respuesta, tiempos y reintentos
- Tu endpoint tiene que responder con cualquier código 2xx en menos de 3 segundos.
- Si responde otro código (4xx o 5xx) o tarda más, el envío se considera fallido y se reintenta: 3 intentos en total, con esperas crecientes (aproximadamente 10 segundos antes del segundo intento y 100 segundos antes del tercero).
- Si tenés que hacer un procesamiento pesado, respondé 200 de inmediato y procesá en segundo plano.
- Las notificaciones salen por una cola que se procesa cada minuto, así que pueden llegar con hasta un par de minutos de demora respecto del evento. No es un canal en tiempo real estricto.
- Por los reintentos, la misma notificación puede llegar más de una vez. Tu procesamiento tiene que ser idempotente: si ya procesaste purchases.charge para TO-P--P6599, ignorá el duplicado.
- El orden de llegada no está garantizado. Si el orden te importa, consultá el estado actual del pedido o producto por la API antes de actuar.
7. Ejemplos de payload por evento
Los datos personales de los ejemplos son ficticios. Los valores numéricos de los pedidos pueden venir como número (46534.9) o como texto con dos decimales ("32000.00") según el campo: convertí siempre a número antes de operar.
7.1 Pedidos: purchases.charge, purchases.success, purchases.cancel, purchases.shipments.deliver_to_other
Los cuatro eventos de pedido usan la misma estructura. Lo que cambia entre uno y otro es el valor del campo status.
Pedido completo (lo que recibe un webhook del market):
{
"event": "purchases.charge",
"data": {
"purchase": {
"order": {
"id": "TO-P--P6599",
"type": "TO-P"
},
"suborders": [
{
"id": "TO-PS--P6599-S6603",
"price": 46534.9,
"status": {
"global": { "id": 1, "name": "En Proceso" },
"shipping": { "id": 7, "name": "Esperando ser Iniciado" },
"payment": { "id": 2, "name": "Confirmado" }
}
}
],
"status": {
"global": { "id": 1, "name": "En Proceso" },
"shipping": { "id": 7, "name": "Esperando ser Iniciado" },
"payment": { "id": 2, "name": "Confirmado" }
},
"currency": {
"id": 1,
"name": "Peso Argentino",
"symbol": "AR$",
"default": 1,
"code": "ARS",
"enabled": 1,
"quote_default_currency": "1.00"
},
"price": 46534.9,
"address_destination": "Av. Siempre Viva 742 | CP: C1425<br>Palermo. CABA. Argentina. ",
"buyer": {
"id": 30135,
"name": "Juan Pérez",
"email": "comprador@ejemplo.com",
"role": "user",
"phone": { "code_country": "54", "code_area": "11", "number": "12345678" },
"document": { "type": "DNI", "number": "30123456" }
},
"items": [
{
"item": {
"type": "product",
"id": 9475,
"store_id": 1161,
"category_id": 102,
"sku": "FV-CHICA"
},
"description": "Funda Valija CHICA (Carry-On)",
"quantity": 1,
"price": "32000.00",
"price_unit": "32000.00",
"commision": 1920,
"weight": 700,
"height": 55,
"width": 36,
"length": 20
}
],
"items_extra": [
{
"item": {
"type": "shipping",
"import_market": 14534.9,
"owner": "stores",
"stores": [
{
"import": 14534.9,
"name": "Oca ePack Simplificado",
"owner": "store",
"id": 2894,
"store_id": 1161,
"import_market": 14534.9
}
]
},
"description": "Medio de Envío",
"quantity": 1,
"price": 14534.9,
"price_unit": 14534.9,
"price_market": 14534.9
}
]
}
}
}Sub-pedido de una tienda (lo que recibe un webhook de tienda): misma estructura, sin suborders, con order.id de tipo TO-PS y solo los ítems de esa tienda:
{
"event": "purchases.charge",
"data": {
"purchase": {
"order": {
"id": "TO-PS--P6599-S6603",
"type": "TO-PS"
},
"status": { ... },
"currency": { ... },
"price": 46534.9,
"address_destination": "...",
"buyer": { ... },
"items": [ ... ],
"items_extra": [ ... ]
}
}
}Campos del pedido:
| Campo | Descripción |
|---|---|
| order.id / order.type | Identificador del pedido o sub-pedido (ver sección 3). |
| suborders[] | Solo en el pedido completo: un elemento por tienda, con su identificador (id), su importe (price) y su estado actual (status, con el mismo formato que el status del pedido). |
| suborder_changed | Solo en los eventos purchases.suborders.*: identificador del sub-pedido que cambió. |
| status.global | Estado general del pedido. Valores frecuentes: 1 En Proceso, 2 Finalizada Exitosamente, 3 Cancelada, 11 Finalizada con Observaciones. |
| status.payment | Estado del pago. Solo si el pedido requiere pago. |
| status.shipping | Estado del envío. Solo si el pedido requiere envío. |
| currency | Moneda del pedido. |
| price | Importe total. |
| address_destination | Dirección de entrega en texto (puede contener <br>). Vacío si no hay envío. |
| buyer | Comprador: id, name, email, role (user o seller; un vendedor también puede comprar), phone y, si lo cargó, document. |
| items[] | Productos comprados. item.id es el id del producto, item.store_id la tienda que lo vende y item.sku el SKU de la variante comprada. price es el subtotal del ítem, price_unit el precio unitario y commision la comisión del market. Medidas en gramos (weight) y centímetros. |
| items_extra[] | Conceptos adicionales al precio de los productos. item.type puede ser shipping (envío), payment (recargo del medio de pago), service_cost (costo de servicio) o discount (descuento, con importe negativo). |
| discount | Solo si se aplicó un cupón o descuento: detalle del descuento. |
| custom_fields | Solo en el pedido completo y si el market tiene campos personalizados en el checkout. |
7.2 Sub-pedidos: purchases.suborders.success, purchases.suborders.cancel, purchases.finished_with_observations
Solo para webhooks del market. Traen el pedido completo, con la misma estructura de la sección 7.1. Ejemplo: en un pedido de dos tiendas, la tienda del sub-pedido S6604 canceló su parte y la otra todavía no terminó:
{
"event": "purchases.suborders.cancel",
"data": {
"purchase": {
"order": {
"id": "TO-P--P6599",
"type": "TO-P"
},
"suborders": [
{
"id": "TO-PS--P6599-S6603",
"price": 46534.9,
"status": {
"global": { "id": 1, "name": "En Proceso" },
"shipping": { "id": 7, "name": "Esperando ser Iniciado" },
"payment": { "id": 2, "name": "Confirmado" }
}
},
{
"id": "TO-PS--P6599-S6604",
"price": 18900,
"status": {
"global": { "id": 3, "name": "Cancelada" },
"shipping": { "id": 7, "name": "Esperando ser Iniciado" },
"payment": { "id": 2, "name": "Confirmado" }
}
}
],
"suborder_changed": "TO-PS--P6599-S6604",
"status": {
"global": { "id": 1, "name": "En Proceso" },
"shipping": { "id": 7, "name": "Esperando ser Iniciado" },
"payment": { "id": 2, "name": "Confirmado" }
},
"currency": { ... },
"price": 65434.9,
"address_destination": "...",
"buyer": { ... },
"items": [ ... ],
"items_extra": [ ... ]
}
}
}- El status del pedido completo sigue "En Proceso" mientras quede alguna tienda sin terminar.
- purchases.finished_with_observations no trae suborder_changed: el pedido terminó, y el resultado de cada tienda está en suborders[].status. El status.global del pedido es 11 (Finalizada con Observaciones).
- El importe del sub-pedido cancelado está en su suborders[].price. Para el detalle de los productos de esa tienda, filtrá items[] por item.store_id.
7.3 Usuarios: users.created, users.verified, seller.created
{
"event": "seller.created",
"data": {
"user": {
"id": 30407,
"name": "María Gómez",
"email": "vendedora@ejemplo.com",
"role": "seller",
"phone": { "code_country": "54", "code_area": "11", "number": "12345678" },
"document": { "type": "DNI", "number": "28123456" }
}
}
}document solo aparece si el usuario lo cargó. Los tres eventos de usuario tienen esta misma estructura.
Usuarios, vendedores y tiendas: un vendedor (seller) es un usuario con rol de vendedor; la tienda es una entidad aparte cuyo dueño es ese usuario. El evento seller.created trae solo el usuario, no la tienda. Para obtener los datos de la tienda (incluido su id) consultá la API. En los productos y pedidos la tienda aparece como store_id / store.id.
7.4 Productos: products.set_category, products.create, products.update, products.destroy
{
"event": "products.create",
"data": {
"item": {
"id": 11973,
"sku": null,
"state": { "id": 3, "name": "Aprobado" },
"name": "Overnight Duffle",
"description": "<p>Bolso de viaje con doble manija...</p>",
"category": {
"id": 3,
"name": "Cajas",
"path_name": "Casa y deco > Deco > Cajas",
"selectable": 1,
"required_price": 0,
"link": "https://tu-market.com/casa-y-decoracion-deco-cajas",
"allow_variations": true,
"require_variations": false
},
"operation_type": { "id": 1, "name": "Compra/Venta", "enabled_wholesale": 1 },
"price": {
"currency": { "id": 1, "name": "Peso Argentino", "symbol": "AR$", "code": "ARS" },
"discount_applied": 0
},
"images": [],
"variations": [
{
"id": 8829,
"name": "M / Rojo",
"product_id": 11973,
"sku": "24-WB07-M-R",
"is_default": 0,
"price": {
"currency": { "id": 1, "name": "Peso Argentino", "symbol": "AR$", "code": "ARS" },
"price": "4000.00"
},
"stock": 100
},
{
"id": 8831,
"name": "L / Rojo",
"product_id": 11973,
"sku": "24-WB07-L-R",
"is_default": 0,
"price": {
"currency": { "id": 1, "name": "Peso Argentino", "symbol": "AR$", "code": "ARS" },
"price": "2000.00"
},
"stock": 100
}
],
"wholesales": [],
"features": [],
"link": "https://tu-market.com/overnight-duffle_11973",
"store": { "id": 1380, "name": "Mi Tienda", "store_type_id": 1 }
}
}
}Precio y stock están siempre en las variantes (variations[]). Un producto sin variantes visibles tiene una única variante con is_default: 1, que es la que contiene su precio y stock. Un stock de -1 significa que el producto no controla stock.
En products.set_category el producto todavía está "en proceso": puede venir sin nombre definitivo, sin variantes y sin imágenes. Los datos completos llegan después, en products.create.
7.5 Variantes: products.variants.create, products.variants.update, products.variants.destroy
{
"event": "products.variants.update",
"data": {
"item": {
"id": 8829,
"name": "M / Rojo",
"product_id": 11973,
"sku": "24-WB07-M-R",
"is_default": 0,
"price": {
"currency": { "id": 1, "name": "Peso Argentino", "symbol": "AR$", "code": "ARS" },
"price": "4000.00"
},
"stock": 100
}
}
}Usá product_id para saber a qué producto pertenece la variante.
7.6 Preguntas: question.received
{
"event": "question.received",
"data": {
"question": {
"id": 3069,
"user_id": 30130,
"product_id": 8107,
"question": "¿Tienen stock en color negro?",
"answer": null,
"approved": 1,
"created_at": "2026-06-24T12:49:07.000000Z",
"updated_at": "2026-06-24T12:49:07.000000Z",
"deleted_at": null,
"answered_by_user_id": null
}
}
}8. Webhooks de aplicaciones del Mercado de Aplicaciones
Una aplicación no necesita que nadie le cree el webhook a mano: si la app tiene definidos sus eventos y su URL de hooks, el webhook se registra automáticamente cada vez que alguien instala la app, y se elimina al desinstalarla.
- Alcance: si la app se instaló en el market, recibe los eventos del market completo; si se instaló en una tienda, solo los de esa tienda (sección 3).
- Firma: el secreto es el hash de la aplicación.
- Datos extra: a data se le agregan las claves app e installation, para que una misma URL pueda atender varios markets e instalaciones:
{
"event": "purchases.charge",
"data": {
"app": {
"app_code": "mi-app",
"url_market": "https://tu-market.com/",
"market_id": 12
},
"installation": {
"id": 1446,
"target_type": "store",
"target_id": 1161,
"owner": { "name": "Mi Tienda", "email": "tienda@ejemplo.com" }
},
"purchase": { ... }
}
}installation.target_type es market o store, y target_id el id del market o de la tienda donde está instalada.
En el panel del administrador, estos webhooks (y los creados por las tiendas) aparecen en la sección "WebHooks de Terceros".
Evitar recibir el eco de tus propios cambios
Si tu aplicación modifica productos por la API (por ejemplo, sincroniza stock desde otro sistema), vas a recibir los webhooks de esos mismos cambios. Para evitarlo, enviá el parámetro disable_hook=1 en el request a la API: los cambios hechos en ese request no disparan ningún webhook, incluidos products.create y los eventos de las variantes.
9. Probar un webhook
En la pantalla de edición del webhook (panel de administración) está la herramienta "Probar webhook":
- Elegí el evento. Se arma automáticamente el payload con el registro más reciente; también podés buscar un registro puntual (un pedido, un producto, un usuario).
- Revisá el JSON generado. Podés editarlo antes de enviarlo.
- Enviá la prueba. Vas a ver el código HTTP, el tiempo de respuesta y el cuerpo que devolvió tu endpoint.
La prueba se envía firmada igual que una notificación real, así que sirve para validar también la verificación de la firma.
En las pruebas de purchases.suborders.* el payload no trae suborder_changed, porque no hay una tienda que haya cambiado. Si querés probar ese campo, agregalo a mano en el JSON antes de enviarlo.
10. Ejemplos de implementación
PHP
<?php
$secret = 'TU_SECRETO_DE_FIRMA';
// 1. Leer el cuerpo crudo ANTES de parsearlo
$body = file_get_contents('php://input');
$signature = $_SERVER['HTTP_SIGNATURE'] ?? '';
// 2. Verificar la firma
$expected = hash_hmac('sha256', $body, $secret);
if (!hash_equals($expected, $signature)) {
http_response_code(401);
exit;
}
$payload = json_decode($body, true);
$data = $payload['data'];
// 3. Responder rápido (si el proceso es pesado, encolalo y respondé 200)
http_response_code(200);
switch ($payload['event']) {
case 'purchases.charge':
$orderId = $data['purchase']['order']['id']; // ej. "TO-P--P6599"
// marcar el pedido como pagado (ignorar si ya estaba procesado)
break;
case 'purchases.cancel':
$orderId = $data['purchase']['order']['id'];
// anular el pedido / revertir el pago asociado
break;
case 'purchases.suborders.cancel':
// solo webhooks del market: una tienda canceló su parte
$orderId = $data['purchase']['order']['id'];
$suborderId = $data['purchase']['suborder_changed']; // ej. "TO-PS--P6599-S6604"
foreach ($data['purchase']['suborders'] as $suborder) {
if ($suborder['id'] === $suborderId) {
$importe = (float) $suborder['price'];
// devolver $importe al comprador (ignorar si ya se procesó)
}
}
break;
case 'purchases.finished_with_observations':
// el pedido terminó con tiendas en estados distintos: ver suborders[].status
break;
case 'products.create':
case 'products.update':
$productId = $data['item']['id'];
foreach ($data['item']['variations'] as $variant) {
// $variant['sku'], $variant['price']['price'], $variant['stock']
}
break;
case 'seller.created':
$userId = $data['user']['id'];
break;
}
Node.js (Express)
const express = require('express');
const crypto = require('crypto');
const app = express();
const SECRET = 'TU_SECRETO_DE_FIRMA';
// express.raw: necesitamos el cuerpo sin parsear para verificar la firma
app.post('/webhook', express.raw({ type: 'application/json' }), (req, res) => {
const signature = req.headers['signature'] || '';
const expected = crypto.createHmac('sha256', SECRET).update(req.body).digest('hex');
const valid = signature.length === expected.length &&
crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected));
if (!valid) return res.sendStatus(401);
// Responder rápido y procesar después
res.sendStatus(200);
const { event, data } = JSON.parse(req.body.toString('utf8'));
switch (event) {
case 'purchases.charge':
// data.purchase.order.id ej. "TO-P--P6599"
break;
case 'purchases.cancel':
// data.purchase.order.id
break;
case 'purchases.suborders.cancel': {
// solo webhooks del market: una tienda canceló su parte
const changed = data.purchase.suborder_changed;
const suborder = data.purchase.suborders.find(s => s.id === changed);
// Number(suborder.price): importe a devolver
break;
}
case 'purchases.finished_with_observations':
// data.purchase.suborders[].status
break;
case 'products.create':
case 'products.update':
// data.item.id, data.item.variations[]
break;
case 'seller.created':
// data.user.id
break;
}
});
app.listen(3000);