Polar 替你处理增值税、消费税、商品与服务税及争议退款。Stripe 不负责这些。如果你正在独立发布面向全球的 SaaS,又不想在 50 个税务管辖区逐一注册,这是一个值得认真权衡的选择。
进入控制台,点击 Products → New product,选择:
保存后,复制 product_id——结账 URL 需要用到它。
前往 Settings → API Keys → Create API key,选择以下权限范围:
checkouts:write——用于创建结账会话customers:read——用于查询客户状态subscriptions:read——用于校验权益添加到 .env:
POLAR_ACCESS_TOKEN=polar_oat_…
POLAR_WEBHOOK_SECRET=polar_whs_… # from next step
POLAR_PRODUCT_ID=prod_… # from step 1
安装 SDK:
npm install @polar-sh/sdk
服务端路由——当用户点击「升级」时触发:
import { Polar } from "@polar-sh/sdk";
const polar = new Polar({
accessToken: process.env.POLAR_ACCESS_TOKEN,
server: "production", // or "sandbox"
});
// POST /api/upgrade
app.post("/api/upgrade", async (req, res) => {
const checkout = await polar.checkouts.create({
products: [process.env.POLAR_PRODUCT_ID],
successUrl: "https://yourapp.com/welcome?checkout_id={CHECKOUT_ID}",
customerEmail: req.user.email,
metadata: { userId: req.user.id }, // critical — see step 5
});
res.json({ url: checkout.url });
});
客户端重定向到 checkout.url。Polar 负责处理信用卡输入、根据客户所在国家计算税额,以及发送收据邮件。
另一种方式:Polar Checkout Links——无需调用 API,直接使用类似 polar.sh/checkout/abc 的托管链接,放入按钮即可。非常适合落地页场景。
Polar 会在状态变更时向你的服务器发送事件。不要依赖成功页面的跳转来确认付款——用户可能中途关闭标签页,卡扣也可能异步重试。
进入 Settings → Webhooks → Add endpoint:
https://yourapp.com/api/webhooks/polarsubscription.created、subscription.updated、subscription.canceled、order.created、order.refundedPOLAR_WEBHOOK_SECRET验证并处理事件:
import { validateEvent } from "@polar-sh/sdk/webhooks";
app.post(
"/api/webhooks/polar",
express.raw({ type: "application/json" }),
async (req, res) => {
let event;
try {
event = validateEvent(
req.body,
req.headers,
process.env.POLAR_WEBHOOK_SECRET,
);
} catch {
return res.status(401).send("bad signature");
}
switch (event.type) {
case "subscription.created":
case "subscription.updated": {
const userId = event.data.metadata.userId;
await db.users.update(userId, {
plan: event.data.product.name,
polarSubscriptionId: event.data.id,
subscriptionStatus: event.data.status, // active | past_due | …
});
break;
}
case "subscription.canceled": {
const userId = event.data.metadata.userId;
await db.users.update(userId, { plan: "free" });
break;
}
}
res.json({ ok: true });
},
);
这与 Stripe webhook 教程的结构完全一致,适用规则也相同:快速返回 2xx、处理函数保持幂等、在写入数据库前完成签名验证。
不要在每次请求时都查询 Polar 的 API——速度慢且有频率限制。直接从数据库读取:
// middleware
function requirePlan(...plans) {
return (req, res, next) => {
if (!plans.includes(req.user.plan)) {
return res.status(402).json({ error: "Upgrade required" });
}
next();
};
}
app.post("/api/pro-only", requirePlan("pro", "max_pro"), handler);
webhook 负责保持数据库与 Polar 同步。Polar 服务器是权威数据源,你的数据库是缓存层。
不要自己搭建取消或升级界面。Polar 提供托管的客户门户。
app.post("/api/billing/portal", async (req, res) => {
const session = await polar.customerPortal.sessions.create({
customerId: req.user.polarCustomerId,
});
res.redirect(session.customerPortalUrl);
});
用户可以在此:更换绑定的信用卡、升级或降级套餐、取消订阅、查看历史账单、下载收据(含正确的税务信息)。套餐变更的折算由 Polar 处理。
metadata 会流转到订单和订阅对象,但需要从 event.data.metadata 读取(而非 event.metadata)。这个拼写错误很容易犯。定价页面:polar.sh/pricing、lemonsqueezy.com/pricing、paddle.com/pricing。