Receiver verification
Authenticate raw request bytes and decode a typed webhook event.
good-webhooks/verify authenticates a webhook request from its raw bytes and decodes it into a typed event.
import { verifyWebhook, parseWebhook, WebhookError } from 'good-webhooks/verify'
import type { ParsedWebhook, WebhookErrorCode } from 'good-webhooks/verify'This entry point does not load database or worker code. It remains a Node.js API. The receiving guide shows complete handlers for Next.js, Hono, Express, and node:http.
parseWebhook
parseWebhook<const E extends EventDefinitions>(
input: Parameters<typeof verifyWebhook>[0] & { events: E },
): Promise<ParsedWebhook<E>>import { parseWebhook } from 'good-webhooks/verify'
import { z } from 'zod'
const events = {
'invoice.paid': z.object({ invoiceId: z.string(), amount: z.number().int() }),
}
export async function POST(request: Request) {
const event = await parseWebhook({
body: new Uint8Array(await request.arrayBuffer()),
headers: request.headers,
secret: process.env.WEBHOOK_SECRET!,
events,
})
if (event.type === 'invoice.paid') console.log(event.data.amount)
return new Response(null, { status: 204 })
}Verifies the signature, validates the Good Webhooks JSON envelope, and decodes its payload with the selected Standard Schema validator. It accepts every verifyWebhook input plus events, which maps accepted event names to receiver validators.
ParsedWebhook<E> is a discriminated union by event type. Each member contains id: string, its event-name literal, occurredAt: string, and data with that validator's output type.
| Rule | Contract |
|---|---|
| Body size | At most 512 KiB, checked before verification. Larger bodies produce PAYLOAD_INVALID. |
| Envelope | A JSON object with id, type, occurredAt, and data. |
id | Must match the authenticated webhook-id header. |
type | Must be a key of events. Unknown event types produce PAYLOAD_INVALID. |
occurredAt | A canonical ISO timestamp. |
data | Must fit the sender's JSON limits: 256 KiB, maximum depth 64, finite numbers, and plain JSON values. Then the validator must accept it. |
Receiver validators accept transmitted JSON and can decode it into local values such as Date. Expected error messages do not expose payload values.
verifyWebhook
verifyWebhook(input: {
body: string | Uint8Array
headers: Headers | Record<string, string | string[] | undefined>
secret: string | readonly string[]
now?: Date
toleranceSeconds?: number
}): voidimport { verifyWebhook } from 'good-webhooks/verify'
export async function POST(request: Request) {
const body = await request.text()
verifyWebhook({ body, headers: request.headers, secret: process.env.WEBHOOK_SECRET! })
const payload: unknown = JSON.parse(body) // Parse only after verification.
return new Response(null, { status: 204 })
}Verifies Standard Webhooks HMAC-SHA256 signatures against the exact body bytes and checks timestamp tolerance. Successful verification returns undefined. It does not parse JSON or record the event ID.
The input table below is generated from the public function signature. VerifyOptions is a documentation alias, not a named package export.
Prop
Type
| Input | Contract |
|---|---|
body | Required raw string or Uint8Array, before JSON parsing or reserialization. |
headers | Required Headers or record of string, string-array, or undefined values. Relevant values must each be one string. |
secret | One signing secret or an array of 1 through 16 secrets. |
now | Optional valid Date, defaulting to the current time. |
toleranceSeconds | Optional nonnegative safe integer, default 300. Applies to timestamps in the past and future. |
| Header | Rule |
|---|---|
webhook-id | 1 through 255 visible ASCII characters without dots or commas. |
webhook-timestamp | Nonnegative integer Unix seconds. |
webhook-signature | At most 4,096 characters. Space-separated v1,<base64> entries. |
Header names are case-insensitive. Duplicate relevant names or array-valued relevant headers are rejected.
Signing secrets have a whsec_ prefix and canonical base64 encoding of 24 through 64 bytes. Good Webhooks generates 32-byte secrets. Verification succeeds when a v1 signature matches any supplied secret, so pass [newSecret, oldSecret] during secret rotation.
Limits you own
verifyWebhook has no request-body size limit. Limit uploads before buffering. Receiver
deduplication of webhook-id is also your responsibility. See process each event
once.
Failures
Both functions throw WebhookError for expected failures. The receiving guide maps each code to an HTTP response.
| Failure | Result |
|---|---|
| Invalid body type, signing secret, clock, or tolerance | INVALID_INPUT |
| Missing or malformed signature headers, or signature mismatch | SIGNATURE_INVALID |
| Timestamp outside tolerance | SIGNATURE_EXPIRED |
| Body over 512 KiB | PAYLOAD_INVALID from parseWebhook |
| Invalid UTF-8, JSON, envelope, unknown event, or rejected event payload | PAYLOAD_INVALID from parseWebhook |
| Missing event map or invalid selected validator | INVALID_CONFIG from parseWebhook |
| Validator throws or returns an invalid result | Ordinary Error with a cause, not an expected request rejection |