Handling Webhooks
Use SDK-provided webhook handlers for Next.js, Hono, and Express to receive and process Whatalo events with built-in signature verification.
@whatalo/plugin-sdk ships framework-specific webhook handlers that handle signature verification, JSON parsing, and event routing for you. Pick the adapter that matches your server framework.
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) => {
console.log("New order:", payload.data.id);
// Trigger fulfilment, send confirmation email, etc.
},
"order.status_changed": async (payload) => {
console.log("Status changed:", payload.data.status);
},
"product.updated": async (payload) => {
console.log("Product changed:", payload.data.id);
// Sync with your catalogue cache
},
"app.installed": async (payload) => {
// Provision resources for the new merchant
await provisionMerchant(payload.store);
},
"app.uninstalled": async (payload) => {
// Clean up stored merchant data
await cleanupMerchant(payload.store);
},
},
onUnhandledEvent: async (event, payload) => {
// Called for any event not listed in handlers above
console.log(`Unhandled event: ${event}`);
},
});The handler reads the raw request body before parsing, which is required for correct HMAC verification. Do not wrap this route in any body-parsing middleware.
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.data);
},
"customer.created": async (payload) => {
await syncCustomer(payload.data);
},
},
});
app.post("/api/webhooks", handler);
export default app;Express
import express from "express";
import { createWebhookHandler } from "@whatalo/plugin-sdk/adapters/express";
const app = express();
// Mount BEFORE any global body-parsing middleware on this route
const handler = createWebhookHandler({
secret: process.env.WHATALO_CLIENT_SECRET!,
handlers: {
"order.created": async (payload) => {
await processNewOrder(payload.data);
},
},
});
app.post("/api/webhooks", handler);If you use express.json() globally, exclude the webhook path:
// Apply JSON parsing to all routes except /api/webhooks
app.use((req, res, next) => {
if (req.path === "/api/webhooks") return next();
express.json()(req, res, next);
});Handler Options
| Option | Type | Required | Description |
|---|---|---|---|
secret | string | Yes | Your plugin's client secret, used for HMAC signature verification |
handlers | object | Yes | Map of event type string → async handler function |
onUnhandledEvent | function | No | Catch-all called for events not in handlers |
Handler Signature
Each handler receives a single typed payload argument:
type WebhookPayload = {
id: string; // Unique delivery ID (X-Webhook-ID)
event: string; // Event type (X-Webhook-Event)
store: string; // Store identifier
timestamp: string; // ISO 8601 timestamp
data: unknown; // Event-specific data (type depends on event)
};Error Handling in Handlers
If a handler throws an error, the adapter catches it, logs the stack trace, and returns 500 to trigger a Whatalo retry. To silently ignore an event without triggering a retry, return normally without throwing:
"order.created": async (payload) => {
const order = payload.data 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) });
},Manual Verification (No Framework Adapter)
If you are using a framework not listed here, verify signatures manually using 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,
secret: process.env.WHATALO_CLIENT_SECRET!,
});
if (!isValid) {
return res.status(401).json({ error: "Invalid signature" });
}
const event = JSON.parse(rawBody);See Verification & Security for full details on the signature algorithm and replay protection.