>_better-webhook
SDK

SDK Getting Started

Build type-safe webhook handlers with automatic signature verification using Better Webhook SDK packages.

SDK Getting Started

The Better Webhook SDK provides type-safe webhook handlers with automatic signature verification. It consists of:

  • Provider packages — Type definitions and schemas for webhook sources (GitHub, Stripe, Ragie, Recall.ai, Resend)
  • Adapter packages — Framework integrations (Next.js, Hono, Express, NestJS, GCP Cloud Functions)
  • Core package — Base functionality for custom providers

Installation

Install a provider and an adapter for your framework:

npm install @better-webhook/github @better-webhook/nextjs
pnpm add @better-webhook/github @better-webhook/nextjs
yarn add @better-webhook/github @better-webhook/nextjs
npm install @better-webhook/github @better-webhook/express
pnpm add @better-webhook/github @better-webhook/express
yarn add @better-webhook/github @better-webhook/express 
npm install @better-webhook/github @better-webhook/nestjs 
pnpm add @better-webhook/github @better-webhook/nestjs
yarn add @better-webhook/github @better-webhook/nestjs
npm install @better-webhook/ragie @better-webhook/gcp-functions
pnpm add @better-webhook/ragie @better-webhook/gcp-functions
yarn add @better-webhook/ragie @better-webhook/gcp-functions
npm install @better-webhook/github @better-webhook/hono
pnpm add @better-webhook/github @better-webhook/hono
yarn add @better-webhook/github @better-webhook/hono

If you also use builder-level observability, install @better-webhook/otel too.

Quick Example

app/api/webhooks/github/route.ts
import { github } from "@better-webhook/github";
import { push, pull_request } from "@better-webhook/github/events";
import { toNextJS } from "@better-webhook/nextjs";

const webhook = github()
  .event(push, async (payload) => {
    // payload is fully typed!
    console.log(`Push to ${payload.repository.name}`);
    console.log(`${payload.commits.length} commits`);
  })
  .event(pull_request, async (payload) => {
    if (payload.action === "opened") {
      console.log(`New PR: ${payload.pull_request.title}`);
    }
  });

export const POST = toNextJS(webhook);
src/webhooks.ts
import express from "express";
import { github } from "@better-webhook/github";
import { push } from "@better-webhook/github/events";
import { toExpress } from "@better-webhook/express";

const app = express();

const webhook = github()
  .event(push, async (payload) => {
    console.log(`Push to ${payload.repository.name}`);
  });

// Important: use express.raw() for signature verification
app.post(
  "/webhooks/github",
  express.raw({ type: "application/json" }),
  toExpress(webhook)
);

app.listen(3000);
src/webhooks.controller.ts
import { Controller, Post, Req, Res } from "@nestjs/common";
import { Request, Response } from "express";
import { github } from "@better-webhook/github";
import { push } from "@better-webhook/github/events";
import { toNestJS } from "@better-webhook/nestjs";

@Controller("webhooks")
export class WebhooksController {
  private webhook = github()
    .event(push, async (payload) => {
      console.log(`Push to ${payload.repository.name}`);
    });

  @Post("github")
  async handleGitHub(@Req() req: Request, @Res() res: Response) {
    const result = await toNestJS(this.webhook)(req);
  if (result.body) {
    return res.status(result.statusCode).json(result.body);
  }
  return res.status(result.statusCode).end();
  }
}
index.ts
import { http } from "@google-cloud/functions-framework";
import { ragie } from "@better-webhook/ragie";
import { document_status_updated, connection_sync_finished } from "@better-webhook/ragie/events";
import { toGCPFunction } from "@better-webhook/gcp-functions";

const webhook = ragie()
  .event(document_status_updated, async (payload) => {
    // payload is fully typed!
    console.log(`Document ${payload.document_id} is now ${payload.status}`);
  })
  .event(connection_sync_finished, async (payload) => {
    console.log(`Sync ${payload.sync_id} completed`);
  });

http("webhookHandler", toGCPFunction(webhook));
src/webhooks.ts
import { Hono } from "hono";
import { github } from "@better-webhook/github";
import { push } from "@better-webhook/github/events";
import { toHono } from "@better-webhook/hono";

const app = new Hono();

const webhook = github()
  .event(push, async (payload) => {
    console.log(`Push to ${payload.repository.name}`);
  });

app.post("/webhooks/github", toHono(webhook));

export default app;

Tree-Shaking

Events are exported separately from each provider's /events subpath, enabling bundlers to tree-shake unused events:

// Only the push schema is included in your bundle
import { push } from "@better-webhook/github/events";

// Multiple events - only these schemas are included
import { push, pull_request } from "@better-webhook/github/events";

This is especially beneficial for serverless deployments where bundle size matters.

How It Works

Create a Webhook Builder

Use a provider function (like github()) to create a webhook builder:

import { github } from "@better-webhook/github";

const webhook = github();

Import Events

Import the specific events you want to handle from the /events subpath:

import { push, issues } from "@better-webhook/github/events";

Register Event Handlers

Chain .event() calls to register handlers for specific event types:

const webhook = github()
  .event(push, async (payload) => {
    // Handle push events
  })
  .event(issues, async (payload) => {
    // Handle issues events
  });

Each handler receives a fully typed payload with autocomplete support.

Convert to Framework Handler

Use an adapter to convert the webhook builder to your framework's handler format:

// Next.js
export const POST = toNextJS(webhook);

// Express
app.post("/webhooks/github", express.raw({ type: "application/json" }), toExpress(webhook));

// NestJS
const result = await toNestJS(this.webhook)(req);

// GCP Cloud Functions
http("webhookHandler", toGCPFunction(webhook));

// Hono
app.post("/webhooks/github", toHono(webhook));

Signature Verification

Signature verification happens automatically when you provide a secret. The SDK looks for secrets in this order:

  1. Adapter options — Pass secret to the adapter function
  2. Provider options — Pass secret when creating the provider
  3. Environment variables — Automatically checks <PROVIDER>_WEBHOOK_SECRET, then WEBHOOK_SECRET as a fallback (for example GITHUB_WEBHOOK_SECRET)
// Option 1: Adapter options
export const POST = toNextJS(webhook, {
  secret: process.env.GITHUB_WEBHOOK_SECRET,
});

// Option 2: Provider options
const webhook = github({
  secret: process.env.GITHUB_WEBHOOK_SECRET,
});

// Option 3: Environment variable (automatic)
// Just set GITHUB_WEBHOOK_SECRET in your environment

Verification is evaluated before unhandled-event routing. Requests for unknown event types still need to pass signature verification before receiving an acknowledgement status (204 by default, 200 for providers such as Resend).

Always configure signature verification in production. Without it, anyone can send fake webhooks to your endpoint.

Replay Protection

Use core replay protection to enforce deduplication with a pluggable store:

import { createInMemoryReplayStore } from "@better-webhook/core";
import { github } from "@better-webhook/github";
import { push } from "@better-webhook/github/events";

const replayStore = createInMemoryReplayStore();

const webhook = github()
  .withReplayProtection({ store: replayStore })
  .event(push, async (payload) => {
    console.log(`Push to ${payload.repository.full_name}`);
  });

Default duplicate behavior is 409.

Error Handling

Register error handlers to catch validation and handler errors:

import { push } from "@better-webhook/github/events";

const webhook = github()
  .event(push, async (payload) => {
    // Your handler
  })
  .onError((error, context) => {
    console.error(`Error in ${context.eventType}:`, error);
    // Log to monitoring service, etc.
  })
  .onVerificationFailed((reason, headers) => {
    console.warn("Signature verification failed:", reason);
    // Alert on potential attacks
  });

Observability

Add tracing and metrics with @better-webhook/otel at the builder level:

import { github } from "@better-webhook/github";
import { push } from "@better-webhook/github/events";
import { createOpenTelemetryInstrumentation } from "@better-webhook/otel";
import { toNextJS } from "@better-webhook/nextjs";

const webhook = github()
  .instrument(
    createOpenTelemetryInstrumentation({
      includeEventTypeAttribute: true,
    }),
  )
  .event(push, async (payload) => {
    console.log(`Push to ${payload.repository.name}`);
  });

export const POST = toNextJS(webhook);

Use builder-level instrumentation for all frameworks. Adapters do not accept observability plugins.

@better-webhook/otel wires the builder into the OpenTelemetry API. To actually export traces and metrics, your application still needs to register an OpenTelemetry SDK and exporters.

See OpenTelemetry for configuration details and cardinality guidance.

Next Steps

On this page