Skip to content

Verify Standard Webhooks webhooks

Verify that a webhook really came from Standard Webhooks before acting on it.

Identifier 'standard-webhooks'
Import import { verifyStandardWebhooks } from 'verihook/standard-webhooks'
Headers webhook-id
webhook-timestamp
webhook-signature
Signature Standard Webhooks: HMAC-SHA256 of <webhook-id>.<webhook-timestamp>.<raw body>, keyed with the base64 secret, sent as v1,<base64>.
Replay window 300 seconds (options.tolerance)
result.eventType the body’s type

The endpoint’s signing secret (whsec_...), from the sender’s dashboard. If you send the webhooks with verihook/send, it’s the secret from generateWebhookSecret().

Store it in an environment variable (WEBHOOK_SECRET below) and never commit it.

import { verifyStandardWebhooks } from 'verihook/standard-webhooks';
export async function POST(request: Request) {
const result = await verifyStandardWebhooks(request, process.env.WEBHOOK_SECRET!);
if (!result.valid) {
console.warn(result.code, result.reason, result.hint);
return new Response('Invalid signature', { status: 401 });
}
console.log(result.eventType, result.event);
return Response.json({ received: true });
}

request can be a Fetch Request or { headers, body, url? } with the raw body. verifyWebhook('standard-webhooks', request, secret) from verihook does the same. Read why the raw body matters if verification fails behind a body parser.

app/api/webhooks/standard-webhooks/route.ts
import { createWebhookHandler } from 'verihook/next';
export const POST = createWebhookHandler('standard-webhooks', process.env.WEBHOOK_SECRET!, async (payload, result) => {
// Runs only for a valid signature
console.log(result.eventType, result.event);
});

Each adapter responds 401 to an invalid signature, 413 to a body over maxBodySize (2 MB by default) and 200 to a duplicate when you pass a dedupeStore.

  • Any service that follows the spec works, as do webhooks sent with verihook/send.
  • During secret rotation several signatures are sent, separated by spaces. Any one matching is enough.
  • The dedupe key is the signed webhook-id.

signWebhook from verihook/testing builds a correctly signed Standard Webhooks request for your tests:

import { signWebhook } from 'verihook/testing';
import { verifyStandardWebhooks } from 'verihook/standard-webhooks';
const hook = await signWebhook('standard-webhooks', {
secret: 'whsec_dGVzdF9zZWNyZXRfa2V5X2Zvcl9zdml4XzEyMw==',
payload: { id: 'evt_test' },
});
const result = await verifyStandardWebhooks(
{ headers: hook.headers, body: hook.body, url: hook.url },
hook.secret,
);
// result.valid === true

From the command line, npx verihook simulate standard-webhooks --url http://localhost:3000/webhooks/standard-webhooks sends one to your local server.

See Testing your handlers for supertest and Fetch Request examples.

Failed results carry a code and often a hint with the likely cause. See Troubleshooting for each error code.

Standard Webhooks’s webhook documentation