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/nextjspnpm add @better-webhook/github @better-webhook/nextjsyarn add @better-webhook/github @better-webhook/nextjsnpm install @better-webhook/github @better-webhook/expresspnpm add @better-webhook/github @better-webhook/expressyarn add @better-webhook/github @better-webhook/express npm install @better-webhook/github @better-webhook/nestjs pnpm add @better-webhook/github @better-webhook/nestjsyarn add @better-webhook/github @better-webhook/nestjsnpm install @better-webhook/ragie @better-webhook/gcp-functionspnpm add @better-webhook/ragie @better-webhook/gcp-functionsyarn add @better-webhook/ragie @better-webhook/gcp-functionsnpm install @better-webhook/github @better-webhook/honopnpm add @better-webhook/github @better-webhook/honoyarn add @better-webhook/github @better-webhook/honoIf you also use builder-level observability, install @better-webhook/otel too.
Quick Example
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);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);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();
}
}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));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:
- Adapter options — Pass
secretto the adapter function - Provider options — Pass
secretwhen creating the provider - Environment variables — Automatically checks
<PROVIDER>_WEBHOOK_SECRET, thenWEBHOOK_SECRETas a fallback (for exampleGITHUB_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 environmentVerification 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
- Providers — GitHub, Stripe, Ragie, Recall.ai, and Resend provider documentation with all supported events
- Custom Providers — Build provider integrations for any webhook source
- Replay and Idempotency — Prevent duplicate processing with replay protection
- Adapters — Framework-specific setup and configuration
- NestJS Adapter — Raw body and response handling for NestJS
- GCP Functions Adapter — Cloud Functions setup for 1st and 2nd gen
- Adapter Options — Shared options for secrets, limits, and callbacks