Manejo de Webhooks
Usa los handlers de webhooks del SDK para Next.js, Hono y Express para recibir y procesar eventos de Whatalo con verificación de firma integrada.
@whatalo/plugin-sdk incluye handlers de webhooks específicos para cada framework que manejan la verificación de firma, el parseo de JSON y el enrutamiento de eventos por ti. Elige el adaptador que corresponda a tu framework de servidor.
Next.js (App Router)
// app/api/webhooks/route.ts
import { createWebhookHandler } from "@whatalo/plugin-sdk/adapters/nextjs";
export const POST = createWebhookHandler({
secret: process.env.WHATALO_CLIENT_SECRET!,
handlers: {
"order.created": async (payload, context) => {
console.log("New order:", payload.order.id);
console.log("Delivery:", context.deliveryId);
// Trigger fulfilment, send confirmation email, etc.
},
"order.updated": async (payload) => {
console.log("Status changed:", payload.order.status);
},
"product.updated": async (payload) => {
console.log("Product changed:", payload.product.id);
// Sync with your catalogue cache
},
},
onUnhandledEvent: async (event, payload, context) => {
// Called for any event not listed in handlers above
console.log(`Unhandled event: ${event}`, context.deliveryId);
},
});El handler lee el cuerpo de la solicitud sin procesar antes de parsearlo, lo cual es necesario para la verificación HMAC correcta. No envuelvas esta ruta en ningún middleware de parseo de body.
Hono
import { Hono } from "hono";
import { createWebhookHandler } from "@whatalo/plugin-sdk/adapters/hono";
const app = new Hono();
const handler = createWebhookHandler({
secret: process.env.WHATALO_CLIENT_SECRET!,
handlers: {
"order.created": async (payload) => {
await processNewOrder(payload.order);
},
"customer.created": async (payload) => {
await syncCustomer(payload.customer);
},
},
});
app.post("/api/webhooks", handler);
export default app;Express
import express from "express";
import { createWebhookHandler } from "@whatalo/plugin-sdk/adapters/express";
const app = express();
const handler = createWebhookHandler({
secret: process.env.WHATALO_CLIENT_SECRET!,
handlers: {
"order.created": async (payload) => {
await processNewOrder(payload.order);
},
},
});
// El adapter verifica la firma sobre el body raw.
// No uses express.json() en esta ruta.
app.post("/api/webhooks", express.raw({ type: "application/json" }), handler);Si usas express.json() globalmente, monta la ruta raw de webhooks antes del parser global:
app.post("/api/webhooks", express.raw({ type: "application/json" }), handler);
app.use(express.json());Opciones del Handler
| Opción | Tipo | Requerido | Descripción |
|---|---|---|---|
secret | string | Sí | El secreto de cliente de tu plugin, usado para verificación de firma HMAC |
handlers | object | Sí | Mapa de tipo de evento → función handler async |
onUnhandledEvent | function | No | Catch-all llamado para eventos no listados en handlers |
Firma del Handler
Cada handler recibe un payload tipado y metadata de entrega tomada de los headers de la solicitud:
type WebhookPayload = {
event_id?: string; // Identificador de evento de negocio para idempotencia
occurred_at?: string; // Timestamp ISO 8601 del evento
store: { id: string; name?: string; timezone: string };
order?: unknown; // Presente en eventos de pedido
product?: unknown; // Presente en eventos de producto
customer?: unknown; // Presente en eventos de cliente
};
type WebhookHandlerContext = {
deliveryId: string; // X-Webhook-Id
event: string; // X-Webhook-Event
timestamp: string; // X-Webhook-Timestamp
};El nombre del evento no está en el body. El adaptador lee X-Webhook-Event para seleccionar el handler correspondiente y lo pasa como context.event.
Manejo de Errores en Handlers
Si un handler lanza un error, el adaptador lo captura y devuelve 500 para activar un reintento de Whatalo. Si necesitas detalles diagnósticos, regístralos dentro de tu handler antes de lanzar. Para ignorar silenciosamente un evento sin activar un reintento, retorna normalmente sin lanzar:
"order.created": async (payload) => {
const order = payload.order as OrderPayload;
// If the order is already in our system, skip silently
const exists = await db.orders.findUnique({ where: { id: order.id } });
if (exists) return; // Returns 200 OK — no retry
// Process the new order
await db.orders.create({ data: mapOrder(order) });
},Verificación Manual (Sin Adaptador de Framework)
Si usas un framework no listado aquí, verifica las firmas manualmente usando verifyWebhook:
import { verifyWebhook } from "@whatalo/plugin-sdk/webhooks";
// rawBody must be the raw string body — read it before JSON.parse
const isValid = verifyWebhook({
payload: rawBody,
signature: req.headers["x-webhook-signature"] as string,
timestamp: req.headers["x-webhook-timestamp"] as string,
secret: process.env.WHATALO_CLIENT_SECRET!,
});
if (!isValid) {
return res.status(401).json({ error: "Invalid signature" });
}
const payload = JSON.parse(rawBody);
const event = req.headers["x-webhook-event"];Consulta Verificación y Seguridad para más detalles sobre el algoritmo de firma y la protección contra replay attacks.