Skip to content

Quick Start

This guide walks you through adding webhooks to an existing application. By the end, you’ll have an event defined, an endpoint registered, a webhook delivered, and its signature verified.

  • A running PostgreSQL 13+ database
  • Node.js 22+
  • An existing application with a database connection
  1. Install

    Terminal window
    npm install @usebetterdev/webhook @usebetterdev/webhook-verify zod

    Install the adapter for your ORM:

    Terminal window
    npm install @usebetterdev/webhook-drizzle

    You’ll install a framework adapter in step 5.

  2. Run migrations

    Terminal window
    npx @usebetterdev/webhook-cli migrate --database-url $DATABASE_URL

    This creates the webhook_endpoints, webhook_deliveries, and webhook_delivery_attempts tables.

  3. Define an event

    src/webhook/events.ts
    import { z } from "zod";
    import type { EventMap } from "@usebetterdev/webhook";
    export const events = {
    "user.created": {
    description: "A new user was created",
    schema: z.object({
    id: z.string(),
    email: z.string(),
    name: z.string(),
    }),
    },
    } satisfies EventMap;
  4. Create the webhook instance

    src/webhook/instance.ts
    import { drizzle } from "drizzle-orm/node-postgres";
    import { Pool } from "pg";
    import { webhook, PollingRunner, parseEncryptionKeyFromEnv } from "@usebetterdev/webhook";
    import { drizzleWebhookAdapter } from "@usebetterdev/webhook/drizzle";
    import { events } from "./events.js";
    const pool = new Pool({ connectionString: process.env.DATABASE_URL });
    const db = drizzle(pool);
    const encryption = parseEncryptionKeyFromEnv(process.env.WEBHOOK_ENCRYPTION_KEY!);
    const adapter = drizzleWebhookAdapter(db);
    const runner = new PollingRunner({
    adapter,
    encryption,
    interval: 2000,
    concurrency: 5,
    });
    export const webhookInstance = webhook({
    events,
    adapter,
    jobRunner: runner,
    encryption,
    });
    runner.start();
    process.on("SIGTERM", () => void runner.stop());
    process.on("SIGINT", () => void runner.stop());
  5. Mount the API routes

    Terminal window
    npm install @usebetterdev/webhook-hono
    src/index.ts
    import { Hono } from "hono";
    import { createWebhookMiddleware } from "@usebetterdev/webhook-hono";
    import { webhookInstance } from "./webhook/instance.js";
    const app = new Hono();
    app.use("*", createWebhookMiddleware(webhookInstance, {
    basePath: "/api/webhooks",
    }));
    export default app;
  6. Register an endpoint

    Start your server, then create an endpoint that subscribes to user.created events:

    Terminal window
    curl -s -X POST http://localhost:3000/api/webhooks/endpoints \
    -H "Content-Type: application/json" \
    -d '{
    "url": "https://example.com/my-webhook",
    "events": ["user.created"]
    }'
    Response
    {
    "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
    "url": "https://example.com/my-webhook",
    "events": ["user.created"],
    "secret": "5f2b9a3e1c7d84f09b3e6a2d5c8f1e7a4b0d6c9f2a5e8b1d4f7a0c3e6b9d2f5",
    "status": "active",
    "createdAt": "2026-03-16T14:30:00.000Z",
    "updatedAt": "2026-03-16T14:30:00.000Z"
    }
  7. Send your first event

    Trigger an event from a route handler in your application:

    src/index.ts
    import { webhookInstance } from "./webhook/instance.js";
    app.post("/users", async (c) => {
    const user = { id: "usr_001", email: "[email protected]", name: "Alice" };
    // ... save user to database ...
    await webhookInstance.send("user.created", user);
    return c.json(user, 201);
    });

    The PollingRunner picks up the delivery and POSTs it to every subscribed endpoint.

  8. Verify delivery

    Check the delivery log via the API (use the endpoint ID from step 6):

    Terminal window
    curl -s "http://localhost:3000/api/webhooks/deliveries?endpointId=a1b2c3d4-e5f6-7890-abcd-ef1234567890"
    Response
    {
    "data": [
    {
    "id": "d8e9f0a1-b2c3-4567-890a-bcdef1234567",
    "endpointId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
    "eventName": "user.created",
    "status": "delivered",
    "attempts": 1,
    "maxAttempts": 5,
    "createdAt": "2026-03-16T14:30:01.000Z",
    "updatedAt": "2026-03-16T14:30:01.000Z"
    }
    ]
    }

    A "status": "delivered" confirms the webhook reached your endpoint.

  9. Verify the signature (consumer side)

    On the receiving end, verify that the webhook payload is authentic using @usebetterdev/webhook-verify. The assertWebhookSignature function throws a WebhookSignatureError if verification fails. Use verifyWebhookSignature instead if you prefer a boolean return.

    consumer/src/index.ts
    import { Hono } from "hono";
    import { assertWebhookSignature } from "@usebetterdev/webhook-verify";
    const app = new Hono();
    app.post("/my-webhook", async (c) => {
    await assertWebhookSignature({
    headers: Object.fromEntries(c.req.raw.headers),
    body: await c.req.text(),
    secret: process.env.WEBHOOK_SECRET!, // the secret from step 6
    });
    const event = await c.req.json();
    console.log("Received:", event.type, event.data);
    return c.json({ received: true });
    });

    The signature is verified using HMAC-SHA256 with the X-Webhook-Signature and X-Webhook-Timestamp headers. If the signature is invalid or the timestamp is too old (default: 5 minutes), verification fails.

  1. You defined a user.created event with a typed Zod schema.
  2. The webhook instance registered the event and connected to your database via the ORM adapter.
  3. When you called webhookInstance.send(), a delivery row was created for each subscribed endpoint.
  4. The PollingRunner picked up pending deliveries, signed the payload with HMAC-SHA256 using the endpoint’s secret, and POSTed it with X-Webhook-ID, X-Webhook-Timestamp, and X-Webhook-Signature headers.
  5. Failed deliveries are retried automatically with exponential backoff (up to 5 attempts by default).
  • Configuration — encryption keys, retry strategies, batch size, hooks
  • Defining Events — event schemas, payload validation, event catalog
  • Security — signature scheme details, key rotation, HTTPS enforcement