Webhooks

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

OptionTypeRequiredDescription
secretstringYesYour plugin's client secret, used for HMAC signature verification
handlersobjectYesMap of event type string → async handler function
onUnhandledEventfunctionNoCatch-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.

On this page