# Autumn Webhooks

[Autumn](https://useautumn.com) handles pricing and billing for AI apps. Webhooks are optional, since Autumn keeps subscription state, usage and access in sync itself, but they let you trigger your own systems: activation emails, provisioning, deprovisioning on cancel, usage alerts, ledger syncs. Autumn delivers them through Svix.

## When to Use This Skill

* How do I receive Autumn webhooks?
* How do I verify an Autumn webhook signature (`svix-signature`)?
* How do I handle `billing.updated`, `balances.limit_reached` or `billing.auto_topup_failed`?
* Why is my Autumn webhook signature verification failing?
* Why do retried or replayed Autumn webhooks fail with "Message timestamp too old"?
* Where do I get the Autumn webhook signing secret?

## Verification (core)

| Header | Value |
| --- | --- |
| `svix-id` | Unique message ID, the same on every retry of that message to that endpoint |
| `svix-timestamp` | Unix seconds when the message was sent |
| `svix-signature` | Space-delimited list of `v1,<base64>` signatures |

Signed content is `{svix-id}.{svix-timestamp}.{raw body}`, HMAC-SHA256, base64. The key is the base64-**decoded** part of the secret after `whsec_`. Autumn's docs point to the official `svix` library (npm and PyPI):

```javascript
import { Webhook } from 'svix'; // svix 2.x is ESM-only

// rawBody: the exact bytes received (express.raw({ type: '*/*' }) / request.text()), NOT re-serialised JSON
function verifyAutumnWebhook(rawBody, headers) {
  const secret = process.env.AUTUMN_WEBHOOK_SECRET; // whsec_...
  if (!secret) throw new Error('AUTUMN_WEBHOOK_SECRET not set'); // fail closed → 500
  new Webhook(secret).verify(rawBody, {
    'svix-id': headers['svix-id'],
    'svix-timestamp': headers['svix-timestamp'],
    'svix-signature': headers['svix-signature'],
  }); // throws WebhookVerificationError → 400
  return JSON.parse(rawBody.toString('utf8')); // svix 2.x verify() returns nothing
}

```

Python: `from svix.webhooks import Webhook`, then `Webhook(secret).verify(raw_body, headers)` followed by `json.loads(raw_body)`.

> **For complete handlers with tests**, see [examples/express/](https://github.com/hookdeck/webhook-skills/tree/main/skills/autumn-webhooks/examples/express/), [examples/nextjs/](https://github.com/hookdeck/webhook-skills/tree/main/skills/autumn-webhooks/examples/nextjs/), [examples/fastapi/](https://github.com/hookdeck/webhook-skills/tree/main/skills/autumn-webhooks/examples/fastapi/).

## The 5-Minute Timestamp Window and Hookdeck Retries

The `svix` and `standardwebhooks` libraries **always** enforce a fixed 5-minute window on `svix-timestamp`, and it can't be turned off or widened. Retries and replays through [Hookdeck](https://hookdeck.com) (and any other queue or replay tool) resend the **original** request with the **original** `svix-timestamp`, and Svix's signature isn't regenerated. A delivery retried or replayed more than 5 minutes after Autumn sent it is rejected by the library as "Message timestamp too old", even though the signature is valid.

If you receive Autumn webhooks through Hookdeck, or want to replay old events, use the manual verifier in [references/verification.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/autumn-webhooks/references/verification.md) (HMAC-SHA256 over `{id}.{timestamp}.{raw body}`, key = base64-decoded secret after `whsec_`, constant-time comparison of **every** space-separated `v1,<base64>` entry). It lets you drop the window or widen it. The examples switch to it when `AUTUMN_TIMESTAMP_TOLERANCE_SECONDS` is set (`off` disables the window). Because the signature is then the only replay protection, make the handler idempotent (below).

## The Signing Secret Is Shown Once

The API returns the `whsec_` secret only when a webhook is created, by `webhooks.create` or `webhooks.sync`. `webhooks.get` never returns it, so store it immediately. Lost it? Create a new webhook. Autumn's docs don't say where the dashboard shows the secret for an endpoint added there. Details in [references/setup.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/autumn-webhooks/references/setup.md).

## Payload and Event Types

Autumn's example bodies are `{ "type": "<event>", "data": { ... } }`. Dispatch on `type`. Its reference pages for `invoice.finalized` and `vercel.*` list fields without showing the envelope. Auto top-up events also carry a top-level `id` and `occurred_at`. Timestamps inside payloads are milliseconds since the Unix epoch.

| Event | Fired when |
| --- | --- |
| `billing.updated` | A customer's plans change; `data.plan_changes[]` has `action`: `activated`, `scheduled`, `updated` or `expired`; `data.tags` may hold `trial_ended` or `phase_changed` |
| `balances.limit_reached` | A feature limit is hit; `data.limit_type`: `included`, `max_purchase`, `spend_limit` or `usage_limit` |
| `balances.usage_alert_triggered` | A configured usage alert threshold is crossed (`data.usage_alert`) |
| `billing.auto_topup_succeeded` | An auto top-up granted prepaid balance |
| `billing.auto_topup_failed` | An auto top-up was blocked, declined or failed; branch on `data.reason` |
| `invoice.finalized` | A Stripe invoice is finalized and its line items reconciled |

The API also accepts `customer.products.updated` and `customer.threshold_reached`, which have no reference page. Autumn's older docs give `customer.products.updated` its own shape (`data.scenario`, `data.customer`, `data.updated_product`, no `plan_changes`), and its changelog shows it firing alongside `billing.updated`, so don't handle the two alike. Unknown types should get a `200`. See [references/overview.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/autumn-webhooks/references/overview.md) for payload details.

## Delivery Semantics

* Delivery is at least once and retried with exponential backoff when your endpoint errors or is unreachable. Retries use the same `svix-id`, so dedupe on it.
* Return a `2xx` quickly and process in the background.
* Delivery attempts can be inspected and retried from the Autumn dashboard.

## Environment Variables

```bash
AUTUMN_WEBHOOK_SECRET=whsec_xxxxx              # Returned once by webhooks.create / sync
AUTUMN_TIMESTAMP_TOLERANCE_SECONDS=            # Optional: seconds, or "off". Unset = svix library (fixed 5 minutes)

```

## Local Development

```bash
# Express / Next.js (use 8000 for FastAPI). No install or account required.
npx hookdeck-cli listen 3000 autumn --path /webhooks/autumn

```

Add the printed URL as an endpoint in the Autumn dashboard (Developer → Webhooks → Add Endpoint). Autumn requires an https URL and rejects localhost, so use the tunnel URL.

## Reference Materials

* [references/overview.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/autumn-webhooks/references/overview.md) - Event types and payload shapes
* [references/setup.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/autumn-webhooks/references/setup.md) - Dashboard and API setup, getting the secret
* [references/verification.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/autumn-webhooks/references/verification.md) - SDK and manual verification, timestamp window, debugging