Node.js Webhooks
The Node.js client can receive webhooks for you. It checks the signature, skips webhooks you have already handled and calls your code for each event type.
Creating the handler
Section titled “Creating the handler”import { handler, WebhookEventTypes } from "@divergent.dev/connect";
const handle = handler({ secrets: [process.env.CONNECT_WEBHOOK_SECRET], publicUrl: "https://yourcompany.com/webhooks/connect", on: { [WebhookEventTypes.SmsDelivered]: async (event) => { console.log(event.data.id, event.data.status); }, [WebhookEventTypes.EmailBounced]: async (event) => { console.log(event.data.recipient); }, },});| Option | Description |
|---|---|
secrets | Your webhook’s secret. While rotating, pass the new and the old one |
publicUrl | The URL you gave Connect for the webhook |
on | Your code for each event type, keyed by a WebhookEventTypes constant such as WebhookEventTypes.SmsDelivered. event.data is typed for the event |
onAny | Runs for every event, after its on handler |
onError | Called when your code throws. Logs the error by default |
dedupe | Where handled Webhook-Ids are kept. In memory by default. See below |
handle takes a standard Request and returns a Response:
| When | Response |
|---|---|
| The signature doesn’t match, or has expired | 401 |
| You already handled this webhook | 200, and your code isn’t run |
| Your code ran, or there is none for the event type | 200 |
| Your code throws | 500, and Connect retries later |
Adding it to your app
Section titled “Adding it to your app”With anything that uses standard Request and Response objects, such as Next.js, SvelteKit, Hono, Bun or Deno, pass the request straight in. For example, in a Next.js route handler:
export const POST = (request: Request) => handle(request);With Express or node:http, use toNodeHandler. With Express, keep the body raw, as the signature covers its exact bytes:
import express from "express";import { toNodeHandler } from "@divergent.dev/connect/node";
const app = express();app.post("/webhooks/connect", express.raw({ type: "application/json" }), toNodeHandler(handle));Running more than one instance
Section titled “Running more than one instance”The handler remembers which webhooks it has handled for 48 hours, but only in its own process. If you run more than one instance, store them somewhere shared, such as Redis, with SET NX:
import { acquiredClaim, duplicateClaim, handler, type WebhookDeduplicator } from "@divergent.dev/connect";import { createClient } from "redis";
const redis = await createClient({ url: process.env.REDIS_URL }).connect();
const dedupe: WebhookDeduplicator = { async claim(webhookId) { const key = `connect-webhook:${webhookId}`; const claimed = await redis.set(key, "1", { NX: true, EX: 172800 }); return claimed ? acquiredClaim(() => redis.del(key)) : duplicateClaim; },};
const handle = handler({ secrets: [process.env.CONNECT_WEBHOOK_SECRET], dedupe, on: { /* ... */ } });The key lasts 48 hours, longer than Connect keeps retrying. If your code throws, the key is deleted so the retry is handled.
Verifying by hand
Section titled “Verifying by hand”To check a webhook yourself, use verify with the raw body. It returns the event, or throws a WebhookVerificationError whose reason says why it failed. It doesn’t skip repeats.
import { verify, WebhookVerificationError } from "@divergent.dev/connect";
try { const event = verify({ url: "https://yourcompany.com/webhooks/connect", headers: req.headers, body: rawBody, secret });} catch (e) { if (e instanceof WebhookVerificationError) console.log(e.reason);}