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

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, the application registers AUTUMN_WEBHOOK_PATH—which exports the string /**/api/autumn/webhook from src/server/billing/autumn-webhook.ts—and forwards matching POST requests to handleAutumnWebhookRequest.

// 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. 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.

// 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. 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. 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.

// 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 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.
  • 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.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →