← Docs

Webhooks

How the generated TypeScript client types webhook payloads and verifies deliveries with the Standard Webhooks convention.

This page covers the hosted CodeForge engine only. It documents what your generated TypeScript client does when your OpenAPI document declares a webhooks key — not the OSS The Codegen Project CLI, which has its own docs.

What triggers this

Nothing extra to configure. If your OpenAPI document has a top-level webhooks key, the generator reads it. A document without one produces none of the types or helpers described below — there is no flag to turn this on or off.

Typed payloads

For each entry under webhooks, the generator reads the operation's JSON request body schema and generates a named payload model, <WebhookKey>WebhookPayload, through the same pipeline that generates your operation models — naming collisions are resolved the same way. Every payload model the document produces is combined into one union type:

export type WebhookPayload = PetAdoptedWebhookPayload | PetTransferredWebhookPayload

A webhooks entry without a JSON request body is skipped, with a warning naming the entry in the run log. That does not fail the run — webhooks are auxiliary to the primary API surface, so the rest of the document still generates.

Verifying a delivery

The client also exports a verifyWebhook() helper and a WebhookVerificationError class, whenever the document's webhooks key is non-empty:

import { verifyWebhook, WebhookVerificationError } from 'your-sdk'

const secret = process.env.WEBHOOK_SECRET
if (!secret) {
	throw new Error('WEBHOOK_SECRET is not set')
}

try {
	await verifyWebhook({
		payload: rawBody, // the raw request body, exactly as received — do not re-serialize it
		headers: request.headers,
		secret, // the 'whsec_' form your provider issued, or a raw string
	})
} catch (error) {
	if (error instanceof WebhookVerificationError) {
		// reject the delivery — do not process an unverified payload
	}
	throw error
}

const event = JSON.parse(rawBody)

verifyWebhook implements the Standard Webhooks convention:

  • It reads the webhook-id, webhook-timestamp, and webhook-signature headers, looked up case-insensitively.
  • It computes an HMAC-SHA256 signature over `${id}.${timestamp}.${payload}` using crypto.subtle — a Web Crypto API global in Node 20+ and every browser, so no crypto dependency is added to your package.
  • webhook-signature can carry several space-separated v1,<base64> entries for key rotation; any one that matches passes verification. An entry with an unrecognized version prefix (not v1) is skipped rather than treated as a mismatch.
  • The signature comparison is constant-time, so a mismatched signature cannot be probed one byte at a time through response timing.
  • Your secret is accepted in either of the two forms providers issue: the whsec_-prefixed base64 form, or a raw string.
  • A timestamp outside toleranceSeconds — 300 by default, in either direction — fails verification, guarding against replay of an old delivery.

Every failure throws WebhookVerificationError, naming what went wrong: a missing or malformed header, a timestamp outside the tolerance, or no signature entry matching the computed one. Success resolves void. Verify the raw body before you parse it — a body that fails verification should never be trusted.

Narrowing the payload

Once a delivery is verified, a valid body parses to one member of the WebhookPayload union. The generator does not emit a discriminator or a dispatch helper — without one declared in your spec there is nothing sound to generate — so narrow the parsed event yourself before reading its fields:

import type { WebhookPayload } from 'your-sdk'

const typedEvent = event as WebhookPayload
// if ('someField' in typedEvent) { ... }

What this does not cover

  • Custom vendor signature schemes. Only the Standard Webhooks convention is implemented. A provider with its own signing scheme is not supported by verifyWebhook.
  • Non-JSON webhook bodies. Only JSON request bodies produce a payload model.
  • Server scaffolding. The generator emits types and a verification helper, not a mounted route or a framework adapter — you wire verifyWebhook into your own handler.