Webhooks
Generate typed webhook events, verify signatures, and test delivery to a local handler.
Webhooks notify consumers when something happens in your API. Describe the events in OpenAPI to generate typed SDK payloads and signature verification. A CLI Target can send sample events; the Pro webhook relay can deliver real events to a local handler.
For typed events and signed samples, you need an OpenAPI spec with webhook schemas and a handler that follows your API's signing contract. Generate an SDK for verification and a CLI for local testing commands. To forward existing events without generating types or samples, go directly to Bring real events to localhost; the relay does not require webhook declarations.
1. Declare webhooks in the spec
OpenAPI 3.1 has a top-level webhooks section. On 3.0 specs, Typeship reads the established x-webhooks convention. Each entry needs a JSON request body schema:
webhooks:
shipment.updated:
post:
requestBody:
content:
application/json:
schema:
type: object
required: [type, shipment]
properties:
type: { type: string, enum: [shipment.updated] }
shipment: { $ref: "#/components/schemas/Shipment" }A property pinned to a single value (enum with one entry, or const) becomes the discriminator, so consumers can switch on event.type.
2. Check your signing contract
The generated verifier follows the signing scheme your webhooks' header parameters declare:
| Declared headers | What the SDK verifies |
|---|---|
None, or webhook-signature | Standard Webhooks: headers webhook-id, webhook-timestamp, and webhook-signature; signed content id.timestamp.payload, HMAC-SHA256, base64, sent as v1,<signature>. Several space-separated signatures are accepted, so keys can rotate. Secrets are whsec_ followed by base64; any other string is used as raw bytes. Timestamps older or newer than five minutes are rejected. |
A signature header whose example is sha256=<hex> (GitHub's X-Hub-Signature-256), bare hex, or base64 with sha256 in its name (Shopify-style) | An HMAC-SHA256 of the raw request body with the secret as UTF-8 bytes, in that header and format. |
A signature header whose example is t=<unix>,v1=<hex> (Stripe-style) | An HMAC-SHA256 of <t>.<raw body> in hex. Any v1 value may match, so keys can rotate. A timestamp more than five minutes from now is rejected; change that with verifyWebhook(secret, payload, headers, { toleranceSeconds }) in TypeScript, verify_webhook(..., tolerance=) in Python, or the WebhookTolerance variable in Go. |
| Any other signature header, or headers without one | Nothing. Generation warns, and the SDK exposes only parsers: no unwrap, no verifyWebhook. |
Declare the signature header on each webhook with an example value, as GitHub's own spec does:
parameters:
- name: X-Hub-Signature-256
in: header
example: sha256=6dcb09b5b57875f334f61aebed695e2e4193db5eFor a new API, Standard Webhooks is an available signing format.
For a signing contract the SDK does not verify, retain that contract and its existing verifier. Verify the original raw body, headers, timestamp, and replay protection using your implementation, then pass the verified payload to the parser: unwrapUnsafe (or parse) in TypeScript, unwrap_unsafe (or parse) in Python, or UnwrapUnsafe (or Parse) in Go. These parsers do not authenticate an event; never use them as a replacement for verification. Keep this adapter and its tests in code you own.
Events named by a header
When the webhooks declare an event header (a header named like X-GitHub-Event, X-Event-Type, or X-Shopify-Topic, with each webhook's value as its example), deliveries are keyed by it. unwrap and parse return { event, payload } in TypeScript and Python, where event is the header's value and, in TypeScript, switching on event narrows payload to that event's types. In Go, WebhookEvent.Event holds the header's value.
Changing a deployed signing scheme requires a migration plan for consumers. It is not a prerequisite for using the generated event types or the relay. The CLI fake-event command below signs with the same scheme the SDK verifies and sets the event header; when the SDK does not verify your scheme, it sends the sample unsigned, so use your existing signing fixtures.
3. Consumers verify with the SDK
const client = new ParcelClient({ webhookKey: process.env.PARCEL_WEBHOOK_KEY });
export async function handler(req: Request) {
const event = await client.webhooks.unwrap(await req.text(), req.headers);
switch (event.type) {
case "shipment.updated": return onUpdated(event.shipment);
case "shipment.delivered": return onDelivered(event.shipment_id);
}
}unwrap throws WebhookVerificationError on a bad signature, a stale timestamp, or a missing key. unwrapUnsafe parses without verifying. Verification uses WebCrypto, so the same code runs on Node, browsers, edge runtimes, and Workers.
All three SDKs sign byte-identically. A payload signed by one verifies in the others.
4. Test before any real event exists
The generated CLI builds a signed sample event from the spec's schemas:
parcel webhooks fake # list declared events
parcel webhooks fake shipment.updated --forward-to localhost:3000/webhooksThe key is --key, then PARCEL_WEBHOOK_KEY, then a throwaway. Set the same key in the handler under test and the signature verifies.
5. Bring real events to localhost
ProAvailable on Pro and Enterprise
With config.cli.relay enabled on the CLI Target, parcel webhooks listen --forward-to localhost:3000/webhooks mints a private URL and replays every event sent there with its original headers. Signature verification works unchanged because nothing is re-signed. Register the printed URL as a webhook endpoint in your API and events start arriving.
The original timestamp is also preserved, so delayed events can exceed your handler's verification window. The relay depends on Typeship's hosted service; see its limits and replacement path before distributing this workflow to customers.