关于 billing-sdk
BillingSDK 是面向 React 和 Next.js 前端的开源计费界面组件库,适合构建 Dodo Payments 定价、订阅和用量页面。组件以源码形式安装到项目中,API 调用则应放在服务端。
主要功能
- 01提供定价与订阅管理界面
- 02展示用量、额度和账单
- 03通过 CLI 或 shadcn 注册表安装
- 04可编辑项目内组件源码
适用场景
- 01构建连接结账流程的定价页
- 02添加订阅管理与客户门户入口
- 03展示用量、额度或发票历史
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ dodopayments/ skills --skill billing-sdk安装后在 Claude Code 中输入/billing-sdk确认已加载。
附带文件references/pricing-page.md
详细介绍
BillingSDK
BillingSDK 是一个用于构建账单界面的开源 React 组件库,由 Dodo Payments 维护,位于
github.com/dodopayments/billingsdk。组件会作为源文件复制到你的代码库中(采用 shadcn/ui 模式),而不是从运行时 npm 包导入。
构建前端时使用此技能。所有 Dodo Payments API 调用都应放在服务端路由处理程序中;组件仅负责展示。
何时使用此技能
- 构建可启动 Dodo 结账流程的定价页面。
- 构建包含订阅详情和“管理账单”按钮的账户/账单页面。
- 显示用量计量器、额度余额、发票历史记录或即将产生的费用。
- 为现有 React 或 Next.js 应用添加取消/升级/降级界面。
- 决定使用
@billingsdk/cli init搭建项目,还是添加单个组件。
核心概念
安装源文件,而非添加依赖。 根目录中的 billingsdk 仓库包是私有的。目前没有经过验证的通用运行时 npm 组件包。你可以通过 CLI 或 shadcn 注册表安装组件,.tsx 文件会放入项目的 components/billingsdk/ 目录中。
已安装文件定义属性契约。 由于组件会以 vendored 文件的形式放入你的代码库,components/billingsdk/<component>.tsx 中导出的 props 类型就是你所用版本的权威依据。接入回调之前请先阅读。不要假设属性名称与其他项目相同。
注册表是判断组件是否可安装的唯一依据。 请使用
registry.json。文档导航与注册表有所出入——例如,导航中有 payment-success-dialog,但当前注册表中没有。如果 add 失败,说明该组件不在注册表中。
组件不会调用 Dodo。 组件接收数据并触发回调。每次 Dodo Payments 调用都在你的服务端使用 DODO_PAYMENTS_API_KEY 执行。该密钥的格式为 dodo_test_... 或 dodo_live_...,绝不能暴露给浏览器。
安装
搭建新的集成(框架配置、API 路由、useBilling hooks、lib/dodopayments.ts、依赖项和环境变量 DODO_PAYMENTS_API_KEY、DODO_PAYMENTS_ENVIRONMENT、DODO_PAYMENTS_WEBHOOK_KEY):
npx @billingsdk/cli init
向现有项目添加单个组件:
npx @billingsdk/cli add pricing-table-one
或者使用 @billingsdk/ 命名空间,通过 shadcn 注册表安装:
npx shadcn@latest add @billingsdk/pricing-table-one
已发布的 CLI 包为 @billingsdk/cli,最近一次看到的版本为 0.9.0。
组件清单
根据当前官方注册表,按用途分组如下:
| 分组 | 组件块 |
|---|---|
| 定价 | pricing-table-one 至 pricing-table-eight |
| 订阅 | subscription-management、usage-based-pricing、invoice-history、update-plan-card、update-plan-dialog、proration-preview、cancel-subscription-card、cancel-subscription-dialog |
| 用量和账单 | usage-meter-linear、usage-meter-circle、usage-table、detailed-usage-table、billing-screen、billing-settings、billing-settings-2、upcoming-charges |
| 付款 | payment-details、payment-details-two、payment-method-selector、payment-card、payment-failure |
| 促销和试用 | banner、limited-offer-dialog、trial-expiry-card |
大多数组件块的页面上都有已发布的用法示例和 props 表,详见
billingsdk.com/docs/components(例如 subscription-management
和 billing-settings);pricing-table-one 的示例收录于
references/pricing-page.md。生成文件中的 props 类型仍是你所安装版本的权威依据,因此运行 add 后请先阅读。
服务端客户端
仅由路由处理程序和服务端组件导入的单个模块。
// lib/dodopayments.ts
import DodoPayments from 'dodopayments';
if (!process.env.DODO_PAYMENTS_API_KEY) {
throw new Error('DODO_PAYMENTS_API_KEY is not set');
}
export const dodo = new DodoPayments({
bearerToken: process.env.DODO_PAYMENTS_API_KEY,
environment: process.env.DODO_PAYMENTS_ENVIRONMENT === 'live_mode' ? 'live_mode' : 'test_mode',
});
如果省略 environment,其默认值为 'live_mode',因此开发期间请显式设置。基础 URL 为
https://test.dodopayments.com 和 https://live.dodopayments.com。
定价页面
完整指南:references/pricing-page.md。
涵盖:
- 套餐目录
- 产品 ID 对照表 — 仅限服务端
- 结账路由
- 客户端组件
订阅管理和客户门户
安装这个区块,然后从生成的文件中读取其 props:
npx @billingsdk/cli add subscription-management
门户路由。调用方法为 customers.customerPortal.create(customerID, { ...params }),会话的 URL 字段为 link。
// app/api/portal/route.ts
import { NextResponse } from 'next/server';
import { dodo } from '@/lib/dodopayments';
import { getCurrentUser } from '@/lib/auth';
export async function POST() {
const user = await getCurrentUser();
if (!user?.dodoCustomerId) {
return NextResponse.json({ error: 'No billing account' }, { status: 400 });
}
try {
const session = await dodo.customers.customerPortal.create(user.dodoCustomerId, {
return_url: `${process.env.NEXT_PUBLIC_APP_URL}/account`,
});
return NextResponse.json({ url: session.link });
} catch (error) {
console.error('Customer portal session failed', error);
return NextResponse.json({ error: 'Could not open billing portal' }, { status: 500 });
}
}
门户链接会在 24 小时后过期,因此每次点击时都应创建一个,而不是缓存链接。在门户中,客户可以查看和取消订阅、在已启用的产品系列中更改套餐、更新付款方式、下载发票以及获取许可证密钥。
客户端:
// components/manage-billing-button.tsx
'use client';
import { useState } from 'react';
export function ManageBillingButton() {
const [pending, setPending] = useState(false);
async function openPortal() {
setPending(true);
try {
const response = await fetch('/api/portal', { method: 'POST' });
if (!response.ok) {
setPending(false);
return;
}
const { url } = (await response.json()) as { url: string };
window.location.href = url;
} catch {
setPending(false);
}
}
return (
<button type="button" onClick={openPortal} disabled={pending}>
{pending ? 'Opening…' : 'Manage billing'}
</button>
);
}
将 openPortal 绑定到已安装的 subscription-management.tsx 所提供的回调 prop,或在旁边渲染独立按钮。在承载该页面的服务端组件中加载订阅本身 — await dodo.subscriptions.retrieve(user.dodoSubscriptionId) — 并向下传递纯数据。
用量和额度显示
安装一个计量表区块:
npx @billingsdk/cli add usage-meter-linear
在服务端获取数值。额度余额通过 creditEntitlements.balances.retrieve 获取:
// app/api/usage/credits/route.ts
import { NextResponse } from 'next/server';
import { dodo } from '@/lib/dodopayments';
import { getCurrentUser } from '@/lib/auth';
const CREDIT_ENTITLEMENT_ID = process.env.DODO_CREDIT_ENTITLEMENT_ID;
export async function GET() {
const user = await getCurrentUser();
if (!user?.dodoCustomerId || !CREDIT_ENTITLEMENT_ID) {
return NextResponse.json({ error: 'No billing account' }, { status: 400 });
}
try {
const balance = await dodo.creditEntitlements.balances.retrieve(user.dodoCustomerId, {
credit_entitlement_id: CREDIT_ENTITLEMENT_ID,
});
return NextResponse.json({ balance: balance.balance, overage: balance.overage });
} catch (error) {
console.error('Credit balance lookup failed', error);
return NextResponse.json({ error: 'Could not read balance' }, { status: 500 });
}
}
使用已安装的 components/billingsdk/usage-meter-linear.tsx 中的 prop 名称,将结果传入计量表区块。适用于同一场景的相关区块:usage-meter-circle、usage-table、detailed-usage-table、upcoming-charges、invoice-history。
额度余额最终会保持一致 — 计量数据到额度的扣减由后台工作进程大约每分钟运行一次 — 因此显示的余额仅供参考。不要据此限制请求。用量计量、额度权益和账本条目详见 usage-based-billing 和 credit-based-billing 技能。
主题和自定义
组件采用 Tailwind CSS 和 shadcn/ui 约定,因此有三个层次:
-
主题令牌。 在
globals.css中覆盖 shadcn CSS 变量;所有区块都会继承这些变量。@layer base { :root { --primary: 220 90% 56%; --primary-foreground: 0 0% 100%; } } -
组件 props。
pricing-table-one接受theme(官方示例中为"classic")和size("medium")。请查看已安装文件中的完整联合类型。 -
修改源文件。 这些文件归你所有。修改它们是受支持的结构性变更方式,但重新运行
add时需要手动协调改动。
托管的 Dodo 结账页面使用单独的主题设置,通过结账会话中的 customization.theme_config 配置 — 与这些组件无关。
框架支持
这些组件并非与框架无关。它们需要 React 或 Next.js,以及 Tailwind CSS 和 shadcn/ui 约定。
CLI 还附带适用于 Next.js、Express、Hono、Fastify 和 React 的服务端集成模板。这仅涵盖路由处理程序一侧。注意:Dodo 的集成页面与 BillingSDK 自身的介绍,对于哪些服务端模板已提供、哪些仍属“即将推出”说法不一 — 在确定使用某个适配器前,请通过当前 CLI 确认。
对于非 React 前端,可通过框架适配器(@dodopayments/nextjs、@dodopayments/express、@dodopayments/sveltekit 等)调用 Dodo,并自行构建 UI。
环境变量
# .env.local — server-side only, never prefixed with NEXT_PUBLIC_
DODO_PAYMENTS_API_KEY=dodo_test_xxxxxxxxxxxxxxxx
DODO_PAYMENTS_ENVIRONMENT=test_mode
DODO_PAYMENTS_WEBHOOK_KEY=xxxxxxxxxxxxxxxx
DODO_CREDIT_ENTITLEMENT_ID=cde_xxxxxxxxxxxxxxxx
# Safe to expose
NEXT_PUBLIC_APP_URL=https://yoursite.com
请同时切换为 dodo_live_... 和 DODO_PAYMENTS_ENVIRONMENT=live_mode。在 test_mode 下使用线上密钥会失败,在 live_mode 下使用测试密钥也会失败。
常见错误
将 API 密钥暴露给浏览器。 NEXT_PUBLIC_DODO_PAYMENTS_API_KEY 会将密钥内联到 JS 包中,任何人都能读取。Dodo Payments 没有可公开的密钥——所有密钥都是机密。只能在路由处理程序和服务端组件中导入 lib/dodopayments.ts。
调用 customers.createPortalSession。 该方法不存在。请使用 client.customers.customerPortal.create(customerID, { ...params }),并读取 .link,而不是 .url。
信任客户端渲染的套餐状态。 React 状态中选中的套餐,或用户返回了 return_url,都不代表订阅已生效。只有在数据库中持久化并经 Webhook 确认的订阅事件才能授予权益,然后据此渲染界面。webhook-integration 技能介绍了 Webhook 签名验证。
接受请求正文中未经处理的 product_id。 调用方随后可以使用你目录中的任意产品进行结账,包括内部产品或折扣产品。在服务端将不透明的套餐标识映射到 pdt_ ID。
在客户端硬编码价格。 plans[].monthlyPrice / yearlyPrice 是展示文本。若你在 Dodo 控制面板中更改价格,结账会收取新金额,但定价页面仍会展示旧价格。任何价格变更都应同时核对 lib/billingsdk-config.ts 和控制面板中的设置。
执行 npm install billingsdk。 根仓库的包是私有的。请通过 @billingsdk/cli 或 shadcn 注册表进行安装。
猜测属性名称。 请查看 billingsdk.com/docs/components 上对应组件的页面,然后打开 components/billingsdk/ 下生成的文件,在进行其他接线操作之前先阅读导出的属性类型。
使用 DODO_PAYMENTS_WEBHOOK_SECRET。 SDK 会为 webhookKey 选项读取 DODO_PAYMENTS_WEBHOOK_KEY。
根据显示的额度余额限制请求。 扣减是异步的,因此显示的余额会有延迟。Dodo 明确警告,不要将其用作严格的逐请求授权依据。
使用 payments.create 或 subscriptions.create 进行购买。 这两种方法都已弃用。请使用 checkoutSessions.create。