# How the Autumn Billing Webhook Integration Works in Open SEO Hosted Mode

> Discover how the Autumn billing webhook integration functions in Open SEO hosted mode. Learn about payload validation, customer state retrieval, and data persistence with Zod, Svix, and the Autumn SDK.

- Repository: [Every App/open-seo](https://github.com/every-app/open-seo)
- Tags: how-to-guide
- Published: 2026-08-19

---

**In hosted mode, Open SEO receives verified billing events from Autumn via a `POST` webhook, validates the payload with Zod and Svix, fetches the latest customer state through the Autumn SDK, and persists a derived snapshot to the database while propagating updates to Loops and PostHog.**

Open SEO relies on **Autumn** as its internal billing service to keep organization subscription states synchronized across hosted deployments. The **Autumn billing webhook integration** drives this synchronization by consuming events such as plan changes, credit grants, and cancellations. In the `every-app/open-seo` repository, the flow is implemented as an idempotent server-side pipeline guarded by hosted-mode checks.

## Webhook Endpoint and Request Flow

In [`src/server.ts`](https://github.com/every-app/open-seo/blob/main/src/server.ts), the application registers `AUTUMN_WEBHOOK_PATH`—which exports the string `/**/api/autumn/webhook` from [`src/server/billing/autumn-webhook.ts`](https://github.com/every-app/open-seo/blob/main/src/server/billing/autumn-webhook.ts)—and forwards matching `POST` requests to `handleAutumnWebhookRequest`.

```ts
// src/server.ts – routing registration
import {
  AUTUMN_WEBHOOK_PATH,
  handleAutumnWebhookRequest,
} from "@/server/billing/autumn-webhook";

if (pathname === AUTUMN_WEBHOOK_PATH) {
  return handleAutumnWebhookRequest(publicRequest);
}

```

### Svix Signature Verification

Every incoming request is authenticated by `verifySvixSignature`, which inspects Svix headers and the raw body. If the signature is invalid, the handler returns a `401` response immediately and the payload is never processed.

### Zod Payload Validation

After signature verification, the raw JSON body is parsed and validated against `autumnWebhookPayloadSchema`, a Zod schema defined in [`src/server/billing/autumn-webhook.ts`](https://github.com/every-app/open-seo/blob/main/src/server/billing/autumn-webhook.ts). Malformed payloads result in a `400` response, preventing corrupt events from reaching the sync logic.

### Dispatching billing.updated Events

When `payload.type === "billing.updated"`, the handler extracts the `customer_id` via `getCustomerId(payload)`. A missing identifier also produces a `400`. Valid events are passed to `syncAutumnCustomerStatus(customerId)` to reconcile local state.

```ts
// src/server/billing/autumn-webhook.ts – event dispatch
export async function handleAutumnWebhookRequest(request: Request) {
  // Method and signature checks omitted for brevity...

  const payload = autumnWebhookPayloadSchema.parse(JSON.parse(rawPayload));

  if (payload.type === "billing.updated") {
    const customerId = getCustomerId(payload);
    await syncAutumnCustomerStatus(customerId);
  }

  return json({ received: true });
}

```

## Customer Status Synchronization Pipeline

The core sync logic lives in [`src/server/billing/customer-status-sync.ts`](https://github.com/every-app/open-seo/blob/main/src/server/billing/customer-status-sync.ts). The function `syncAutumnCustomerStatus` performs a **read-modify-write** that converges on the latest Autumn data.

### Fetching the Customer Record

The function calls `autumn.customers.getOrCreate({ customerId })` through the Autumn SDK client lazy-loaded in [`src/server/billing/autumn.ts`](https://github.com/every-app/open-seo/blob/main/src/server/billing/autumn.ts). This guarantees the handler always works with the current upstream state rather than stale webhook data alone.

### Deriving and Persisting the Snapshot

The raw customer object is passed to `deriveBillingCustomerStatusSnapshot`, which produces a `BillingCustomerStatusSnapshot`. This snapshot is then upserted into the `billingCustomerStatus` table via **Drizzle ORM**.

### Propagating to Downstream Services

After persistence, `syncBillingStatusToLoops(snapshot)` pushes the updated status to **Loops** for marketing and analytics. If any step throws, the error is logged, captured by **PostHog** via `captureServerError`, and surfaced as a `500` response. On success, the endpoint returns `{ "received": true }` with status `200`.

```ts
// src/server/billing/customer-status-sync.ts – sync logic
export async function syncAutumnCustomerStatus(customerId: string) {
  const customer = await autumn.customers.getOrCreate({ customerId });
  const snapshot = deriveBillingCustomerStatusSnapshot(customer);
  await upsertBillingCustomerStatus(snapshot);
  await syncBillingStatusToLoops(snapshot);
  return snapshot;
}

```

## Hosted-Mode Guards and Idempotency

### Why Self-Hosted Instances Skip Autumn

The integration is wrapped in `isHostedServerAuthMode()` guards. Self-hosted deployments skip all Autumn-related logic entirely, including the webhook handler and customer creation routines. In hosted mode, `getOrCreateOrganizationCustomer` in [`src/server/billing/subscription.ts`](https://github.com/every-app/open-seo/blob/main/src/server/billing/subscription.ts) ensures an Autumn customer exists when a new organization is created, preventing fresh organizations from hitting a "no-credits" gate before any Durable Object checks the balance.

### Idempotent Read-Modify-Write Semantics

The webhook handler is **idempotent** by design. Because `syncAutumnCustomerStatus` fetches the latest truth from Autumn and performs an upsert, receiving the same `billing.updated` event multiple times—or out of order—leaves the database in the same final state. No deduplication table is required.

### Svix Retries and Eventual Consistency

Svix automatically retries on any non-`2xx` response. Combined with the idempotent handler, transient errors resolve without manual intervention, guaranteeing eventual consistency.

## Summary

- The **Autumn billing webhook integration** is active only in **hosted mode**, guarded by `isHostedServerAuthMode()`.
- Autumn sends `POST` events to `/**/api/autumn/webhook`, defined as `AUTUMN_WEBHOOK_PATH` in [`src/server/billing/autumn-webhook.ts`](https://github.com/every-app/open-seo/blob/main/src/server/billing/autumn-webhook.ts).
- Every request is verified via **Svix** (`verifySvixSignature`) and validated against a **Zod** schema (`autumnWebhookPayloadSchema`).
- For `billing.updated` events, `syncAutumnCustomerStatus` fetches the latest customer record via the **Autumn SDK**, derives a `BillingCustomerStatusSnapshot`, and upserts it into the database through **Drizzle ORM**.
- The pipeline is **idempotent** and relies on Svix retries for eventual consistency, so duplicate or out-of-order events converge to the correct state.
- Downstream services such as **Loops** and **PostHog** receive updates for analytics and error observability.

## Frequently Asked Questions

### What happens if the Svix signature verification fails?

If `verifySvixSignature` determines that the headers or raw body do not match the expected signature, the handler returns a `401` response immediately. The payload is never parsed or processed, which protects the endpoint from spoofed requests.

### Is the Autumn webhook handler used in self-hosted Open SEO deployments?

No. Self-hosted instances skip the entire Autumn flow because all related code paths are wrapped in `isHostedServerAuthMode()` checks. The webhook route and customer synchronization logic are only active in hosted mode according to the `every-app/open-seo` source code.

### How does Open SEO handle duplicate or out-of-order webhook events?

The handler is idempotent. `syncAutumnCustomerStatus` performs a read-modify-write by fetching the latest customer state from Autumn and upserting a derived snapshot into the `billingCustomerStatus` table. Repeating the same event produces the same final database state, and Svix retries ensure delivery even after transient failures.

### Which downstream services are updated when a billing event is processed?

After persisting the snapshot to the database via Drizzle, `syncBillingStatusToLoops` pushes the updated status to Loops for marketing and analytics. If an error occurs at any point, `captureServerError` reports it to PostHog for observability.