---
title: "Webhooks"
description: "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."
url: https://typeship.dev/docs/guides/webhooks
markdown: https://typeship.dev/docs/guides/webhooks.md
section: "Get started"
---
> ## Documentation index
> Fetch the complete documentation index at https://typeship.dev/llms.txt (every page, one line each) or the full text at https://typeship.dev/llms-full.txt.
> Append .md to any docs URL, or send Accept: text/markdown, for the markdown twin of that page.

# 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:

```yaml
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

**TypeScript**

```ts
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.

**Python**

```python
client = AcmeClient(webhook_key=os.environ["ACME_WEBHOOK_KEY"])

def handler(request):
    event = client.webhooks.unwrap(request.body, request.headers)
    if event["type"] == "account.closed":
        on_closed(event["account_id"])
```

**Go**

```go
client, _ := acme.New(acme.WithWebhookKey(os.Getenv("ACME_WEBHOOK_KEY")))

func handler(w http.ResponseWriter, r *http.Request) {
    body, _ := io.ReadAll(r.Body)
    event, err := client.Webhooks.Unwrap(body, r.Header)
    if err != nil { http.Error(w, "bad signature", 400); return }
    if closed, err := event.AsAccountClosed(); err == nil {
        onClosed(closed.AccountID)
    }
}
```

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:

```bash
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

_Available on Pro and Enterprise._

With the [webhook relay](https://typeship.dev/docs/platforms/cli#webhooks-listen) 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.

## Sitemap

[Every page of these docs](https://typeship.dev/llms.txt)
