# Stripe (/docs/plugins/stripe)

用于 Better Auth 的 Stripe 插件，管理订阅和支付。



Stripe 插件将 Stripe 的支付与订阅功能集成到 Better Auth 中。由于支付和认证通常紧密相关，此插件简化了在应用中集成 Stripe 的流程，负责客户创建、订阅管理和 webhook 处理。

## 功能 [#功能]

* 创建用户注册时的 Stripe 客户
* 管理订阅计划和定价
* 处理订阅生命周期事件（创建、更新、取消）
* 安全处理 Stripe webhook 并进行签名验证
* 向应用暴露订阅数据
* 支持试用期和订阅升级
* **自动试用期滥用防护** - 每个账户在所有计划中只能获得一次试用
* 灵活的引用系统，用于关联订阅与用户或组织
* 支持带席位管理的团队订阅。

## 安装 [#安装]

<Steps>
  <Step>
    ### 安装插件 [#安装插件]

    首先，安装插件：

    <CodeBlockTabs defaultValue="npm" groupId="persist-install">
      <CodeBlockTabsList>
        <CodeBlockTabsTrigger value="npm">
          npm
        </CodeBlockTabsTrigger>

        <CodeBlockTabsTrigger value="pnpm">
          pnpm
        </CodeBlockTabsTrigger>

        <CodeBlockTabsTrigger value="yarn">
          yarn
        </CodeBlockTabsTrigger>

        <CodeBlockTabsTrigger value="bun">
          bun
        </CodeBlockTabsTrigger>
      </CodeBlockTabsList>

      <CodeBlockTab value="npm">
        ```bash
        npm install @better-auth/stripe
        ```
      </CodeBlockTab>

      <CodeBlockTab value="pnpm">
        ```bash
        pnpm add @better-auth/stripe
        ```
      </CodeBlockTab>

      <CodeBlockTab value="yarn">
        ```bash
        yarn add @better-auth/stripe
        ```
      </CodeBlockTab>

      <CodeBlockTab value="bun">
        ```bash
        bun add @better-auth/stripe
        ```
      </CodeBlockTab>
    </CodeBlockTabs>

    <Callout>
      如果您使用的是分离的客户端和服务器设置，请确保在项目的两个部分中都安装该插件。
    </Callout>
  </Step>

  <Step>
    ### 安装 Stripe SDK [#安装-stripe-sdk]

    接下来，在服务器上安装 Stripe SDK：

    <CodeBlockTabs defaultValue="npm" groupId="persist-install">
      <CodeBlockTabsList>
        <CodeBlockTabsTrigger value="npm">
          npm
        </CodeBlockTabsTrigger>

        <CodeBlockTabsTrigger value="pnpm">
          pnpm
        </CodeBlockTabsTrigger>

        <CodeBlockTabsTrigger value="yarn">
          yarn
        </CodeBlockTabsTrigger>

        <CodeBlockTabsTrigger value="bun">
          bun
        </CodeBlockTabsTrigger>
      </CodeBlockTabsList>

      <CodeBlockTab value="npm">
        ```bash
        npm install stripe@^22.0.0
        ```
      </CodeBlockTab>

      <CodeBlockTab value="pnpm">
        ```bash
        pnpm add stripe@^22.0.0
        ```
      </CodeBlockTab>

      <CodeBlockTab value="yarn">
        ```bash
        yarn add stripe@^22.0.0
        ```
      </CodeBlockTab>

      <CodeBlockTab value="bun">
        ```bash
        bun add stripe@^22.0.0
        ```
      </CodeBlockTab>
    </CodeBlockTabs>
  </Step>

  <Step>
    ### 将插件添加到认证配置 [#将插件添加到认证配置]

    ```ts title="auth.ts"
    import { betterAuth } from "better-auth"
    import { stripe } from "@better-auth/stripe"
    import Stripe from "stripe"

    const stripeClient = new Stripe(process.env.STRIPE_SECRET_KEY!, {
        apiVersion: "2026-06-24.dahlia", // 截至 Stripe SDK v22.0.0 的最新 API 版本
    })

    export const auth = betterAuth({
        // ... 现有配置
        plugins: [
            stripe({
                stripeClient,
                stripeWebhookSecret: process.env.STRIPE_WEBHOOK_SECRET!,
                createCustomerOnSignUp: true,
            })
        ]
    })
    ```

    <Callout type="info">
      **从 Stripe v18 升级？** 版本 19 使用异步 webhook 签名验证（`constructEventAsync`），由插件内部处理。您端无需进行任何代码更改！
    </Callout>
  </Step>

  <Step>
    ### 添加客户端插件 [#添加客户端插件]

    ```ts title="auth-client.ts"
    import { createAuthClient } from "better-auth/client"
    import { stripeClient } from "@better-auth/stripe/client"

    export const authClient = createAuthClient({
        // ... 现有配置
        plugins: [
            stripeClient({
                subscription: true // 如果要启用订阅管理
            })
        ]
    })
    ```
  </Step>

  <Step>
    ### 迁移数据库 [#迁移数据库]

    运行迁移或生成模式以将必要的表添加到数据库。

    <Tabs items="[&#x22;migrate&#x22;, &#x22;generate&#x22;]">
      <Tab value="migrate">
        <CodeBlockTabs defaultValue="npm" groupId="persist-install">
          <CodeBlockTabsList>
            <CodeBlockTabsTrigger value="npm">
              npm
            </CodeBlockTabsTrigger>

            <CodeBlockTabsTrigger value="pnpm">
              pnpm
            </CodeBlockTabsTrigger>

            <CodeBlockTabsTrigger value="yarn">
              yarn
            </CodeBlockTabsTrigger>

            <CodeBlockTabsTrigger value="bun">
              bun
            </CodeBlockTabsTrigger>
          </CodeBlockTabsList>

          <CodeBlockTab value="npm">
            ```bash
            npx auth migrate
            ```
          </CodeBlockTab>

          <CodeBlockTab value="pnpm">
            ```bash
            pnpm dlx auth migrate
            ```
          </CodeBlockTab>

          <CodeBlockTab value="yarn">
            ```bash
            yarn dlx auth migrate
            ```
          </CodeBlockTab>

          <CodeBlockTab value="bun">
            ```bash
            bun x auth migrate
            ```
          </CodeBlockTab>
        </CodeBlockTabs>
      </Tab>

      <Tab value="generate">
        <CodeBlockTabs defaultValue="npm" groupId="persist-install">
          <CodeBlockTabsList>
            <CodeBlockTabsTrigger value="npm">
              npm
            </CodeBlockTabsTrigger>

            <CodeBlockTabsTrigger value="pnpm">
              pnpm
            </CodeBlockTabsTrigger>

            <CodeBlockTabsTrigger value="yarn">
              yarn
            </CodeBlockTabsTrigger>

            <CodeBlockTabsTrigger value="bun">
              bun
            </CodeBlockTabsTrigger>
          </CodeBlockTabsList>

          <CodeBlockTab value="npm">
            ```bash
            npx auth generate
            ```
          </CodeBlockTab>

          <CodeBlockTab value="pnpm">
            ```bash
            pnpm dlx auth generate
            ```
          </CodeBlockTab>

          <CodeBlockTab value="yarn">
            ```bash
            yarn dlx auth generate
            ```
          </CodeBlockTab>

          <CodeBlockTab value="bun">
            ```bash
            bun x auth generate
            ```
          </CodeBlockTab>
        </CodeBlockTabs>
      </Tab>
    </Tabs>

    请参阅 [Schema](#schema) 部分以手动添加表。
  </Step>

  <Step>
    ### 设置 Stripe webhook [#设置-stripe-webhook]

    在 Stripe 仪表板中创建一个 webhook 端点，指向：

    ```
    https://your-domain.com/api/auth/stripe/webhook
    ```

    `/api/auth` 是认证服务器的默认路径。

    确保选择以下事件：

    * `checkout.session.completed`
    * `customer.subscription.created`
    * `customer.subscription.updated`
    * `customer.subscription.deleted`

    保存 Stripe 提供的 webhook 签名密钥，并将其添加到环境变量中，名为 `STRIPE_WEBHOOK_SECRET`。
  </Step>
</Steps>

## 使用 [#使用]

### 客户管理 [#客户管理]

您可以仅使用此插件进行客户管理而不启用订阅。适用于只想关联 Stripe 客户和用户的场景。

当设置 `createCustomerOnSignUp: true` 时，用户注册时会自动创建 Stripe 客户，并与数据库中的用户关联。您也可以自定义客户创建流程：

```ts title="auth.ts"
stripe({
    // ... 其他选项
    createCustomerOnSignUp: true,
    onCustomerCreate: async ({ stripeCustomer, user }, ctx) => {
        // 处理新创建的客户
        console.log(`客户 ${stripeCustomer.id} 已为用户 ${user.id} 创建`);
    },
    getCustomerCreateParams: async (user, ctx) => {
        // 定制 Stripe 客户创建参数
        return {
            metadata: {
                referralSource: user.metadata?.referralSource
            }
        };
    }
})
```

### 订阅管理 [#订阅管理]

#### 定义计划 [#定义计划]

您可以静态定义订阅计划，也可以动态获取：

```ts title="auth.ts"
// 静态定义
subscription: {
    enabled: true,
    plans: [
        {
            name: "basic", // 计划名称，存入数据库时自动转为小写
            priceId: "price_1234567890", // Stripe 价格 ID
            annualDiscountPriceId: "price_1234567890", // （可选）年付折扣价格 ID
            limits: {
                projects: 5,
                storage: 10
            }
        },
        {
            name: "pro",
            priceId: "price_0987654321",
            limits: {
                projects: 20,
                storage: 50
            },
            freeTrial: {
                days: 14,
            }
        }
    ]
}

// 动态定义（从数据库或接口获取）
subscription: {
    enabled: true,
    plans: async () => {
        const plans = await db.query("SELECT * FROM plans");
        return plans.map(plan => ({
            name: plan.name,
            priceId: plan.stripe_price_id,
            limits: JSON.parse(plan.limits)
        }));
    }
}
```

详情参见 [计划配置](#plan-configuration)。

#### 创建订阅 [#创建订阅]

通过调用 `subscription.upgrade` 方法创建订阅：

**Endpoint:** `POST /subscription/upgrade`

### Client Side

```ts
const { data, error } = await authClient.subscription.upgrade({
    plan: "pro", // required, 要升级到的计划名称。
    annual: true, // 是否升级为年付计划。
    referenceId: "123", // 订阅的引用 ID。默认为基于 customerType 的值。
    subscriptionId: "sub_123", // 要升级的订阅 ID。
    metadata, // 与订阅一起存储的附加元数据。
    customerType, // 计费的客户类型（默认："user"）。
    seats: 1, // 升级到的席位数量（如果适用）。
    locale, // Checkout 显示的语言环境 IETF 标签。 如果未提供或设置为 "auto"，则使用浏览器语言环境。
    successUrl, // required, Stripe 完成支付或设置后要跳转到的 URL。
    cancelUrl, // required, 如果设置，将显示返回按钮，用户取消支付时将跳转至此。
    returnUrl, // 从计费门户返回时跳转的 URL（用于升级现有订阅）。
    disableRedirect: false, // required, 禁用成功订阅后的重定向。
    scheduleAtPeriodEnd: false, // 在当前计费周期结束时计划更改，而不是立即应用。
});
```

### Server Side

```ts
const data = await auth.api.upgradeSubscription({
    body: {
        plan: "pro", // required, 要升级到的计划名称。
        annual: true, // 是否升级为年付计划。
        referenceId: "123", // 订阅的引用 ID。默认为基于 customerType 的值。
        subscriptionId: "sub_123", // 要升级的订阅 ID。
        metadata, // 与订阅一起存储的附加元数据。
        customerType, // 计费的客户类型（默认："user"）。
        seats: 1, // 升级到的席位数量（如果适用）。
        locale, // Checkout 显示的语言环境 IETF 标签。 如果未提供或设置为 "auto"，则使用浏览器语言环境。
        successUrl, // required, Stripe 完成支付或设置后要跳转到的 URL。
        cancelUrl, // required, 如果设置，将显示返回按钮，用户取消支付时将跳转至此。
        returnUrl, // 从计费门户返回时跳转的 URL（用于升级现有订阅）。
        disableRedirect: false, // required, 禁用成功订阅后的重定向。
        scheduleAtPeriodEnd: false, // 在当前计费周期结束时计划更改，而不是立即应用。
    },
    // This endpoint requires session cookies.
    headers: await headers(),
});
```

### Type Definition

```ts
type upgradeSubscription = {
    /**
     * 要升级到的计划名称。
     */
    plan: string = "pro"
    /**
     * 是否升级为年付计划。
     */
    annual?: boolean = true
    /**
     * 订阅的引用 ID。默认为基于 customerType 的值。
     */
    referenceId?: string = "123"
    /**
     * 要升级的订阅 ID。
     */
    subscriptionId?: string = "sub_123"
    /**
     * 与订阅一起存储的附加元数据。
     */
    metadata?: Record<string, any>
    /**
     * 计费的客户类型（默认："user"）。
     */
    customerType?: "user" | "organization"
    /**
     * 升级到的席位数量（如果适用）。
     */
    seats?: number = 1
    /**
     * Checkout 显示的语言环境 IETF 标签。
     * 如果未提供或设置为 "auto"，则使用浏览器语言环境。
     */
    locale?: string
    /**
     * Stripe 完成支付或设置后要跳转到的 URL。
     */
    successUrl: string
    /**
     * 如果设置，将显示返回按钮，用户取消支付时将跳转至此。
     */
    cancelUrl: string
    /**
     * 从计费门户返回时跳转的 URL（用于升级现有订阅）。
     */
    returnUrl?: string
    /**
     * 禁用成功订阅后的重定向。
     */
    disableRedirect: boolean = false
    /**
     * 在当前计费周期结束时计划更改，而不是立即应用。
     */
    scheduleAtPeriodEnd?: boolean = false
}
```

**简单示例：**

```ts title="client.ts"
await authClient.subscription.upgrade({
    plan: "pro",
    successUrl: "/dashboard",
    cancelUrl: "/pricing",
    annual: true, // 可选：升级为年付计划
    referenceId: "org_123", // 可选：默认为 customerType 的值
    seats: 5, // 可选：团队计划席位数量
    locale: "en" // 可选：用英文显示 Checkout
});
```

这将创建一个 Checkout 会话并将用户重定向到 Stripe Checkout 页面。

<Callout type="info">
  该插件每个引用 ID（用户或组织）一次仅支持一个活跃或试用中的订阅。同一引用 ID 下不允许同时存在多个订阅。

  如果用户已有活跃订阅，则 **必须** 在升级时提供 `subscriptionId` 参数。否则，可能会在与现有订阅并行的位置创建新订阅，导致重复计费。
</Callout>

> **重要提示：** `successUrl` 参数将在内部被修改，以处理结账完成与 webhook 处理之间的竞争条件。插件会创建一个跳转中转页面，在订阅状态正确更新后才跳转到您指定的成功页。

```ts
const { error } = await authClient.subscription.upgrade({
    plan: "pro",
    successUrl: "/dashboard",
    cancelUrl: "/pricing",
});

if (error) {
    alert(error.message);
}
```

#### 切换计划 [#切换计划]

要切换订阅计划，可以调用 `subscription.upgrade`：

要将订阅切换到不同的计划，请使用 `subscription.upgrade` 方法：

```ts title="client.ts"
await authClient.subscription.upgrade({
    plan: "pro",
    successUrl: "/dashboard",
    cancelUrl: "/pricing",
    subscriptionId: "sub_123", // 当前用户计划的 Stripe 订阅 ID
});
```

这确保用户只支付新计划费用，而不会同时支付两个计划。

确保用户只支付新计划，不会同时支付两个计划费用。

#### 在周期结束时调度计划更改 [#在周期结束时调度计划更改]

默认情况下，计划更改会立即生效并自动按比例计费。如果希望用户继续使用当前计划直到结算周期结束，然后切换计划，可以这样做：

```ts title="client.ts"
await authClient.subscription.upgrade({
    plan: "pro",
    successUrl: "/dashboard",
    cancelUrl: "/pricing",
    returnUrl: "/billing",
    scheduleAtPeriodEnd: true, // 默认 false
});
```

此方案使用 [Stripe 订阅调度 API](https://docs.stripe.com/billing/subscriptions/subscription-schedules)，创建两阶段执行：当前计划持续至周期结束，然后自动启动新计划，且不会按比例计费。

<Callout type="info">
  当 `scheduleAtPeriodEnd` 为 `true` 时：

  * 订阅计划 **不会更改**，直到计费周期结束 —— 仅存储 `stripeScheduleId` 以便客户端检测待处理更改
  * 不会重定向到 Stripe Checkout 或计费门户，更改在服务器端应用
  * 在计费周期结束时，Stripe 会触发 `customer.subscription.updated` webhook，自动更新订阅记录
  * 如果在周期结束前请求新的升级或计划调度，将首先释放现有的待处理调度
</Callout>

#### 列出活跃订阅 [#列出活跃订阅]

获取用户的活跃订阅：

**Endpoint:** `GET /subscription/list`

### Client Side

```ts
const { data: subscriptions, error } = await authClient.subscription.list({
    query: {
        referenceId: '123', // 要列出的订阅的引用 ID。
        customerType, // 计费的客户类型（默认："user"）。
    },
});
// 获取活跃订阅
const activeSubscription = subscriptions.find(
    sub => sub.status === "active" || sub.status === "trialing"
);

// 检查订阅限额
const projectLimit = subscriptions?.limits?.projects || 0;
```

### Server Side

```ts
const subscriptions = await auth.api.listActiveSubscriptions({
    query: {
        referenceId: '123', // 要列出的订阅的引用 ID。
        customerType, // 计费的客户类型（默认："user"）。
    },
    // This endpoint requires session cookies.
    headers: await headers(),
});
// 获取活跃订阅
const activeSubscription = subscriptions.find(
    sub => sub.status === "active" || sub.status === "trialing"
);

// 检查订阅限额
const projectLimit = subscriptions?.limits?.projects || 0;
```

### Type Definition

```ts
type listActiveSubscriptions = {
    /**
     * 要列出的订阅的引用 ID。
     */
    referenceId?: string = '123'
    /**
     * 计费的客户类型（默认："user"）。
     */
    customerType?: "user" | "organization"
}
```

确保在插件配置中加入了 `authorizeReference` 以授权引用 ID：

```ts title="auth.ts"
stripe({
    // ... 其他选项
    subscription: {
        // ... 其他订阅配置
        authorizeReference: async ({ user, session, referenceId, action }) => {
            if(action === "list-subscription") {
                const org = await db.member.findFirst({
                    where: {
                        organizationId: referenceId,
                        userId: user.id
                    }   
                });
                return org?.role === "owner"
            }
            // 检查用户是否有权限列出此引用的订阅
            return true;
        }
    }
})
```

#### 取消订阅 [#取消订阅]

取消订阅：

**Endpoint:** `POST /subscription/cancel`

### Client Side

```ts
const { data, error } = await authClient.subscription.cancel({
    referenceId: 'org_123', // 要取消的订阅的引用 ID。默认为基于 customerType 的值。
    customerType, // 计费的客户类型（默认："user"）。
    subscriptionId: 'sub_123', // 要取消的订阅 ID。
    returnUrl: '/account', // required, 点击计费门户返回链接时要跳转的 URL。
});
```

### Server Side

```ts
const data = await auth.api.cancelSubscription({
    body: {
        referenceId: 'org_123', // 要取消的订阅的引用 ID。默认为基于 customerType 的值。
        customerType, // 计费的客户类型（默认："user"）。
        subscriptionId: 'sub_123', // 要取消的订阅 ID。
        returnUrl: '/account', // required, 点击计费门户返回链接时要跳转的 URL。
    },
    // This endpoint requires session cookies.
    headers: await headers(),
});
```

### Type Definition

```ts
type cancelSubscription = {
    /**
     * 要取消的订阅的引用 ID。默认为基于 customerType 的值。
     */
    referenceId?: string = 'org_123'
    /**
     * 计费的客户类型（默认："user"）。
     */
    customerType?: "user" | "organization"
    /**
     * 要取消的订阅 ID。
     */
    subscriptionId?: string = 'sub_123'
    /**
     * 点击计费门户返回链接时要跳转的 URL。
     */
    returnUrl: string = '/account'
}
```

将重定向用户进入 Stripe 账单门户，让用户自行取消订阅。

<Callout type="info">
  **理解取消状态**

  Stripe 支持不同类型的取消，插件会追踪所有状态：

  | 字段                  | 描述                                               |
  | ------------------- | ------------------------------------------------ |
  | `cancelAtPeriodEnd` | 订阅是否（如果状态为 active）或已（如果状态为 canceled）在当前计费周期结束时取消 |
  | `cancelAt`          | 如果订阅计划被取消，这是取消生效的时间                              |
  | `canceledAt`        | 如果订阅已被取消，这是取消的时间                                 |
  | `endedAt`           | 如果订阅已结束，这是订阅结束的日期                                |
  | `status`            | 仅在订阅实际结束后更改为 "canceled"                          |
</Callout>

#### 恢复订阅 [#恢复订阅]

> <small className="font-normal">
>   **注意：**
>
>    仅适用于仍处于活跃状态但有挂起取消或计划更改的订阅。无法恢复已结束的订阅（
>
>   `status: "canceled"`
>
>    且已设置 
>
>   `endedAt`
>
>   ）。
> </small>

如果用户在取消订阅或计划调度后改变主意，您可以恢复订阅：

**Endpoint:** `POST /subscription/restore`

### Client Side

```ts
const { data, error } = await authClient.subscription.restore({
    referenceId: '123', // 要恢复的订阅的引用 ID。默认为基于 customerType 的值。
    customerType, // 计费的客户类型（默认："user"）。
    subscriptionId: 'sub_123', // 要恢复的订阅 ID。
});
```

### Server Side

```ts
const data = await auth.api.restoreSubscription({
    body: {
        referenceId: '123', // 要恢复的订阅的引用 ID。默认为基于 customerType 的值。
        customerType, // 计费的客户类型（默认："user"）。
        subscriptionId: 'sub_123', // 要恢复的订阅 ID。
    },
    // This endpoint requires session cookies.
    headers: await headers(),
});
```

### Type Definition

```ts
type restoreSubscription = {
    /**
     * 要恢复的订阅的引用 ID。默认为基于 customerType 的值。
     */
    referenceId?: string = '123'
    /**
     * 计费的客户类型（默认："user"）。
     */
    customerType?: "user" | "organization"
    /**
     * 要恢复的订阅 ID。
     */
    subscriptionId?: string = 'sub_123'
}
```

<Callout type="info">
  此端点处理两种情况：

  * **挂起取消**：设置 `cancelAtPeriodEnd` 为 `false` 并清除 `cancelAt` / `canceledAt`，订阅将继续自动续订
  * **挂起计划更改**（通过 `scheduleAtPeriodEnd`）：释放 Stripe 订阅计划并清除 `stripeScheduleId`，当前计划保持不变
</Callout>

#### 创建计费门户会话 [#创建计费门户会话]

要创建一个 [Stripe 计费门户会话](https://docs.stripe.com/api/customer_portal/sessions/create)，让客户可以管理其订阅、更新支付方式和查看账单历史记录：

**Endpoint:** `POST /subscription/billing-portal`

### Client Side

```ts
const { data, error } = await authClient.subscription.billingPortal({
    locale, // 客户门户显示的语言环境 IETF 标签。 如果未提供或设置为 "auto"，则使用浏览器语言环境。
    referenceId: "123", // 订阅的引用 ID。
    customerType, // 计费的客户类型（默认："user"）。
    returnUrl, // 退出计费门户后重定向回的 URL。
    disableRedirect: false, // 禁用自动重定向到计费页面。 @default false
});
```

### Server Side

```ts
const data = await auth.api.createBillingPortal({
    body: {
        locale, // 客户门户显示的语言环境 IETF 标签。 如果未提供或设置为 "auto"，则使用浏览器语言环境。
        referenceId: "123", // 订阅的引用 ID。
        customerType, // 计费的客户类型（默认："user"）。
        returnUrl, // 退出计费门户后重定向回的 URL。
        disableRedirect: false, // 禁用自动重定向到计费页面。 @default false
    },
    // This endpoint requires session cookies.
    headers: await headers(),
});
```

### Type Definition

```ts
type createBillingPortal = {
    /**
     * 客户门户显示的语言环境 IETF 标签。
     * 如果未提供或设置为 "auto"，则使用浏览器语言环境。
     */
    locale?: string
    /**
     * 订阅的引用 ID。
     */
    referenceId?: string = "123"
    /**
     * 计费的客户类型（默认："user"）。
     */
    customerType?: "user" | "organization"
    /**
     * 退出计费门户后重定向回的 URL。
     */
    returnUrl?: string
    /**
     * 禁用自动重定向到计费页面。
     * @default false
     */
    disableRedirect?: boolean = false
}
```

<Callout type="info">
  有关支持的语言环境，请参阅 [IETF 语言标签文档](https://docs.stripe.com/js/appendix/supported_locales)。
</Callout>

此端点创建 Stripe 计费门户会话，并在响应中以 `data.url` 返回。您可以重定向用户到此地址，让他们管理订阅、支付方式和账单历史记录。

该接口会创建 Stripe 账单门户会话，响应中以 `data.url` 返回，可以重定向用户至该地址进行订阅和支付管理。

### 引用系统 [#引用系统]

默认情况下，订阅与用户 ID 关联。您也可以自定义引用 ID，将订阅关联到其他实体，如组织：

```ts title="client.ts"
// 为组织创建订阅
await authClient.subscription.upgrade({
    plan: "pro",
    referenceId: "org_123456",
    successUrl: "/dashboard",
    cancelUrl: "/pricing",
    seats: 5 // 团队计划席位数量
});

// 列出组织的订阅
const { data: subscriptions } = await authClient.subscription.list({
    query: {
        referenceId: "org_123456"
    }
});
```

#### 团队订阅与席位数量 [#团队订阅与席位数量]

团队或组织计划可以指定席位数量：

```ts
await authClient.subscription.upgrade({
    plan: "team",
    referenceId: "org_123456",
    seats: 10, // 10 个团队成员
    successUrl: "/org/billing/success",
    cancelUrl: "/org/billing"
});
```

`seats` 参数将传递给 Stripe 作为订阅品项数量，可用于应用逻辑中限制团队成员数量。

要授权引用 ID，请实现 `authorizeReference`：

```ts title="auth.ts"
subscription: {
    // ... 其他选项
    authorizeReference: async ({ user, session, referenceId, action }) => {
        // 检查用户是否有权限管理此引用的订阅
        if (action === "upgrade-subscription" || action === "cancel-subscription" || action === "restore-subscription") {
            const org = await db.member.findFirst({
                where: {
                    organizationId: referenceId,
                    userId: user.id
                }   
            });
            return org?.role === "owner"
        }
        return true;
    }
}
```

### Webhook 处理 [#webhook-处理]

插件自动处理常见 webhook 事件：

* `checkout.session.completed`：结账完成后更新订阅状态
* `customer.subscription.created`：在结账流程外创建订阅时创建订阅
* `customer.subscription.updated`：订阅更改时更新订阅详情
* `customer.subscription.deleted`：标记订阅为已取消

您也可以响应自定义事件：

```ts title="auth.ts"
stripe({
    // ... 其他选项
    onEvent: async (event) => {
        // 处理任何 Stripe 事件
        switch (event.type) {
            case "invoice.paid":
                // 处理已付款发票
                break;
            case "payment_intent.succeeded":
                // 处理支付成功
                break;
        }
    }
})
```

### 订阅生命周期钩子 [#订阅生命周期钩子]

您可以挂载订阅各种生命周期事件的回调：

```ts title="auth.ts"
subscription: {
    // ... 其他选项
    onSubscriptionComplete: async ({ event, subscription, stripeSubscription, plan }) => {
        // 结账方式成功创建订阅时调用
        await sendWelcomeEmail(subscription.referenceId, plan.name);
    },
    onSubscriptionCreated: async ({ event, subscription, stripeSubscription, plan }) => {
        // 订阅在结账外创建时调用（比如后台仪表盘）
        await sendSubscriptionCreatedEmail(subscription.referenceId, plan.name);
    },
    onSubscriptionUpdate: async ({ event, subscription, stripeSubscription }) => {
        // 订阅更新时调用。使用 `stripeSubscription` 获取原始 Stripe 字段，例如 `cancellation_details`。
        console.log(`订阅 ${subscription.id} 已更新`);
    },
    onSubscriptionCancel: async ({ event, subscription, stripeSubscription, cancellationDetails }) => {
        // 订阅取消时调用
        await sendCancellationEmail(subscription.referenceId);
    },
    onSubscriptionDeleted: async ({ event, subscription, stripeSubscription }) => {
        // 订阅删除时调用
        console.log(`订阅 ${subscription.id} 已删除`);
    }
}
```

### 试用期 [#试用期]

您可以为计划配置试用期：

```ts title="auth.ts"
{
    name: "pro",
    priceId: "price_0987654321",
    freeTrial: {
        days: 14,
        onTrialStart: async (subscription) => {
            // 试用期开始时调用
            await sendTrialStartEmail(subscription.referenceId);
        },
        onTrialEnd: async ({ subscription }, ctx) => {
            // 试用期结束时调用
            await sendTrialEndEmail(subscription.referenceId);
        },
        onTrialExpired: async (subscription, ctx) => {
            // 试用到期未转换时调用
            await sendTrialExpiredEmail(subscription.referenceId);
        }
    }
}
```

## 架构 [#架构]

该 Stripe 插件会向您的数据库添加以下数据表：

### 用户 [#用户]

表名：`user`。



<DatabaseTable name="user" fields="stripeUserTableFields" />

### 组织 [#组织]

表名：`organization` <small className="text-xs">（仅当 `organization.enabled` 为 `true` 时有效）</small>



<DatabaseTable name="organization" fields="stripeOrganizationTableFields" />

### 订阅 [#订阅]

表名：`subscription`



<DatabaseTable name="subscription" fields="stripeSubscriptionTableFields" />

### 自定义 Schema [#自定义-schema]

若需要更改表名或字段，可向 Stripe 插件传入 `schema` 选项：

```ts title="auth.ts"
stripe({
    // ... 其他选项
    schema: {
        subscription: {
            modelName: "stripeSubscriptions", // 将订阅表映射为 stripeSubscriptions
            fields: {
                plan: "planName" // 将 plan 字段映射为 planName
            }
        }
    }
})
```

## 选项 [#选项]

| 选项                        | 类型         | 说明                                            |
| ------------------------- | ---------- | --------------------------------------------- |
| `stripeClient`            | `Stripe`   | Stripe 客户端实例。**必需**。                          |
| `stripeWebhookSecret`     | `string`   | Stripe webhook 签名密钥。**必需**。                   |
| `createCustomerOnSignUp`  | `boolean`  | 用户注册时是否自动创建 Stripe 客户。默认：`false`。             |
| `onCustomerCreate`        | `function` | 客户创建后回调，参数 `{ stripeCustomer, user }` 和上下文。   |
| `getCustomerCreateParams` | `function` | 自定义 Stripe 客户创建参数，参数是 `user` 和上下文。            |
| `onEvent`                 | `function` | 收到任何 Stripe webhook 事件时回调，参数为 `Stripe.Event`。 |
| `subscription`            | `object`   | 订阅配置。详见下文[订阅选项](#subscription-options)。       |
| `organization`            | `object`   | 启用组织客户支持。详见下文[组织选项](#organization-options)。   |
| `schema`                  | `object`   | 自定义 Stripe 插件的数据库 schema。                     |

### 订阅选项 [#订阅选项]

| 选项                         | 类型                          | 说明                                                                                                                          |
| -------------------------- | --------------------------- | --------------------------------------------------------------------------------------------------------------------------- |
| `enabled`                  | `boolean`                   | 是否启用订阅功能。&#x2A;*必需。**                                                                                                       |
| `plans`                    | `StripePlan[]` 或 `function` | 订阅计划数组，或返回计划的异步函数。启用时**必需**。                                                                                                |
| `requireEmailVerification` | `boolean`                   | 在允许订阅升级前是否要求邮箱验证。默认：`false`。                                                                                                |
| `authorizeReference`       | `function`                  | 授权 reference ID。接收 `{ user, session, referenceId, action }` 以及上下文。                                                          |
| `getCheckoutSessionParams` | `function`                  | 自定义 Stripe Checkout 会话参数。接收 `{ user, session, plan, subscription }`、请求和上下文。                                                 |
| `onSubscriptionComplete`   | `function`                  | 通过 checkout 创建订阅时调用。接收 `{ event, stripeSubscription, subscription, plan }` 和上下文。                                            |
| `onSubscriptionCreated`    | `function`                  | 在 checkout 之外创建订阅时调用。接收 `{ event, stripeSubscription, subscription, plan }`。                                                |
| `onSubscriptionUpdate`     | `function`                  | 订阅更新时调用。接收 `{ event, subscription, stripeSubscription }`。对于 `cancellation_details` 这类原始 Stripe 字段，请使用 `stripeSubscription`。 |
| `onSubscriptionCancel`     | `function`                  | 订阅取消时调用。接收 `{ event, subscription, stripeSubscription, cancellationDetails }`。                                              |
| `onSubscriptionDeleted`    | `function`                  | 订阅删除时调用。接收 `{ event, stripeSubscription, subscription }`。                                                                   |

#### 计划配置 [#计划配置]

| 选项                        | 类型           | 说明                                                                     |
| ------------------------- | ------------ | ---------------------------------------------------------------------- |
| `name`                    | `string`     | 计划名称。&#x2A;*必需。**                                                      |
| `priceId`                 | `string`     | Stripe price ID。除非使用 `lookupKey`，否则为**必需**。                            |
| `lookupKey`               | `string`     | Stripe price lookup key。可替代 `priceId`。                                 |
| `annualDiscountPriceId`   | `string`     | 年付账单的 price ID。                                                        |
| `annualDiscountLookupKey` | `string`     | 年付账单的 Stripe price lookup key。                                         |
| `limits`                  | `object`     | 计划限制（例如 `{ projects: 10, storage: 5 }`）。                               |
| `group`                   | `string`     | 用于对计划分组的组名。                                                            |
| `seatPriceId`             | `string`     | 按席位计费的 price ID。需要 `organization` 插件。                                  |
| `prorationBehavior`       | `string`     | 订阅更新时的按比例计费行为：`"create_prorations"`（默认）、`"always_invoice"` 或 `"none"`。 |
| `lineItems`               | `LineItem[]` | 在 checkout 会话中包含的附加条目。                                                 |
| `freeTrial`               | `object`     | 试用配置。见[下文](#free-trial-configuration)。                                 |

<Callout type="info">
  Stripe 不支持通过 Checkout Sessions 进行[混合计费周期订阅](https://docs.stripe.com/billing/subscriptions/mixed-interval)。checkout 中的所有条目都应使用**相同的计费周期**（例如全部按月或全部按年）。如果周期不同，Stripe API 会拒绝该请求。
</Callout>

#### 免费试用配置 [#免费试用配置]

| 选项               | 类型         | 说明                                   |
| ---------------- | ---------- | ------------------------------------ |
| `days`           | `number`   | 试用天数。&#x2A;*必需。**                    |
| `onTrialStart`   | `function` | 试用开始时调用。接收 `subscription`。           |
| `onTrialEnd`     | `function` | 试用结束时调用。接收 `{ subscription }` 和上下文。  |
| `onTrialExpired` | `function` | 试用在未转化时过期后调用。接收 `subscription` 和上下文。 |

### 组织选项 [#组织选项]

| 选项                        | 类型         | 说明                                                    |
| ------------------------- | ---------- | ----------------------------------------------------- |
| `enabled`                 | `boolean`  | 启用组织客户支持。&#x2A;*必需。**                                 |
| `getCustomerCreateParams` | `function` | 自定义组织的 Stripe 客户创建参数。接收 `organization` 和上下文。          |
| `onCustomerCreate`        | `function` | 组织客户创建后调用。接收 `{ stripeCustomer, organization }` 和上下文。 |

## 高级用法 [#高级用法]

### 与组织插件一起使用 [#与组织插件一起使用]

Stripe 插件集成了[组织插件](/docs/plugins/organization)，支持组织作为 Stripe 客户。订阅的计费对象由个人用户转为组织，适合 B2B 场景。

<Callout type="info">
  **启用组织客户时：**

  * 当组织首次订阅时会自动创建 Stripe Customer
  * 组织名称变更会同步到 Stripe Customer
  * 有活跃订阅的组织无法被删除
</Callout>

#### 启用组织客户 [#启用组织客户]

将 `organization.enabled` 设置为 `true`，并确保已安装组织插件：

```ts title="auth.ts"
plugins: [
    organization(),
    stripe({
        // ... 其他选项
        subscription: {
            enabled: true,
            plans: [...],
        },
        organization: { // [!code highlight]
            enabled: true // [!code highlight]
        } // [!code highlight]
    })
]
```

#### 创建组织订阅 [#创建组织订阅]

即使启用了组织客户，用户订阅仍可用且为默认选项。若需将组织设为计费客户，请传入 `customerType: "organization"`：

```ts title="client.ts"
await authClient.subscription.upgrade({
    plan: "team",
    referenceId: activeOrg.id,
    customerType: "organization", // [!code highlight]
    seats: 10,
    successUrl: "/org/billing/success",
    cancelUrl: "/org/billing"
});
```

#### 授权 [#授权]

请实现 `authorizeReference`，以确认用户有权管理组织订阅：

```ts title="auth.ts"
subscription: {
    // ... 其他订阅选项
    authorizeReference: async ({ user, referenceId, action }) => {
        const member = await db.members.findFirst({
            where: {
                userId: user.id,
                organizationId: referenceId
            }
        });

        return member?.role === "owner" || member?.role === "admin";
    }
}
```

#### 组织账单邮箱 [#组织账单邮箱]

组织没有唯一邮箱，账单邮箱不会自动同步。组织通常使用专门的账单邮箱，与用户账户邮箱不同。
你可以在结账后通过 Stripe 仪表盘修改邮箱，或使用 `stripeClient` 自定义：

```ts
await stripeClient.customers.update(organization.stripeCustomerId, {
    email: "billing@company.com"
});
```

### 处理用户删除 [#处理用户删除]

有活跃订阅的组织会被自动阻止删除，但用户不会。若要在用户删除时实现相同行为，可在订阅活跃时于[`beforeDelete`](/docs/concepts/users-accounts#回调函数)回调中抛出错误：

```ts title="auth.ts"
import { betterAuth } from "better-auth";
import { APIError } from "better-auth/api";

export const auth = betterAuth({
    user: {
        deleteUser: {
            enabled: true,
            beforeDelete: async (user) => {
                if (!user.stripeCustomerId) return;

                for await (const sub of stripeClient.subscriptions.list({
                    customer: user.stripeCustomerId,
                    status: "all",
                })) {
                    if (["canceled", "incomplete", "incomplete_expired"].includes(sub.status)) continue;

                    throw new APIError("BAD_REQUEST", {
                        message: "请在删除账户前取消当前活跃订阅",
                    });
                    // 或立即取消：await stripeClient.subscriptions.cancel(sub.id);
                    // 或在周期结束时取消：      await stripeClient.subscriptions.update(sub.id, { cancel_at_period_end: true });
                }
            },
        },
    },
});
```

### 自定义 Checkout Session 参数 [#自定义-checkout-session-参数]

你可以向 Stripe Checkout 传递额外参数：

```ts title="auth.ts"
getCheckoutSessionParams: async ({ user, session, plan, subscription }, ctx) => {
    return {
        params: {
            allow_promotion_codes: true,
            tax_id_collection: {
                enabled: true
            },
            billing_address_collection: "required",
            custom_text: {
                submit: {
                    message: "我们将立即为您启用订阅"
                }
            },
            metadata: {
                planType: "business",
                referralCode: user.metadata?.referralCode
            }
        },
        options: {
            idempotencyKey: `sub_${user.id}_${plan.name}_${Date.now()}`
        }
    };
}
```

### 税务信息收集 [#税务信息收集]

启用税务 ID 收集：

```ts title="auth.ts"
subscription: {
    // ... 其他选项
    getCheckoutSessionParams: async ({ user, session, plan, subscription }, ctx) => {
        return {
            params: {
                tax_id_collection: {
                    enabled: true
                }
            }
        };
    }
}
```

### 自动税费计算 [#自动税费计算]

启用自动税费计算（根据客户位置），设置 `automatic_tax`：

```ts title="auth.ts"
subscription: {
    // ... 其他选项
    getCheckoutSessionParams: async ({ user, session, plan, subscription }, ctx) => {
        return {
            params: {
                automatic_tax: {
                    enabled: true
                }
            }
        };
    }
}
```

注意，需先在 Stripe 仪表盘中设置税务注册和配置。

### 试用期管理 [#试用期管理]

**工作原理：**

* 系统会跟踪每个用户在所有计划中的试用使用情况
* 当用户订阅带试用期的计划时，系统会检查其订阅历史
* 如果用户曾经使用过试用期（由 `trialStart`/`trialEnd` 字段或 `trialing` 状态表示），则不会再获得新的试用期
* 这可防止用户通过取消订阅再重新订阅来多次获得免费试用

**示例场景：**

1. 用户订阅“Starter”计划，有 7 天试用期
2. 用户试用后取消订阅
3. 用户尝试订阅“Premium”计划，不会再获得试用期
4. 用户将立即支付 Premium 计划费用

**示例：**

1. 用户订阅“Starter”计划，有 7 天试用期
2. 用户试用后取消订阅
3. 用户尝试订阅“Premium”计划，试用期将被禁止
4. 用户将立即支付 Premium 计划费用

此行为会自动生效，无需额外配置，且不可通过配置覆盖。

## 故障排查 [#故障排查]

### Webhook 问题 [#webhook-问题]

若 Webhook 未正确处理：

1. 确认 Stripe 仪表盘中配置的 Webhook URL 是否正确
2. 验证 Webhook 签名密钥是否正确
3. 检查是否已勾选所有必需事件
4. 查看服务器日志中是否有处理错误

### 订阅状态异常 [#订阅状态异常]

若订阅状态未正确更新：

1. 确认 Webhook 事件已接收并处理
2. 确认 `stripeCustomerId` 和 `stripeSubscriptionId` 是否正确填写
3. 核对应用中引用的 ID 是否与 Stripe 中的一致

### 本地测试 Webhook [#本地测试-webhook]

可使用 Stripe CLI 将 Webhook 转发至本地：

```bash
stripe listen --forward-to localhost:3000/api/auth/stripe/webhook
```

CLI 会输出本地使用的 Webhook 签名密钥，将其配置到本地环境即可

