---
title: "Stripe Integration"
description: "Add checkout, portal sessions, billing status, webhooks, product catalogs, metering, and database-backed billing snapshots."
canonical_url: "https://mcp.farmjs.dev/docs/integrations/stripe"
markdown_url: "https://mcp.farmjs.dev/docs/integrations/stripe.md"
last_updated: "2026-10-20"
x_farming_labs_generated_preamble: true
---

# Stripe Integration
URL: /docs/integrations/stripe
LLM index: /llms.txt
Description: Add checkout, portal sessions, billing status, webhooks, product catalogs, metering, and database-backed billing snapshots.

# Stripe Integration

Add checkout, portal sessions, billing status, webhooks, product catalogs, metering, and database-backed billing snapshots.

## Install from the CLI

**Terminal**

```bash
farm add integration stripe --ui
```

## Config-first setup

**src/lib/integrations.ts**

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

export const integrations = {
  billing: stripe({
    secretKey: process.env.STRIPE_SECRET_KEY,
    webhookSecret: process.env.STRIPE_WEBHOOK_SECRET,
    products: [
      {
        id: "pro",
        name: "Pro",
        prices: [{ interval: "month", amount: 2900, currency: "usd" }],
      },
    ],
  }),
};
```

## Choose SDK ownership

### Let `@farm.js/stripe` construct the SDK

The config-first example is the adapter-owned path. When `instance` is omitted, the
`@farm.js/stripe` adapter creates a real Stripe SDK client from `secretKey`, supplied directly or
through `STRIPE_SECRET_KEY`.

### Provide an application-owned instance

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

const stripeClient = new Stripe(process.env.STRIPE_SECRET_KEY!, {
  maxNetworkRetries: 2,
});

export const integrations = {
  billing: stripe({
    instance: stripeClient,
    webhookSecret: process.env.STRIPE_WEBHOOK_SECRET,
  }),
};
```

Use this path when the application needs to own retries, telemetry, API-version settings, or a
compatible test adapter. The instance wins if a secret key is also supplied. Webhook, product,
billing, route, and storage settings remain integration options in either mode.

`stripeClient` above is the vendor's `Stripe` SDK object—not another Farm wrapper. The
`stripe(...)` call remains responsible for adapting that SDK to Farm routes, callers, webhooks, and
lifecycle behavior.

## Usage

**Client checkout**

```ts
const checkout = await apiClient.billing.checkout.post({
  body: {
    productId: "pro",
    successPath: "/success",
    cancelPath: "/pricing",
  },
});

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

## Database-backed billing

The Stripe integration can use Farm's integration ORM layer through `ctx.args.db`, so relational billing snapshot reads and writes can work across supported database clients. This is database access, not Farm KV storage.

## What Stripe adds

| Area            | Details                                                                                     |
| --------------- | ------------------------------------------------------------------------------------------- |
| Catalog         | Public product and price metadata for pricing pages.                                        |
| Checkout        | A typed checkout route that can return JSON for callers or redirect for browser navigation. |
| Customer portal | A typed route for opening Stripe's billing portal.                                          |
| Billing status  | Subscription, trial, seats, cancellation, and plan state.                                   |
| Entitlements    | Feature, limit, usage, meter, and billing checks.                                           |
| Webhooks        | Event verification and snapshot updates for checkout and subscription events.               |
| Database        | Schema-backed billing account snapshots through `ctx.args.db`.                              |

## Common callers

**Load pricing**

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

**Read current billing state**

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

if (status.data?.status === "active") {
  console.log(status.data.planId);
}
```

**Open the portal**

```ts
const portal = await api.billing.portal.post({
  body: {
    returnTo: "/settings/billing",
  },
});

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

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

## Billing owner

Production billing usually needs an owner resolver. That resolver decides whether the billing account belongs to a user, organization, workspace, or team.

```ts
stripe({
  secretKey: process.env.STRIPE_SECRET_KEY,
  webhookSecret: process.env.STRIPE_WEBHOOK_SECRET,
  billing: {
    async resolveOwner(ctx) {
      const userId = ctx.req.get<string>("user.id");

      if (!userId) {
        return null;
      }

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

The same `ctx` includes `ctx.args.db`, `ctx.data`, request params, the raw request, and request-scoped context values from middleware or auth integrations.

## Production notes

- Set `STRIPE_SECRET_KEY`, `STRIPE_WEBHOOK_SECRET`, and `APP_BASE_URL`.
- Keep product IDs stable because they become the app-facing contract.
- Verify webhook signatures before mutating billing state.
- A checkout grants its plan only once the session is `complete` and its payment is `paid` (or
  needed none). Subscribe the webhook endpoint to `checkout.session.completed` and
  `checkout.session.async_payment_succeeded` so delayed payment methods are fulfilled when the
  money arrives.
- Store billing snapshots through the integration schema when the app needs fast entitlement reads.
- Run `farm stripe migrate --apply` after upgrading so the additive
  `billing_checkout_session` table can persistently deduplicate success-page and webhook delivery.
- Checkout completion, billing sync, and payment hooks are claimed by Stripe session ID before they
  run. Custom persistence should implement `claimCheckoutSession`; without it, Farm falls back to
  comparing the stored billing snapshot, so its billing-account reader must accompany
  `saveBillingSnapshot`. Implement `releaseCheckoutSession` alongside it: when saving the snapshot
  or a checkout hook fails, Farm releases the claim so Stripe's retry can finish the checkout. The
  built-in storage adapters do this already.
- Use server callers for admin-only operations and browser callers for checkout/portal redirects.
- Test checkout success, cancel, webhook replay, portal return, subscription update, and trial edge cases.

## 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).
