🎣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, Ragie)
  • Adapter packages — Framework integrations (Next.js, Express, NestJS, GCP Cloud Functions)
  • Core package — Base functionality for custom providers

Installation

Install a provider and an adapter for your framework:

bash npm install @better-webhook/github @better-webhook/nextjs

bash pnpm add @better-webhook/github @better-webhook/nextjs

bash yarn add @better-webhook/github @better-webhook/nextjs

bash npm install @better-webhook/github @better-webhook/express

bash pnpm add @better-webhook/github @better-webhook/express

bash yarn add @better-webhook/github @better-webhook/express

bash npm install @better-webhook/github @better-webhook/nestjs

bash pnpm add @better-webhook/github @better-webhook/nestjs

bash yarn add @better-webhook/github @better-webhook/nestjs

bash pnpm add @better-webhook/ragie @better-webhook/gcp-functions

bash yarn add @better-webhook/ragie @better-webhook/gcp-functions

Quick Example

app/api/webhooks/github/route.ts
import { github } from "@better-webhook/github";
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 { 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 { 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);
    return res.status(result.statusCode).json(result.body);
  }
}
index.ts
import { http } from "@google-cloud/functions-framework";
import { ragie } from "@better-webhook/ragie";
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));

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();

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));

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 GITHUB_WEBHOOK_SECRET, RAGIE_WEBHOOK_SECRET, etc.
// 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

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

Error Handling

Register error handlers to catch validation and handler errors:

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 metrics, logging, and tracing to your webhook handlers with the built-in observer API:

import { github } from "@better-webhook/github";
import { createWebhookStats } from "@better-webhook/core";
import { toNextJS } from "@better-webhook/nextjs";

// Built-in stats collector
const stats = createWebhookStats();

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

export const POST = toNextJS(webhook);

// Access stats anytime
console.log(stats.snapshot());
// { totalRequests: 150, successCount: 145, errorCount: 5, ... }

Custom Observers

Create custom observers to integrate with your metrics infrastructure:

import { type WebhookObserver } from "@better-webhook/core";

const metricsObserver: WebhookObserver = {
  onCompleted: (event) => {
    metrics.histogram("webhook_duration_ms", event.durationMs, {
      provider: event.provider,
      eventType: event.eventType,
      status: String(event.status),
    });
  },
  onHandlerFailed: (event) => {
    logger.error("Webhook handler failed", {
      error: event.error.message,
      provider: event.provider,
      eventType: event.eventType,
    });
  },
};

const webhook = github().observe(metricsObserver).event("push", handler);

You can also add observers at the adapter level:

export const POST = toNextJS(webhook, {
  observer: metricsObserver,
});

Next Steps

  • Providers — GitHub and Ragie provider documentation with all supported events
  • Adapters — Framework-specific setup and configuration

On this page