Skip to content
Webhooks SDK
Esc
navigateopen⌘Jpreview
On this page

Build a custom provider

Most providers are a description, not an implementation — createHmacProvider supplies the rest.

Most providers are a description, not an implementation. Scheme families 1, 2, and 3 are one algorithm with four parameters — which headers carry the signature, how the digest is encoded, what string was signed, and how the secret is decoded. createHmacProvider supplies everything else: the replay window, secret rotation, multiple candidate signatures, constant-time comparison, and the error taxonomy.

A body-HMAC provider in full

Family 1 — HMAC(secret, rawBody) in a single header:

import { createHmacProvider, MissingSignatureError } from 'webhooks-sdk'

export function acme(options: { secret: string | string[] }) {
  return createHmacProvider({
    id: 'acme',
    name: 'Acme',
    secret: options.secret,
    encoding: 'hex',
    extract: (raw) => {
      const header = raw.header('x-acme-signature')
      if (!header) throw new MissingSignatureError('Missing x-acme-signature')
      return { candidates: [header] }
    },
    content: (_material, raw) => raw.text(),
    event: (raw) => {
      const payload = raw.json<{ id: string; event: string; sent_at: number }>()
      return {
        id: payload.id,
        provider: 'acme',
        type: payload.event,
        timestamp: new Date(payload.sent_at * 1000),
        payload,
        raw,
      }
    },
  })
}

The pieces:

Option Role
id / name Machine id and display name for the provider.
secret Shared secret — a single string or an array for rotation.
encoding How the digest is encoded in the header: 'hex' or 'base64'.
extract Pull the candidate signature(s) — and the timestamp, if the scheme has one — out of the request.
content Build the exact string the provider signed.
event Map the verified request to the event envelope.

Adding a timestamp (family 2)

Return the timestamp from extract, fold it into content, and set a tolerance (seconds) to enforce the replay window:

tolerance: 300,
extract: (raw) => {
  const { t, v1 } = parseAcmeHeader(raw.header('x-acme-signature'))
  return { candidates: v1, timestamp: t }
},
content: (material, raw) => `${material.timestamp}.${raw.text()}`,

A request whose timestamp falls outside the tolerance fails with timestamp_out_of_tolerance before any handler runs.

When the scheme isn’t an HMAC

Reach past createHmacProvider only when the scheme genuinely is not an HMAC over a string — asymmetric signatures, certificate chains, JWTs. Those still compose from the same pieces (resolveSecrets, assertWithinTolerance, matchesAnyHmac), which is how the Standard Webhooks provider supports both a symmetric v1 and an Ed25519 v1a branch.

Testing your provider

Write a signAcmeWebhook helper alongside the provider so tests can produce real signatures — a test that stubs the verifier tests nothing. Cover at least:

  • a valid signature
  • the wrong secret
  • a body tampered with after signing
  • a missing header
  • a stale timestamp, where the scheme signs one

See Testing for the request-building utilities.

Was this page helpful?