Stripe / Pix / 订阅与 New-API 充值对接¶
1. 资金与额度链路¶
用户持有 New-API 后台 PAT
→ POST /api/payments/checkout(支付服务向 /api/user/self 验证身份)
→ Stripe Checkout(BRL)
→ Stripe 签名 Webhook
→ PostgreSQL 去重账本 + advisory lock
→ New-API POST /api/user/manage { action:add_quota, mode:add }
→ 账本标记 credited
不要相信浏览器提交的用户 ID。 Checkout 接口从调用者的 New-API 后台 Bearer Token 获取用户 ID;普通 /v1 推理 Key 不是后台 PAT,不能用于创建订单。
2. Stripe 后台配置¶
- 完成企业/KYC、结算币种、税务信息和 Pix 权限审核;先在 sandbox 测试。
- 创建两个 BRL Price:
- 一次性充值 Price,填入
STRIPE_ONE_TIME_PRICE_IDS。 - 周期性订阅 Price,填入
STRIPE_SUBSCRIPTION_PRICE_IDS。 - 多个 Price 用英文逗号分隔。代码采用白名单,客户端不能传入任意价格。
- Payment methods 中启用 Pix;一次性默认
STRIPE_ONE_TIME_METHODS=pix。 - 订阅默认
card。Stripe 当前也支持符合条件的跨境商家通过 Pix Automático 做周期扣款,但巴西本地 Stripe 商家不可用;能否启用取决于账户所在地、资格和 mandato 流程。只有实际 sandbox 验证后才把订阅方式改为pix。 - 在 Developers / Workbench → Webhooks 添加:
- URL:
https://api.example.com.br/api/payments/webhook - 事件:
checkout.session.completed、checkout.session.async_payment_succeeded、invoice.paid - 必须额外订阅:
charge.refunded、charge.dispute.created、charge.dispute.closed(服务会落账留痕,见第 7 节)。 - 建议额外订阅
invoice.payment_failed用于告警,当前服务会记为 ignored,不充值。 - 将 Webhook Signing Secret 写入
STRIPE_WEBHOOK_SECRET。Secret Key 和 Webhook Secret 不可放到前端。
Pix 是一次性即时确认支付方式;订阅续费以
invoice.paid为准。订阅的checkout.session.completed被明确忽略,避免首期重复入账。Pix Automático 的预扣款通知与处理延迟由 Stripe 处理,只有最终invoice.paid才入账。
3. 创建 Checkout Session¶
curl -X POST https://api.example.com.br/api/payments/checkout \
-H "Authorization: Bearer NEW_API_USER_DASHBOARD_PAT" \
-H "Idempotency-Key: order-20261109-0001" \
-H "Content-Type: application/json" \
-d '{"mode":"payment","priceId":"price_xxx"}'
订阅把 mode 改为 subscription 并使用订阅 Price。成功响应包含 id 和 url,将用户 303 重定向到 url。同一用户、同一 Idempotency-Key 会命中 Stripe 幂等创建;不同订单必须生成新键。
4. BRL → USD → quota¶
金额全部以 Stripe 的最小货币单位(centavos)处理,使用 decimal.js,不使用浮点乘法:
USD 入账面值 = amount_brl_minor ÷ 100 ÷ BRL_PER_USD
quota = floor(USD 入账面值 × QUOTA_PER_USD × CREDIT_FACTOR)
示例:R$100.00、汇率 5.60 BRL/USD、QUOTA_PER_USD=500000、系数 1.0:
100 ÷ 5.60 × 500000 = 8928571.428...
最终入账 = 8928571 quota(约 US$17.857142 的站内面值)
BRL_PER_USD是财务结算汇率,不是实时行情;汇率更新只影响新订单,历史订单保留当时的汇率快照。QUOTA_PER_USD必须与 New-API 的QuotaPerUnit一致,默认 500,000。CREDIT_FACTOR=1.10表示额外赠送 10%,不得用于掩盖汇率或手续费。- Stripe 费用、IOF、税、汇兑与退款准备不自动从额度中扣除,应计入售价。跨境 Pix 的 IOF 由 Stripe/合作方按账户与页面配置处理;上线前由巴西会计师确认展示方式和实际费率。
- 当前只接受
currency=brl,其他币种事件进入failed,不会入账。
5. 幂等与异常复核¶
payment_credits.credit_key 使用 checkout:<session_id> 或 invoice:<invoice_id>,数据库唯一约束防止不同事件重复充值;stripe_events.event_id 防止同一事件重复处理。同一单据的并发处理由 PostgreSQL advisory lock 串行化。
New-API 的 add_quota 是原子增量,但不接受可用于去重的业务订单号。虽然请求携带 Idempotency-Key,当前 New-API 并不会据此去重。因此:
- 明确成功:
credited。 - 明确 4xx 拒绝(401/403/400 等配置类错误):
failed,需修复配置/用户问题;webhook 返回 200,Stripe 不再重试。 - 超时、5xx、响应不可读、以及 408/425/429 瞬态错误:
requires_review,服务绝不自动重放加额;webhook 返回 503,Stripe 会在约 3 天内指数退避重试,这就是人工对账窗口。 - 服务崩溃后发现已有未确认尝试:同样进入
requires_review。
复核 SQL:
docker compose exec postgres psql -U newapi -d newapi -c \
"SELECT credit_key,user_id,quota,status,attempts,last_error_code FROM payment_credits WHERE status IN ('requires_review','failed') ORDER BY updated_at;"
人工核对 New-API 用户额度历史、操作审计和 Stripe 单据:
- 若 New-API 已入账,只把本地记录改为
credited(事件在 Stripe 侧继续重试也无害,账本会直接判重):sql UPDATE payment_credits SET status='credited', credited_at=now() WHERE credit_key='<key>'; - 若确定未入账且原因已修复,重置为待处理,Stripe 的下一次重试会自动重新执行加额(通常几分钟内):
sql UPDATE payment_credits SET status='pending', attempts=0, last_error_code=NULL WHERE credit_key='<key>'; - 若 Stripe 重试已超过 3 天(事件过期),只能手动驱动:按上方重置后,用 Stripe CLI
stripe events resend <event_id>补发。 - 不要直接执行
add_quota后又不更新本地账本,否则后续重放可能双入账。
生产应为 requires_review、failed 和超过 5 分钟仍处于 processing 的记录配置外部告警;这些是资金对账事项,不是普通日志噪音。
6. Stripe CLI 测试¶
本地/服务器测试:
stripe login
stripe listen \
--events checkout.session.completed,checkout.session.async_payment_succeeded,invoice.paid \
--forward-to https://api.example.com.br/api/payments/webhook
若使用 Stripe CLI 转发到公网,CLI 输出的 whsec_... 要临时写入服务并重启;生产后台 Webhook Secret 与 CLI Secret 不通用。
stripe trigger checkout.session.completed
stripe trigger invoice.paid
stripe trigger 的样例通常缺少 new_api_user_id,预期结果是 failed/INVALID_USER_ID,不代表真实充值测试成功。完整验收必须:
- 用测试用户 PAT 创建真实 sandbox Checkout。
- Pix 在 Stripe 提供的测试页面完成二维码/测试确认;不能用生产二维码伪造回调。
- 重发同一事件,确认额度只增加一次。
- 对订阅生成首期与下一周期 paid invoice,确认每张 invoice 各入账一次。
- 模拟 New-API 超时,确认进入人工复核而非二次加额。
- 核对 Stripe Balance Transaction、支付账本、New-API 审计三者的单据 ID、金额和 quota。
7. 退款、拒付和取消¶
当前版本不会自动扣回额度。charge.refunded / charge.dispute.* 事件会以 ignored 状态 + CHARGE_REFUNDED/CHARGE_DISPUTED 错误码写入 stripe_events 并输出 error 日志,但不会自动扣减 quota:
SELECT event_id, event_type, error_code, received_at FROM stripe_events
WHERE error_code IN ('CHARGE_REFUNDED','CHARGE_DISPUTED') ORDER BY received_at DESC;
退款、Pix dispute、订阅取消和 invoice.payment_failed 必须进入客服/财务工单:先冻结异常令牌,再根据已消费额度、合同和巴西消费者规则决定扣减或追缴。不要简单把余额扣成负数。自动退款和负余额策略上线前需另行设计与测试。
8. 上线核对¶
- sandbox 全套测试通过后才换
sk_live、正式 Price 和生产 Webhook Secret。 - 测试密钥与正式密钥不可混用;更换环境时核对所有 Price ID。
- Stripe Webhook 签名失败返回 400;永久失败事件返回 200(不重试);
requires_review返回 503(Stripe 持续重试 = 对账窗口)。 NEW_API_ADMIN_TOKEN仍是BOOTSTRAP_REPLACE_...占位符时,webhook 一律返回 503PAYMENTS_NOT_CONFIGURED,/ready返回 503——资金事件不会被丢弃,但必须尽快替换 token 并docker compose up -d payments重建容器。- 每日自动备份 PostgreSQL,并保留 Stripe 对账导出;备份恢复必须做演练。