Wocha Docs
Examples

Express webhook handler

Express middleware for verified webhook delivery

Example Express middleware that verifies Wocha webhook signatures and dispatches by event type.

Setup

npm install @wocha/sdk express

Add your signing secret to .env:

WOCHA_WEBHOOK_SECRET=whsec_...

Middleware

import express from "express";
import { constructWebhookEvent } from "@wocha/sdk/webhooks";
import type { WebhookEvent } from "@wocha/sdk/webhooks";
 
const app = express();
 
// Register the webhook route BEFORE express.json() — raw body is required
app.post(
  "/webhooks/wocha",
  express.raw({ type: "application/json" }),
  async (req, res) => {
    const payload = req.body.toString("utf8");
    const signature = (req.headers["x-wocha-signature"] as string | undefined) ?? "";
 
    let event: WebhookEvent;
    try {
      event = await constructWebhookEvent(
        payload,
        signature,
        process.env.WOCHA_WEBHOOK_SECRET!,
      );
    } catch {
      return res.status(401).send("Invalid signature");
    }
 
    try {
      await handleWebhookEvent(event);
      return res.status(200).send("OK");
    } catch (err) {
      console.error("[webhook] handler error:", err);
      return res.status(500).send("Handler failed");
    }
  },
);
 
// Other routes can use JSON parsing
app.use(express.json());
 
async function handleWebhookEvent(event: WebhookEvent): Promise<void> {
  switch (event.event_type) {
    case "user.created":
      await provisionAccount(event.data);
      break;
    case "user.updated":
      await syncUserProfile(event.data);
      break;
    case "user.deleted":
      await deleteAccount(event.data.id);
      break;
    case "session.created":
      await trackSession(event.data);
      break;
    case "session.revoked":
      await invalidateSessionCache(event.data.id);
      break;
    case "org.created":
      await createOrganisationRecord(event.data);
      break;
    case "org.member.added":
      await syncMembership(event.data);
      break;
    case "org.member.removed":
      await removeMembership(event.data);
      break;
    case "connection.created":
      await syncConnection(event.data);
      break;
    case "application.created":
      await registerApplication(event.data);
      break;
    case "application.deleted":
      await unregisterApplication(event.data.client_id);
      break;
    default:
      console.log(`[webhook] unhandled event: ${event.event_type}`);
  }
}
 
async function provisionAccount(data: { id: string; email: string }) {
  // Your provisioning logic
}
 
async function syncUserProfile(data: { id: string }) {
  // Your sync logic
}
 
async function deleteAccount(userId: string) {
  // Your cleanup logic
}
 
async function trackSession(data: { id: string; identity_id: string }) {
  // Your analytics logic
}
 
async function invalidateSessionCache(sessionId: string) {
  // Your cache invalidation logic
}
 
async function createOrganisationRecord(data: { id: string; slug: string }) {
  // Your org sync logic
}
 
async function syncMembership(data: { org_id: string; identity_id: string; role: string }) {
  // Your membership sync logic
}
 
async function removeMembership(data: { org_id: string; identity_id: string }) {
  // Your membership cleanup logic
}
 
async function syncConnection(data: { id: string; type: string; provider: string }) {
  // Your connection sync logic
}
 
async function registerApplication(data: { client_id: string; client_name: string }) {
  // Your app registry logic
}
 
async function unregisterApplication(clientId: string) {
  // Your app cleanup logic
}
 
app.listen(3000, () => {
  console.log("Listening on http://localhost:3000");
});

Error handling pattern

For production handlers, separate signature errors from business logic errors:

import { GreetError } from "@wocha/sdk";
 
app.post("/webhooks/wocha", express.raw({ type: "application/json" }), async (req, res) => {
  const payload = req.body.toString("utf8");
  const signature = (req.headers["x-wocha-signature"] as string) ?? "";
 
  try {
    const event = await constructWebhookEvent(payload, signature, process.env.WOCHA_WEBHOOK_SECRET!);
    await handleWebhookEvent(event);
    return res.status(200).json({ received: true });
  } catch (err) {
    if (err instanceof GreetError && err.code === "unauthorized") {
      return res.status(401).json({ error: "invalid_signature" });
    }
    console.error("[webhook]", err);
    return res.status(500).json({ error: "handler_error" });
  }
});

Important notes

  1. Raw body required — use express.raw({ type: "application/json" }) on the webhook route only.
  2. Route order matters — register the webhook route before express.json().
  3. Return 2xx quickly — offload heavy processing to a queue.
  4. Idempotency — deduplicate by event_id before side effects.

Local testing

wocha webhook listen --forward-to http://localhost:3000/webhooks/wocha
wocha webhook test http://localhost:3000/webhooks/wocha user.created

On this page