# How Billing is Managed in OpenSEO Using Autumn: A Complete Technical Guide

> Discover how OpenSEO manages billing with Autumn. Learn about entitlement checks, usage tracking, and quota enforcement with automatic webhook sync and self-hosted fallback.

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

---

**OpenSEO uses a thin wrapper around the autumn-js SDK to check entitlements, track usage events, and enforce quota limits via Autumn's billing platform, with automatic webhook synchronization and self-hosted fallback support.**

The OpenSEO repository (`every-app/open-seo`) implements a robust billing layer that leverages Autumn as its central quota-management platform. By wrapping the official `autumn-js` SDK in a lazy-loaded client at [`src/server/billing/autumn.ts`](https://github.com/every-app/open-seo/blob/main/src/server/billing/autumn.ts), the application enforces usage limits, tracks consumption, and maintains customer state without coupling business logic to the underlying billing provider.

## Architecture of the Autumn Integration

### The SDK Wrapper Pattern

OpenSEO does not import the Autumn SDK directly throughout the codebase. Instead, it uses a thin wrapper module ([`src/server/billing/autumn.ts`](https://github.com/every-app/open-seo/blob/main/src/server/billing/autumn.ts)) that lazily initializes the client and exposes only the four operations the application requires:

- **`autumn.check`** – Wraps `client.check()` to verify entitlements and remaining balances
- **`autumn.track`** – Wraps `client.track()` to record usage events
- **`autumn.getOrCreate`** – Wraps `client.customers.getOrCreate()` to initialize customer records
- **`autumn.get`** – Wraps `client.customers.get()` to fetch existing customer data

This abstraction ensures that the Org ID (used as the Autumn customer ID) is handled consistently across all billing operations.

## Customer Lifecycle and Quota Enforcement

### Ensuring Customer Existence

For every authenticated request, OpenSEO runs a **billing-ensure** middleware step that guarantees an Autumn customer exists. The `ensureAutumnCustomer` function in [`src/server/billing/subscription.ts`](https://github.com/every-app/open-seo/blob/main/src/server/billing/subscription.ts) invokes `autumn.getOrCreate` using the organization's ID as the customer identifier. This creates a deterministic mapping between OpenSEO organizations and Autumn billing entities.

### Checking Monthly Balances

The subscription module ([`src/server/billing/subscription.ts`](https://github.com/every-app/open-seo/blob/main/src/server/billing/subscription.ts)) enforces quota limits by calling `autumn.check` with the feature ID `AUTUMN_SEO_DATA_BALANCE_FEATURE_ID`. This retrieves the current monthly balance for the organization.

The implementation normalizes edge cases gracefully:
- Missing balances or negative values are treated as zero
- 404 responses (customer not yet created) are handled without throwing
- Results are cached for a short period to prevent repeated round-trips

If the balance is zero, the system throws a **billing error code** (`BILLING_EXCEEDED`) which bubbles to the UI, preventing further API calls that would incur costs.

```typescript
import { ensureAutumnCustomer } from "@/server/billing/subscription";

const billingCtx = await ensureAutumnCustomer(orgId);
// billingCtx.balance contains the remaining quota (in cents)

```

## Usage Tracking and Event Recording

All billable operations—such as rank checks, site audits, and DataForSEO requests—record consumption via `autumn.track`. The wrapper configures `AUTUMN_TRACK_RETRY_OPTIONS` with exponential backoff to survive transient network failures.

When the track call receives a 429 (rate-limited) response, the retry logic backs off before attempting again. This ensures that quotas are only decremented once the request finally succeeds, preventing under-counting during high-traffic periods.

```typescript
await autumn.track(
  {
    event: "rank_check",
    customer_id: billing.customerId,
    properties: { keywordsChecked: result.keywordsChecked },
  },
  AUTUMN_TRACK_RETRY_OPTIONS
);

```

## Real-Time Synchronization via Webhooks

To maintain consistency with external subscription changes, OpenSEO listens to Autumn webhooks through [`src/server/billing/autumn-webhook.ts`](https://github.com/every-app/open-seo/blob/main/src/server/billing/autumn-webhook.ts). When the webhook receives a `billing.updated` event, it triggers `syncAutumnCustomerStatus` to re-read the latest customer record and update the internal cache. This ensures that plan upgrades, downgrades, or administrative adjustments reflect immediately without waiting for cache expiration.

```typescript
import { handleAutumnWebhookRequest } from "@/server/billing/autumn-webhook";

export async function POST(req: Request) {
  return handleAutumnWebhookRequest(req);
}

```

## Self-Hosted Deployment Support

For **self-hosted** deployments, OpenSEO disables the billing gate entirely. The code detects the "self-hosted" environment flag and skips all Autumn checks, treating every organization as having an infinite balance. This architectural decision allows the same codebase to operate as a metered SaaS platform (with Autumn enforcing quotas) or as an unrestricted private installation.

## End-to-End Implementation Example

The following pattern demonstrates the complete billing flow for a protected endpoint:

```typescript
export async function rankCheckHandler(req: Request) {
  // 1. Verify quota exists
  const { billing } = await ensureAutumnCustomer(req.orgId);
  if (billing.balance <= 0) {
    throw new Error("Billing quota exhausted"); // UI shows billing screen
  }

  // 2. Perform the operation
  const result = await runRankCheck(...);

  // 3. Record consumption
  await autumn.track(
    {
      event: "rank_check",
      customer_id: billing.customerId,
      properties: { keywordsChecked: result.keywordsChecked },
    },
    AUTUMN_TRACK_RETRY_OPTIONS
  );

  return result;
}

```

## Summary

- **Autumn SDK Wrapper**: Located at [`src/server/billing/autumn.ts`](https://github.com/every-app/open-seo/blob/main/src/server/billing/autumn.ts), it exposes `check`, `track`, `getOrCreate`, and `get` methods with lazy initialization.
- **Quota Enforcement**: The `ensureAutumnCustomer` function in [`src/server/billing/subscription.ts`](https://github.com/every-app/open-seo/blob/main/src/server/billing/subscription.ts) checks `AUTUMN_SEO_DATA_BALANCE_FEATURE_ID` and throws `BILLING_EXCEEDED` when limits are reached.
- **Reliable Tracking**: Usage events use `autumn.track` with `AUTUMN_TRACK_RETRY_OPTIONS` for exponential backoff and 429 handling.
- **Webhook Sync**: [`src/server/billing/autumn-webhook.ts`](https://github.com/every-app/open-seo/blob/main/src/server/billing/autumn-webhook.ts) processes billing events to keep cached customer data fresh.
- **Deployment Flexibility**: Self-hosted mode bypasses all billing checks, enabling unlimited usage without code changes.

## Frequently Asked Questions

### What is Autumn in the context of OpenSEO?

Autumn is the third-party billing and quota-management platform that OpenSEO uses to enforce usage limits, track consumption events, and manage customer subscription states. According to the OpenSEO source code, it serves as the central authority for determining whether an organization has sufficient balance to perform billable operations like rank checks and site audits.

### How does OpenSEO handle billing quota exceeded errors?

When `ensureAutumnCustomer` detects a zero or negative balance via `autumn.check`, it returns a `BILLING_EXCEEDED` error code. This error bubbles up through the API layer to the frontend, which displays a billing screen to the user, preventing any further API calls that would incur additional costs until the quota is replenished.

### Can OpenSEO run without connecting to Autumn?

Yes. In **self-hosted** deployments, OpenSEO detects the self-hosted configuration flag and bypasses all Autumn SDK calls entirely. The system treats every organization as having infinite quota, allowing the application to function without any external billing provider connection while using the same codebase.

### How does usage tracking handle network failures or rate limiting?

The `autumn.track` wrapper implements retry logic through `AUTUMN_TRACK_RETRY_OPTIONS`, which configures exponential backoff. If Autumn returns a 429 (rate-limited) status, the client waits and retries the request. This ensures that usage is only recorded once the request succeeds, maintaining accurate quota accounting even during network instability.