Docs/Sending

Signing secrets

How webhooks are signed, which secret is used, and how to generate one.

Every request carries three headers from the Standard Webhooks spec:

HeaderValue
webhook-idThe event id, like evt_3f1c…. Same on every retry
webhook-timestampUnix time in seconds, fresh on every attempt
webhook-signaturev1, followed by the base64 signature

The signature is HMAC-SHA256 over {id}.{timestamp}.{body}, where body is the exact JSON string that is sent.

Secret formats

A secret that starts with whsec_ is base64-decoded, and the decoded bytes are the key. This is the format Svix uses. Any other non-empty string is used as-is, as UTF-8.

A whsec_ secret with nothing after the prefix, or with invalid base64, is rejected: send() returns invalid signing secret for subscriber <url> instead of signing with a key anyone could guess.

Generate a secret

Use 32 random bytes:

node -e "console.log('whsec_' + require('crypto').randomBytes(32).toString('base64'))"

Share it with the receiver over a channel you trust. Both sides need the same value.

Which secret is used

  1. The subscriber's own secret, if it has one.
  2. Otherwise signing.secret from the config.

With neither, send() returns no signing secret for subscriber <url>. Giving each subscriber its own secret means one leaked secret only exposes one receiver.

const wh = webhooks({
  events,
  subscribers: async ({ subject }) => {
    const endpoint = await db.endpoint.findFirstOrThrow({ where: { accountId: subject } });
    return [{ url: endpoint.url, secret: endpoint.secret }];
  },
});

In the outbox, the secret is stored on the row, so a worker signs with the secret that was current when the event was sent.

Signing in your own code

The SDK exports the two helpers it uses, for tests and tooling:

import { isValidSecret, signPayload } from "@typedhook/sdk";

isValidSecret("whsec_"); // false
const signature = await signPayload(secret, "evt_1", 1791655200, body); // "v1,…"
Edit on GitHubLast updated on October 14, 2026

On this page

For coding agents

Every page is available as Markdown. Point your agent at the full docs in one file.