Skip to content
Webhooks SDK
Esc
↑↓navigate↵open⌘Jpreview
On this page

Standard Webhooks

One spec, dozens of vendors — Resend, Clerk, Polar, Replicate, OpenAI, Loops, Dodo Payments ship as wrappers; everything else works generically.

Resend, Clerk, Polar, Replicate, OpenAI, Loops, Dodo Payments — and every other Svix-backed vendor — sign webhooks the same way: the Standard Webhooks spec. One provider covers them all, whichever header generation your vendor sends (webhook-* or the older svix-*) and whichever signature flavor it uses (symmetric v1 or Ed25519 v1a).

Named wrappers

Seven vendors ship as typed one-line wrappers:

import { resend } from 'webhooks-sdk/resend'

resend({ secret: process.env.RESEND_WEBHOOK_SECRET! })
// event types: email.sent, email.delivered, email.bounced, …

Full page →

import { clerk } from 'webhooks-sdk/clerk'

clerk({ secret: process.env.CLERK_WEBHOOK_SIGNING_SECRET! })
// event types: user.created, session.created, organization.updated, …

Full page →

import { polar } from 'webhooks-sdk/polar'

polar({ secret: process.env.POLAR_WEBHOOK_SECRET! })
// event types: order.paid, subscription.active, checkout.created, …

Full page →

import { replicate } from 'webhooks-sdk/replicate'

replicate({ secret: process.env.REPLICATE_WEBHOOK_SIGNING_SECRET! })
// event types are prediction statuses: starting, processing, succeeded, failed, canceled, …

Full page →

import { openai } from 'webhooks-sdk/openai'

openai({ secret: process.env.OPENAI_WEBHOOK_SECRET! })
// event types: response.completed, batch.failed, fine_tuning.job.succeeded, …

Full page →

import { loops } from 'webhooks-sdk/loops'

loops({ secret: process.env.LOOPS_SIGNING_SECRET! })
// event types: contact.created, email.delivered, email.hardBounced, …

Full page →

import { dodoPayments } from 'webhooks-sdk/dodo-payments'

dodoPayments({ secret: process.env.DODO_PAYMENTS_WEBHOOK_KEY! })
// event types: payment.succeeded, subscription.active, refund.succeeded, …

Full page →

Each takes { secret, tolerance? } and adds typed event names, plus the one per-vendor deviation where there is one — Replicate’s and Loops’ event-name fields, Polar’s raw secret. Nothing else differs from the generic provider.

Any other vendor, right now

A vendor without a wrapper doesn’t need one:

import { standardWebhooks } from 'webhooks-sdk/standard-webhooks'

standardWebhooks({ id: 'svix', secret: process.env.SVIX_WEBHOOK_SECRET! })

That covers Stytch and Svix itself — and any vendor whose docs mention whsec_ secrets or svix-*/webhook-* headers.

Options

Option Type Default
secret string | string[] — The whsec_… signing secret(s).
publicKey string | string[] — The whpk_… Ed25519 key(s), for vendors signing with v1a. Either credential alone suffices.
tolerance number 300 Replay window in seconds.
id string 'standard-webhooks' Provider slug used in the envelope and error messages.
name string 'Standard Webhooks' Display name.
eventType string | ((payload) => string | undefined) 'type' Which body field names the event.

Two details the spec makes easy to get wrong

The secret is base64 after the whsec_ prefix. Signing with the literal string produces a digest that never matches. The SDK decodes it for you — and throws a loud ConfigurationError on an undecodable secret instead of failing every delivery as a bad signature.

The signature header is a list. During key rotation, vendors send several space-delimited candidates (v1,abc v1,def). The SDK checks all of them; unknown versions are ignored rather than rejected, so a future v2 fails closed instead of breaking you.

The envelope

  • event.id — the webhook-id header, the spec’s canonical dedup key.
  • event.type — the body’s type field by default; override with eventType (Replicate’s wrapper uses the status field, Loops’ uses eventName).
  • event.timestamp — the signed webhook-timestamp.

Standalone & testing

import {
  verifyStandardWebhook,  // (raw, options) — throws on failure
  parseStandardWebhook,   // (raw, options?) — the envelope
  signStandardWebhook,    // (body, secret, { id?, timestamp?, headerPrefix? }) — all three headers
} from 'webhooks-sdk/standard-webhooks'

signStandardWebhook returns the three headers as a record — pass headerPrefix: 'svix' to produce the legacy names. See Testing.

Last updated on September 1, 2026

Was this page helpful?