Wocha Docs
Examples

Next.js webhook handler

App Router route that verifies signatures and dispatches events

Example App Router route that verifies Wocha webhook signatures and dispatches by event type.

Setup

npm install @wocha/sdk

Add your signing secret to .env.local:

WOCHA_WEBHOOK_SECRET=whsec_...

Route handler

Create app/api/webhooks/wocha/route.ts:

import { constructWebhookEvent } from "@wocha/sdk/webhooks";
import type { WebhookEvent } from "@wocha/sdk/webhooks";
 
export const runtime = "nodejs";
 
export async function POST(req: Request) {
  const payload = await req.text();
  const signature = req.headers.get("x-wocha-signature") ?? "";
 
  let event: WebhookEvent;
  try {
    event = await constructWebhookEvent(
      payload,
      signature,
      process.env.WOCHA_WEBHOOK_SECRET!,
    );
  } catch {
    return new Response("Invalid signature", { status: 401 });
  }
 
  try {
    await handleWebhookEvent(event);
  } catch (err) {
    console.error("[webhook] handler error:", err);
    return new Response("Handler failed", { status: 500 });
  }
 
  return new Response("OK", { status: 200 });
}
 
async function handleWebhookEvent(event: WebhookEvent): Promise<void> {
  const type = event.event_type;
 
  if (type === "user.created") {
    await provisionAccount(event.data);
    return;
  }
  if (type === "user.updated") {
    await syncUserProfile(event.data);
    return;
  }
  if (type === "user.deleted") {
    await deleteAccount(event.data.id);
    return;
  }
  if (type === "session.created") {
    await trackSession(event.data);
    return;
  }
  if (type === "session.revoked") {
    await invalidateSessionCache(event.data.id);
    return;
  }
  if (type === "org.created") {
    await createOrganisationRecord(event.data);
    return;
  }
  if (type === "org.member.added") {
    await syncMembership(event.data);
    return;
  }
  if (type === "org.member.removed") {
    await removeMembership(event.data);
    return;
  }
  if (type === "application.created") {
    await registerApplication(event.data);
    return;
  }
  if (type === "application.deleted") {
    await unregisterApplication(event.data.client_id);
    return;
  }
 
  console.log(`[webhook] unhandled 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 registerApplication(data: { client_id: string; client_name: string }) {
  // Your app registry logic
}
 
async function unregisterApplication(clientId: string) {
  // Your app cleanup logic
}

Important notes

  1. Use the raw body — call req.text() before parsing JSON. Next.js must not pre-parse the body for this route.
  2. Return 2xx quickly — queue heavy work to a background job; Wocha retries on timeout.
  3. Deduplicate — track processed event_id or idempotency_key values to handle at-least-once delivery.
  4. Legacy event names — some events arrive as org.*; handle both forms or normalize before switching.

Local testing

# Terminal 1 — start your Next.js app
npm run dev
 
# Terminal 2 — forward platform webhooks
wocha webhook listen --forward-to http://localhost:3000/api/webhooks/wocha
 
# Terminal 3 — send a test payload
wocha webhook test http://localhost:3000/api/webhooks/wocha user.created

On this page