Guides

Webhooks

Declare webhooks in your spec and every package gets typed events, signature verification, a fake-event command, and a local relay. The full loop, end to end.

Webhooks are half of most APIs and usually the half SDKs ignore. typeship generates the receiving side from your spec: typed payloads, a verifying parser, a command that sends signed sample events, and a relay that brings real events to a laptop.

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:
  account.updated:
    post:
      requestBody:
        content:
          application/json:
            schema:
              type: object
              required: [type, account]
              properties:
                type: { type: string, enum: [account.updated] }
                account: { $ref: "#/components/schemas/Account" }

A property pinned to a single value (enum with one entry, or const) becomes the discriminator, so consumers can switch on event.type.

2. Sign events the standard way

The generated verifier follows the Standard Webhooks convention:

  • Headers webhook-id, webhook-timestamp, and webhook-signature.
  • Signed content is 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.

If your API already signs this way, nothing changes. If it does not, adopting the convention is what makes the generated unwrap work.

3. Consumers verify with the SDK

const client = new AcmeClient({ webhookKey: process.env.ACME_WEBHOOK_KEY });

export async function handler(req: Request) {
  const event = await client.webhooks.unwrap(await req.text(), req.headers);
  switch (event.type) {
    case "account.updated": return onUpdated(event.account);
    case "account.closed":  return onClosed(event.account_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:

acme webhooks fake                                                    # list declared events
acme webhooks fake account.updated --forward-to localhost:3000/webhooks

The key is --key, then ACME_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 the webhook relay enabled on the project, acme 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.

On this page