计划执行与检查节点
executing-plans
按既定实施计划完成工作,保持过程检查和反馈。
billing-sdk【效率与协作】BillingSDK, the open-source React and shadcn component library for Dodo Payments billing UI. Use when building pricing tables, pricing pages, account billing pages, usage meters, credit balances, invoice history, or cancel and upgrade UI in React or Next.js, or running @billingsdk/cli init; keep Dodo API calls server-side.
独立 GitHub 快照 · 自动分类 · 以原作者文档为准
BillingSDK is an open-source React component library for billing interfaces, maintained by Dodo Payments
at github.com/dodopayments/billingsdk. Components are copied into your repository as source files
(shadcn/ui model) rather than imported from a runtime npm package.
Use this skill when building the front end. All Dodo Payments API calls belong in server-side route handlers; the components are presentation only.
@billingsdk/cli init or add individual components.Source-installed, not a dependency. The repository root package billingsdk is private. There is no
verified general-purpose runtime npm component package. You install components through the CLI or the
shadcn registry, and the .tsx files land in your project under components/billingsdk/.
The installed file is the prop contract. Because components are vendored into your repo, the exported
props type in components/billingsdk/<component>.tsx is authoritative for your version. Read it before
wiring callbacks. Do not assume prop names from another project.
The registry is the installability source of truth. Use
registry.json. The docs navigation
has drifted from it — for example payment-success-dialog appears in navigation but not in the current
registry. If add fails, the block is not in the registry.
Components never call Dodo. They receive data and fire callbacks. Every Dodo Payments call runs on your
server with DODO_PAYMENTS_API_KEY. That key is dodo_test_... or dodo_live_... and must never reach
the browser.
Scaffold a new integration (framework config, API routes, useBilling hooks, lib/dodopayments.ts,
dependencies, and env vars DODO_PAYMENTS_API_KEY, DODO_PAYMENTS_ENVIRONMENT,
DODO_PAYMENTS_WEBHOOK_KEY):
npx @billingsdk/cli init
Add a single component to an existing project:
npx @billingsdk/cli add pricing-table-one
Or install through the shadcn registry using the @billingsdk/ namespace:
npx shadcn@latest add @billingsdk/pricing-table-one
The published CLI package is @billingsdk/cli, most recently seen at version 0.9.0.
From the current official registry, grouped by purpose:
| Group | Blocks |
|---|---|
| Pricing | pricing-table-one through pricing-table-eight |
| Subscription | subscription-management, usage-based-pricing, invoice-history, update-plan-card, update-plan-dialog, proration-preview, cancel-subscription-card, cancel-subscription-dialog |
| Usage and billing | usage-meter-linear, usage-meter-circle, usage-table, detailed-usage-table, billing-screen, billing-settings, billing-settings-2, upcoming-charges |
| Payments | payment-details, payment-details-two, payment-method-selector, payment-card, payment-failure |
| Promotion and trials | banner, limited-offer-dialog, trial-expiry-card |
Most blocks have a published usage example and props table on their page at
billingsdk.com/docs/components (for example subscription-management
and billing-settings); pricing-table-one is reproduced in
references/pricing-page.md. The generated file's props type is still
authoritative for the version you installed, so read it after running add.
One module, imported only by route handlers and server components.
// 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 defaults to 'live_mode' when omitted, so set it explicitly during development. Base URLs
are https://test.dodopayments.com and https://live.dodopayments.com.
Full guide: references/pricing-page.md.
Covers:
Install the block, then read its props from the generated file:
npx @billingsdk/cli add subscription-management
The portal route. The method is customers.customerPortal.create(customerID, { ...params }) and the
session's URL field is 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 });
}
}
Portal links expire after 24 hours, so mint one per click instead of caching it. In the portal a customer can view and cancel subscriptions, change plans within enabled product collections, update payment methods, download invoices, and retrieve license keys.
Client side:
// 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>
);
}
Wire openPortal to whichever callback prop your installed subscription-management.tsx exposes, or
render the standalone button next to it. Load the subscription itself in the server component that hosts
the page — await dodo.subscriptions.retrieve(user.dodoSubscriptionId) — and pass plain data down.
Install a meter block:
npx @billingsdk/cli add usage-meter-linear
Fetch the numbers server-side. Credit balances come from 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 });
}
}
Pass the result into the meter block using the prop names in your installed
components/billingsdk/usage-meter-linear.tsx. Related blocks for the same surface: usage-meter-circle,
usage-table, detailed-usage-table, upcoming-charges, invoice-history.
Credit balances are eventually consistent — meter-to-credit deduction runs on a background worker roughly
once a minute — so the displayed balance is informational. Do not gate a request on it. Metering, credit
entitlements, and ledger entries are covered in the usage-based-billing and credit-based-billing skills.
Components are Tailwind CSS plus shadcn/ui conventions, so there are three layers:
Theme tokens. Override the shadcn CSS variables in globals.css; every block inherits them.
@layer base {
:root {
--primary: 220 90% 56%;
--primary-foreground: 0 0% 100%;
}
}
Component props. pricing-table-one accepts theme ("classic" in the official example) and
size ("medium"). Check the installed file for the full union types.
Source edits. The files are yours. Editing them is the supported path for structural changes, at
the cost of manual reconciliation when you re-run add.
The hosted Dodo checkout page is themed separately, through customization.theme_config on the checkout
session — not by these components.
The components are not framework-agnostic. They require React or Next.js plus Tailwind CSS and shadcn/ui conventions.
The CLI additionally ships server integration templates for Next.js, Express, Hono, Fastify, and React. That covers the route-handler side only. Caution: Dodo's integration page and BillingSDK's own introduction have disagreed about which server templates are shipping versus "coming soon" — confirm the adapter you want with the current CLI before committing to it.
For non-React front ends, call Dodo through a framework adapter (@dodopayments/nextjs,
@dodopayments/express, @dodopayments/sveltekit, and others) and build your own 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
Switch to dodo_live_... and DODO_PAYMENTS_ENVIRONMENT=live_mode together. A live key against
test_mode fails, and a test key against live_mode fails.
Exposing the API key to the browser. NEXT_PUBLIC_DODO_PAYMENTS_API_KEY inlines the secret into the
JS bundle for anyone to read. There is no publishable key in Dodo Payments — every key is secret. Import
lib/dodopayments.ts only from route handlers and server components.
Calling customers.createPortalSession. That method does not exist. Use
client.customers.customerPortal.create(customerID, { ...params }) and read .link, not .url.
Trusting client-rendered plan state. The selected plan in React state, or the fact that the user
returned to return_url, does not mean the subscription is active. Grant entitlements only from
webhook-confirmed subscription events persisted in your database, then render UI from that. Webhook
signature verification is covered in the webhook-integration skill.
Accepting a raw product_id from the request body. A caller can then check out against any product in
your catalogue, including internal or discounted ones. Map an opaque plan slug to a pdt_ id server-side.
Hardcoding prices in the client. plans[].monthlyPrice / yearlyPrice are display text. When you change a price in the Dodo
dashboard, the checkout charges the new amount while the pricing page keeps advertising the old one.
Reconcile lib/billingsdk-config.ts with the dashboard as part of any pricing change.
npm install billingsdk. The root repository package is private. Install through
@billingsdk/cli or the shadcn registry instead.
Guessing prop names. Check the block's page on billingsdk.com/docs/components, then open the
generated file under components/billingsdk/ and read the exported props type before wiring anything else.
Using DODO_PAYMENTS_WEBHOOK_SECRET. The SDK reads DODO_PAYMENTS_WEBHOOK_KEY for the webhookKey
option.
Gating requests on the displayed credit balance. Deduction is asynchronous, so the reported balance lags. Dodo explicitly warns against using it as strict per-request authorization.
Using payments.create or subscriptions.create for a purchase. Both are deprecated. Use
checkoutSessions.create.