Webhooks
Recibe notificaciones en tiempo real cuando ocurren eventos en una tienda.
Descripción General
Los webhooks permiten que tu aplicación reciba notificaciones HTTP POST cuando ocurren eventos en una tienda Whatalo. En lugar de hacer polling a la API, tu servidor recibe datos automáticamente.
Antes de empezar: gestionar webhooks requiere una API key con los scopes
read:webhooks y write:webhooks. Cuando el comerciante crea la key en
Settings > Developer > API Keys, selecciona el grupo de permisos
Automatizaciones (que incluye ambos scopes). Una key sin ellos recibe una
respuesta 403 SCOPE_INSUFFICIENT. Una vez que la key tiene los scopes, tu
integración registra los webhooks por sí misma con POST /v1/webhooks — no
hace falta configurarlos manualmente en el panel.
Cómo Funcionan los Webhooks
1. Ocurre un evento (ej. nuevo pedido realizado)
2. Whatalo envía un POST request a tu URL registrada
3. Tu servidor procesa el payload y devuelve 2xx
4. Si la entrega falla, Whatalo reintenta con backoff exponencialGestión de Endpoints de Webhooks
Listar Webhooks
GET /v1/webhooksScope requerido: read:webhooks
Crear Webhook
POST /v1/webhooks{
"url": "https://your-server.com/webhooks/whatalo",
"events": ["order.created", "order.updated"]
}| Campo | Tipo | Requerido | Descripción |
|---|---|---|---|
url | string | Sí | URL del endpoint HTTPS (debe ser HTTPS) |
events | string[] | Sí | Tipos de eventos a suscribir |
secret | string | No | Secret de firma personalizado (se genera automáticamente si se omite) |
Las URLs de webhooks deben usar HTTPS. Los endpoints HTTP son rechazados.
Scope requerido: write:webhooks
Actualizar Webhook
PATCH /v1/webhooks/:idScope requerido: write:webhooks
Eliminar Webhook
DELETE /v1/webhooks/:idScope requerido: write:webhooks
Tipos de Eventos
| Evento | Disparador |
|---|---|
order.created | Nuevo pedido realizado |
order.updated | Estado o datos del pedido cambiaron |
order.cancelled | Pedido cancelado |
order.completed | Pedido completado |
product.created | Nuevo producto creado |
product.updated | Datos del producto cambiaron |
product.deleted | Producto eliminado |
customer.created | Nuevo cliente registrado |
customer.updated | Datos del cliente cambiaron |
checkout.completed | Checkout completado |
checkout.abandoned | Checkout marcado como abandonado tras inactividad |
Payload del Webhook
Los payloads de webhooks son objetos JSON. El header X-Webhook-Event identifica el tipo de evento y el body contiene el payload específico del evento.
Para eventos de pedido, cliente y producto, event_id es el identificador público de la entidad afectada. Coincide con order.id, customer.id o product.id dentro del mismo payload. Usa X-Webhook-Id para idempotencia por entrega y X-Webhook-Event junto con event_id para deduplicación a nivel de negocio.
order.created
{
"event_id": "ord_abc123",
"occurred_at": "2026-03-01T15:00:00.000Z",
"order": {
"id": "ord_abc123",
"order_number": 1042,
"status": "pending",
"payment_status": "pending",
"payment_method": "cash_on_delivery",
"total": 8997,
"subtotal": 8997,
"shipping": 0,
"discount": 0,
"coupon_code": null,
"free_shipping": false,
"currency": "DOP",
"created_at": "2026-03-01T15:00:00.000Z",
"custom_fields": []
},
"customer": {
"id": "cus_abc123",
"name": "Ana Pérez",
"email": "[email protected]",
"phone": "8090000000"
},
"store": {
"id": "sto_abc123",
"name": "Mi Tienda",
"timezone": "America/Santo_Domingo"
}
}order.updated
Los payloads de actualización de pedidos pueden ser parciales. Para cambios de estado, el body incluye solo los campos modificados:
{
"event_id": "ord_abc123",
"occurred_at": "2026-03-01T15:05:00.000Z",
"order": {
"id": "ord_abc123",
"status": "completed"
},
"store": {
"id": "sto_abc123",
"timezone": "America/Santo_Domingo"
}
}Las actualizaciones de pago usan la misma forma parcial con order.payment_status.
Cuando los datos del pedido cambian sin un cambio de estado —por ejemplo, al editar sus líneas de producto— el body omite tanto status como payment_status y solo incluye el id del pedido, occurred_at y la tienda. Trátalo como una señal de "datos modificados" y vuelve a consultar el pedido completo mediante GET /v1/orders/{id}:
{
"event_id": "ord_abc123",
"occurred_at": "2026-03-01T15:05:00.000Z",
"order": {
"id": "ord_abc123"
},
"store": {
"id": "sto_abc123",
"timezone": "America/Santo_Domingo"
}
}Eventos de Producto
product.created y product.updated usan esta forma:
{
"event_id": "prd_abc123",
"occurred_at": "2026-03-01T15:00:00.000Z",
"product": {
"id": "prd_abc123",
"name": "Kit Inicial",
"slug": "kit-inicial",
"price": 2500,
"product_status": "active",
"archived_at": null,
"main_image": "https://cdn.example.test/product.jpg"
},
"store": {
"id": "sto_abc123",
"timezone": "America/Santo_Domingo"
}
}product.deleted incluye el identificador del producto eliminado:
{
"event_id": "prd_abc123",
"occurred_at": "2026-03-01T15:00:00.000Z",
"product": {
"id": "prd_abc123"
},
"store": {
"id": "sto_abc123",
"timezone": "America/Santo_Domingo"
}
}Eventos de Cliente
customer.created y customer.updated usan esta forma:
{
"event_id": "cus_abc123",
"occurred_at": "2026-03-01T15:00:00.000Z",
"customer": {
"id": "cus_abc123",
"name": "Ana Pérez",
"email": "[email protected]",
"phone": "8090000000"
},
"store": {
"id": "sto_abc123",
"name": "Mi Tienda",
"timezone": "America/Santo_Domingo"
}
}checkout.abandoned
Se dispara cuando un borrador de checkout se marca como abandonado tras un periodo de inactividad (un trabajo en segundo plano evalúa los borradores periódicamente). Usa checkout.recovery_url para traer de vuelta al comprador a su carrito.
Función premium: este evento requiere que el plan de la tienda incluya
Carritos Abandonados. Suscribirse sin ella devuelve 403 FEATURE_ACCESS_DENIED, y las entregas se pausan automáticamente mientras la
tienda esté en un plan sin la función (el plan se verifica en cada disparo).
Los checkouts abandonados también pueden leerse bajo demanda vía GET /v1/checkout-drafts (scope read:checkout_drafts, mismo requisito de plan).
El event_id es el identificador público del checkout abandonado y coincide con checkout.id. A diferencia de los eventos de pedido, customer no tiene id — un borrador abandonado se captura antes de que exista un registro de cliente, por lo que solo lleva los campos de contacto (nulos permitidos). Siempre está presente email o phone.
{
"event_id": "chk_abc123",
"occurred_at": "2026-03-01T15:30:00.000Z",
"checkout": {
"id": "chk_abc123",
"status": "abandoned",
"total": 8997,
"subtotal": 8997,
"shipping": 0,
"discount": 0,
"currency": "DOP",
"recovery_url": "https://mitienda.example/r/RECOVERYTOKEN",
"created_at": "2026-03-01T15:00:00.000Z",
"abandoned_at": "2026-03-01T15:30:00.000Z",
"shipping_address": {
"name": "Ana Perez",
"phone": "8090000000",
"address": "Calle 1 #2",
"city": "Santo Domingo",
"province": "Distrito Nacional",
"country": "República Dominicana",
"postal_code": "10101",
"notes": null
},
"items": [
{
"product_id": "prd_abc123",
"product_name": "Starter Kit",
"variant_name": "Color: Rojo",
"quantity": 2,
"unit_price": 4498.5,
"total_price": 8997
}
]
},
"customer": {
"name": "Ana Perez",
"email": "[email protected]",
"phone": "8090000000"
},
"store": {
"id": "sto_abc123",
"name": "Mi Tienda",
"timezone": "America/Santo_Domingo"
}
}Cuando el borrador abandonado capturó order bumps, el webhook también incluye checkout.order_bumps con { total, items: [{ bump_id, title, price }] }. La clave se omite cuando no hubo ninguno.
Los campos monetarios como total, subtotal, shipping, discount y price usan unidades mayores decimales, no unidades menores ni centavos. Ejemplo: 1500.50 significa 1500.50 en la moneda del payload.
Seguridad de Webhooks
Cada entrega de webhook incluye estos headers:
| Header | Descripción |
|---|---|
X-Webhook-Id | ID único de entrega. Guárdalo para idempotencia. |
X-Webhook-Timestamp | Timestamp Unix en segundos. |
X-Webhook-Signature | Digest hexadecimal HMAC-SHA256. |
X-Webhook-Event | Nombre del evento, por ejemplo order.created. |
La firma se calcula así:
HMAC-SHA256(secret, `${timestamp}.${rawBody}`) -> hexUsa el body raw exactamente como fue recibido, antes de hacer JSON.parse. Rechaza timestamps con más de 300 segundos de antigüedad para reducir el riesgo de replay.
Para idempotencia, guarda X-Webhook-Id y omite entregas duplicadas. Para deduplicación de negocio, también puedes combinar X-Webhook-Event con el event_id del body.
Ejemplo de Verificación
import crypto from "crypto";
function verifyWebhookSignature(rawBody, headers, secret) {
const timestamp = headers["x-webhook-timestamp"];
const signature = headers["x-webhook-signature"];
if (!timestamp || !signature || !/^[a-f0-9]{64}$/i.test(signature)) return false;
const timestampValue = Number(timestamp);
if (!Number.isInteger(timestampValue)) return false;
const ageSeconds = Math.abs(Math.floor(Date.now() / 1000) - timestampValue);
if (ageSeconds > 300) return false;
const expected = crypto
.createHmac("sha256", secret)
.update(`${timestamp}.${rawBody}`, "utf8")
.digest("hex");
try {
return crypto.timingSafeEqual(
Buffer.from(signature, "hex"),
Buffer.from(expected, "hex")
);
} catch {
return false;
}
}Política de Reintentos
Si tu endpoint devuelve un código de estado diferente a 2xx, Whatalo reintenta con backoff exponencial:
| Intento | Demora antes del intento |
|---|---|
| 2do intento | 1 segundo |
| 3er intento | 2 segundos |
Las respuestas 4xx se tratan como errores del cliente y no se reintentan. Las respuestas 5xx y los errores de red se reintentan hasta 3 intentos totales.