---
title: "Autumn Integration"
description: "Add subscription and usage billing with Autumn while keeping Farm's integration and database APIs stable."
canonical_url: "https://farmjs.dev/docs/integrations/autumn"
markdown_url: "https://farmjs.dev/docs/integrations/autumn.md"
last_updated: "2026-11-03"
x_farming_labs_generated_preamble: true
---

# Autumn Integration
URL: /docs/integrations/autumn
LLM index: /llms.txt
Description: Add subscription and usage billing with Autumn while keeping Farm's integration and database APIs stable.

# Autumn Integration

Autumn fits apps that want product-led billing without building every plan, entitlement, and usage event by hand.

## Install from the CLI

**Terminal**

```bash
farm add integration autumn --ui
```

## Config-first setup

**src/lib/integrations.ts**

```ts
import { autumn } from "@farm.js/autumn";

export const integrations = {
  billing: autumn({
    secretKey: process.env.AUTUMN_SECRET_KEY,
    billing: {
      resolveOwner(ctx) {
        const userId = ctx.req.get<string>("user.id");
        return userId ? { id: userId, kind: "user" } : null;
      },
    },
  }),
};
```

`AUTUMN_WEBHOOK_SECRET` is read from the environment when webhook routes are configured. Use the
endpoint's signing secret exactly as Autumn provides it, including the `whsec_` prefix. Autumn
delivers webhooks through Svix, and the prefix tells Farm to decode the key the way Svix signed it.

## Choose SDK ownership

### Let Farm construct Autumn

The config-first example is the default path. When `instance` is omitted, Farm creates the Autumn
SDK from `secretKey` (or `AUTUMN_SECRET_KEY`) and the optional `serverURL` option.

### Provide an application-owned instance

```ts
import { Autumn } from "autumn-js";
import { autumn } from "@farm.js/autumn";

const autumnClient = new Autumn({
  secretKey: process.env.AUTUMN_SECRET_KEY,
});

export const billing = autumn({
  instance: autumnClient,
  billing: {
    resolveOwner: () => null,
  },
});
```

The instance wins if a secret key is also supplied. Billing, webhook, route, product, and meter
settings remain integration options in either mode.

## Usage

**Checkout**

```ts
const checkout = await api.billing.checkout.post({
  body: {
    productId: "pro",
    successPath: "/dashboard",
  },
});
```

The Autumn customer always comes from `billing.resolveOwner`; a request body cannot choose it.
Billing state lives in Autumn: the routes read it through the Autumn API, and Farm stores no billing
records of its own.

## What Autumn adds

| Area     | Details                                                                               |
| -------- | ------------------------------------------------------------------------------------- |
| Products | Public products for pricing pages.                                                    |
| Status   | Current plan, subscription, trial, features, limits, and entitlements.                |
| Checkout | Attach an Autumn product and redirect users when payment or confirmation is required. |
| Portal   | Open the customer portal from a typed caller.                                         |
| Usage    | Meter usage, report usage, check balance, and read current charges.                   |
| Webhooks | Verify Autumn events and pass them to your `onEvent` handlers.                        |

## Common callers

```ts
const products = await api.billing.products.get();
const status = await api.billing.status.get();

const allowed = await api.billing.check.post({
  body: {
    key: "ai-generations",
    amount: 1,
  },
});
```

## Checkout flow

```ts
const checkout = await api.billing.checkout.post({
  body: {
    productId: "pro",
    successPath: "/dashboard",
    metadata: {
      source: "pricing",
    },
  },
});

if (checkout.data?.redirectTo) {
  window.location.href = checkout.data.redirectTo;
}
```

Checkout and portal return paths must be same-origin, root-relative application paths. Protocol-relative paths such as `//example.com` are rejected.

## Owner and entitlements

Autumn needs a billing owner when it checks or attaches customer state. Use the owner resolver to connect Farm auth/session data with Autumn customers.

```ts
autumn({
  secretKey: process.env.AUTUMN_SECRET_KEY,
  billing: {
    async resolveOwner(ctx) {
      const organizationId = ctx.req.get<string>("organization.id");

      return organizationId
        ? {
            id: organizationId,
            kind: "organization",
            email: ctx.req.get<string>("user.email"),
          }
        : null;
    },
  },
});
```

Farm names the owner's Autumn customer `<kind>_<id>` when the id only uses letters, digits, `_`,
and `-` (and does not start or end with `-`), such as the random ids auth providers issue. Any other
id, such as an email or a non-ASCII handle, is encoded as `<kind>_-<base64url of the id>` so no two
owners share a customer. Return your own id from `billing.resolveExternalCustomerId(owner)` to
choose it yourself.

Before this, such ids were lossy: `a.b` and `a-b` both became `user_a-b`, and ids with no ASCII
letters became `user_unknown`. If your owner ids are emails or handles, their owners now get new
Autumn customers. To keep the existing ones while you move them, return
`legacyAutumnExternalCustomerId(owner)` (from `@farm.js/autumn`) from
`resolveExternalCustomerId`; owners that collided under it still share one customer.

## Production notes

- Set `AUTUMN_SECRET_KEY`, `AUTUMN_WEBHOOK_SECRET`, and `APP_BASE_URL`.
- Keep Farm product IDs separate from provider IDs when you want a stable app contract.
- Use `check` before expensive operations and `reportUsage` after successful work.
- `reportUsage` adds to Autumn's stored usage with `track`, so parallel reports all count. When a
  meter `guard` sets a hard cap, a report that pushes usage past it is taken back out and answered
  with `state: "hard_limit_reached"`. `last` meters set the reported value instead.
- A meter whose feature is billed as a prepaid quantity is bought, not reported: `reportUsage`
  answers `409` for it, and the quantity changes through checkout.
- The billing POST routes act for the signed-in caller from its session cookie, so a request a
  browser sends from another origin is refused with `403`: a page on another site cannot report
  usage, start a checkout or open the portal for your visitor. Calls with no `Origin` (your own
  server, a script) are unaffected. If your frontend runs on another origin, list it in
  `allowedOrigins`, using the same syntax as `serverActions.allowedOrigins`.
- Treat webhooks as the source of truth for subscription state.
- Test free-plan reads, checkout redirects, portal redirects, usage limits, and webhook sync.

## Sitemap

See the full [sitemap](/sitemap.md) for all pages.
Docs-scoped sitemap: [/docs/sitemap.md](/docs/sitemap.md).
Well-known sitemap: [/.well-known/sitemap.md](/.well-known/sitemap.md).
