Herald

多货币定价

让一个 Stripe 产品以多种货币销售。覆盖购买页货币分组与显式选择,以及面向第三方应用的 api-ext 按货币解析接口。

让一个 Stripe 产品以多种货币销售。覆盖购买页货币分组与显式选择,以及面向第三方应用的 api-ext 按货币解析接口。

适合谁看

  • 想让同一产品以多种货币销售的 Realm Admin
  • 把 Herald 购买流程接进自己应用、需要知道权益支持哪些货币以及如何按货币解析价格行的开发者

前置条件

核心概念

Herald 没有本地商品目录。价格挂在 Stripe 的 Price 对象上,同步时每个 Price 生成一条独立的 Entitlement Mapping 行。一个配了 USD 月付、USD 年付、EUR 月付的产品,同步后就是三行映射。多货币能力完全建立在这个模型上:不新增任何存储,每行自带的货币就是全部事实。

没有默认货币,也没有用户级偏好货币。购买页展示全部已配置货币,用户显式选择一个之后才出现价格行;应用侧则由调用方显式传 currency 参数解析。无论哪一侧选择,最终扣的都是被选中的那行映射。

货币本身定位不了一行价格。同一产品可以同时有 USD 月付和 USD 年付,所以货币只是过滤条件,不是唯一键——(权益 + 计费类型 + 计费周期 + 货币) 才能锁定一行。凡是解析结果不是恰好一行,Herald 都会拒绝而不是猜一个。不会出现静默换币,也不会出现零金额扣款。

第 1 步:在 Stripe 搭建多货币目录

  1. Stripe Dashboard → Products 打开你的产品
  2. 每种货币加一个 Price,比如在 $9.99 / month 之外加 €9.99 / month¥69 / month
  3. 在 Herald 打开 Entitlement Mappings,执行 Sync Provider Products

每个 Price 到达后各占一行映射,带各自的货币和金额。把要卖的行启用。禁用的行不会出现在购买页,也不会进入提供给第三方应用的货币集合。

第 2 步:用户在购买页看到什么

Stripe 产品有多个货币的价格时,购买页按货币分组,每个货币是一个可选按钮。没有预选货币:用户显式选择之前不渲染任何价格行——页面显示「请选择货币」的提示。选中的组内,每个计费周期一行(月付、年付并排放在 USD 组里)。

只有一种货币的产品不显示选择器——唯一的货币直接展示价格行,因为没什么可选。

其他渠道的产品也没有选择器。Creem 按产品定价,IAP 按商店地区定价,WeChat Pay 的价格手工配置在映射上——它们都只展示单一价格。

一个 Stripe 侧的注意点:如果你在 Stripe 启用了 Adaptive Pricing,Stripe 收银台可能把金额换算成买家本地货币。Herald 购买页始终展示 Price 上的基础货币,并标注「实际扣款以支付页为准」。Herald 自己不做任何换算。

第 3 步:在应用里按货币解析价格

第三方应用用两个接口处理货币。都走 API key(X-API-Key 请求头)加 billing.view 权限,且都只统计已启用的 Stripe 映射行。

查询权益支持的货币集合:

curl -H "X-API-Key: your-api-key" \
  "https://your-herald-domain/api/ext/{realmId}/entitlements/{entitlementKey}/currencies"
{
  "entitlementKey": "pro-plan",
  "currencies": ["USD", "EUR", "CNY"]
}

按货币解析默认价格行,可用计费类型和周期收窄范围:

curl -H "X-API-Key: your-api-key" \
  "https://your-herald-domain/api/ext/{realmId}/entitlements/{entitlementKey}/default-price?currency=EUR&billingType=recurring&billingPeriod=monthly"
{
  "mappingId": "0198c7e4-...",
  "entitlementKey": "pro-plan",
  "paymentProvider": "stripe",
  "currency": "EUR",
  "amount": 999,
  "billingType": "recurring",
  "billingPeriod": "monthly",
  "externalPriceId": "price_..."
}

拿响应里的 mappingId 作为购买支付尝试的 targetId 发起购买——和浏览器购买页走的显式选行是同一条路径。

失败行为是有意区分的:

  • 400 —— 货币码缺失或格式非法
  • 404 —— 该权益在这个货币下没有已启用的 Stripe 行。不会回退到任何其他货币,怎么提示用户由调用方决定
  • 409 —— 匹配到多行(比如只传 currency=USD 时同时存在 USD 月付和年付)。补上 billingTypebillingPeriod 再试

货币码与目录比较时不区分大小写,响应统一返回大写 ISO 码(usdUSD 是同一货币)。订阅类和一次性映射都带货币——订阅详情和一次性映射响应里有 currency 字段。

容易踩的规则

货币永远是显式选定的。 没有 Realm 默认货币、没有用户偏好、没有回退链。购买页在用户选定货币后才渲染价格行;程序化接口只按你传入的货币解析一次,解析不了就报错。这正是为了不让用户被以一个从没选过的货币扣款。

Stripe 行缺价格信息会被拒绝,不会补默认值。 Stripe 映射行万一缺了价格数据,购买会以明确错误被拒绝,而不是编一个金额发出去。健康的行上显式 targetId 购买不受影响。store 侧定价渠道(Creem、IAP、WeChat Pay)恰好相反:它们的行本来就不带 Herald 侧价格,因为订单金额由渠道决定。WeChat 映射是唯一的手工场景——价格和货币在创建弹窗里填写,随每笔订单下发。

排查

刚加的货币 default-price 返回 404

依次确认:映射行是 Enabled;行的 provider 是 Stripe(Creem/IAP/WeChat 行永远不会被考虑);在 Stripe 加完 Price 之后确实重新同步过。

default-price 返回 409

该权益在这个货币下有多行——常见是月付加年付。补上 billingTypebillingPeriod 再试。

default-price 拒绝我传的货币码

货币码必须是恰好 3 个大写字母,XXX/XTS 是保留码。美元、带空格的 usd、4 位码都会以 400 被拒,不会进入解析。

检查清单

  • Stripe 产品为每个要卖的货币各配了一个 Price
  • 执行过 Sync Provider Products;每个货币行可见且 Enabled
  • 用非默认货币完成过一次购买;扣款货币与所选行一致
  • (应用)渲染货币选择器前先调 currencies404/409 有面向用户的处理

On this page