关于 salla-app-billing
Salla App Billing Flow 是面向 Salla 应用开发者的技能,用于实现商户套餐、附加项、试用和按量计费。它指导通过 Salla API 和 Partners MCP 配置价格、处理 Webhook、跟踪权益及核对订阅。
主要功能
- 01在定价配置中设置套餐和附加项
- 02连接计费事件 Webhook
- 03跟踪套餐与附加项权益
- 04处理续订、到期和试用
- 05通过 API 核对订阅
适用场景
- 01为 Salla 应用设置订阅价格
- 02按套餐或附加项状态控制功能
- 03从遗漏的 Webhook 中恢复计费状态
- 04管理外部周期续订
MCP 相关
使用 Salla Partners MCP 配置并验证发布定价、设置应用 Webhook 地址及读取订阅详情。技能还介绍通过 Salla Admin API 核对订阅和写入用量余额。
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ sallaapp/ salla-partners-agent-kit --skill salla-app-billing安装后在 Claude Code 中输入/salla-app-billing确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
references/pricing-shapes.mdsubscription-api.mdsubscription-events.md详细介绍
Salla 应用计费流程
设置商家如何为你的应用付费,并准确了解每位商家的套餐状态。 计费由 Salla 管理——你需要响应事件,并(可选)读取订阅端点; 你无需自行收取银行卡费用。请按顺序完成以下步骤;完成每个阶段后再继续。 步骤 1、2 和 5 会使用 Salla Partners MCP 执行操作;步骤 3–4 和 6 是你需要编写的运行时逻辑。
唯一可信来源
套餐状态由事件驱动——依据经过验证的 app.subscription.* /
app.trial.* Webhook 更新,然后通过以下地址的 Admin(Merchant)API 进行核对:https://api.salla.dev/admin/v2(OAuth,offline_access)。此技能使用两个实际端点:
GET /apps/{app_id}/subscriptions— 获取应用的订阅状态和详细信息,涵盖套餐和附加项(通过item_type筛选):套餐状态、权益、日期、subscription_balance(用于核对;步骤 5)。完整的 OpenAPI Schema(唯一可信来源): https://docs.salla.dev/5401098e0.mdPOST /apps/balance— 写回按量付费(Pay-As-You-Go)的使用余额(步骤 5b)。POST /apps/subscriptions/{subscription_id}/renew— 对于external_recurring套餐/附加项,每次续订都由合作伙伴发起(Salla 不会自动续订)。从订阅 Webhook 获取subscription_id;需要offline_access。返回续订后的订阅(item_type/item_slug/plan_*/start_date/end_date/subscription_balance/features)。 处理以下错误:not_renewable、subscription_not_active、auto_renew_disabled、payment_failed(403)、rate_limit_exceeded(429 — 每天一次)。适用于support_renew: true的附加项。(由 Salla 管理的周期性订阅会自动续订——你只会收到app.subscription.renewed。)请从在线 OpenAPI 文档中读取准确的请求/响应 Schema 和完整的错误响应约定——这是唯一可信来源: https://docs.salla.dev/37396517e0.md(不要手动编写数据结构;请以文档为准)。
开始编码前,请通过 App Events 参考文档
(https://docs.salla.dev/421413m0.md)或 salla_events action=list 确认负载和字段结构。Salla Partners MCP 执行以下操作:
| 工具 | 操作 | 作用 |
|---|---|---|
app_publish | set validate | 设置 pricing 部分(套餐/附加项),然后验证发布 |
salla_apps | connect | 设置 webhook_url,用于自动接收应用事件(app.subscription.* / app.trial.*) |
salla_apps | subscriptions | 只读:读取应用的订阅详情 |
套餐与附加项
| 概念 | 说明 | item_type | item_slug |
|---|---|---|---|
| 套餐 | 商家在应用中的主订阅 | "plan" | null |
| 附加项 | 在套餐基础上额外购买的项目 | "addon" | 附加项标识符 |
两者都会触发相同的 app.subscription.* / app.trial.* 事件。**此技能涵盖套餐、附加项、权益限制和用量计费。**在应用内购买附加项 → salla-addon-purchase。plan_type 的取值:free · once(一次性)·
recurring(按月/按年)· on_demand(按需付费)。
步骤 0 — 了解需求
开始前请询问:
- 你会提供哪些套餐类型?(免费、按月、按年、试用、一次性、按需付费)
- 每种套餐会解锁哪些功能?(
features[]→ 权益映射) - 付费套餐前是否需要免费试用?
步骤 1 — 定义定价计划(publication 的 pricing 部分)
计划和附加项都在 publication 的 pricing 部分中定义 —
app_publish action=set section=pricing data={…},然后执行 app_publish action=validate。没有
独立的定价端点;Portal 的 Pricing 向导步骤是基于同一部分的 UI 辅助工具。发布机制由
salla-publication-consistency 负责;pricing 数据结构如下:
plan_type—"free"|"recurring"|"once"|"on_demand"(必填;精确的 API 值 — 不使用one_time或pay_as_you_go)。它决定适用哪些字段。free受资格限制 — 仅当can_have_free_plan为 true 时才可用(shipping/communication app 或show_app_free_plan功能);提供免费计划前,请从 app details 中读取该值(→ references/pricing-shapes.md)。- Recurring —
plans[](≤8;0–4 个 monthly,0–4 个 yearly)。每个计划:name{ar,en}、subtitle{ar,en}、price、recurring(free|monthly|yearly|one-time)、recommended、is_compare_included、hidden、initialization_cost、discount、additional_features[]、promotions[](最多 1 个)、balance(适用于 one-time/on_demand),以及用于原位更新计划的id。另有顶层的plan_features[]对比矩阵。 - Once —
one_time_price、one_time_old_price(> price)、plan_additional_features[]{key,name,price,adjustable,min,max}。不使用plans[]。 - On-demand —
plans[],其中必须包含balance+on_demand_type(emails|messages|per-transaction)。 - 附加项(所有类型,最多 5 个)—
{name,description,price,slug,support_renew}。每个条目都必须包含name、price和slug— 缺少任一项时,服务器都会拒绝该附加项。所有价格均以 SAR 计价;没有货币字段。始终为一次性;support_renew是唯一的周期性控制项:true⇒external_recurring(通过 renew API 管理续订;请在附加项标题/描述中说明周期),否则为once事件。 - 试用字段位于顶层:
plan_trial(天数,最小值为 1,受 max-trial-days 限制 — 默认值为 7)+trial_description(30–1000)。流失挽回:unsubscribe_reward、unsubscribe_email_reward。
命名陷阱:
plan_type:"recurring"(模型)≠ 每个计划的recurring:"monthly"(周期);plan_additional_features(顶层,once)≠ 每个计划的additional_features(recurring)。
完整字段表、类型和服务器规则:请加载 references/pricing-shapes.md。
使用 app_publish action=set section=pricing 设置此部分,然后执行 app_publish action=validate(保存草稿)。完整发布流程 → salla-publication-consistency。
| 计划类型 | 用途 |
|---|---|
| Free | 不收费 |
| Monthly / Yearly | 周期性收费(plan_type: recurring) |
| Trial | 付费计划前的限时免费使用 |
| One-Time | 单次收费(plan_type: once) |
| Pay As You Go | 按使用量收费 / 按需(plan_type: on_demand) |
| Addon | 计划之外的额外项目(在 addons 中定义) |
应用创建后,App details → Custom Plans 可配置按商户定制的计划。
检查项:“计划是否已在 publication 的 pricing 部分中配置
(app_publish action=set section=pricing),且 publication 验证通过?”
第 2 步 — 设置 Webhook URL(应用事件会自动投递)
事件是你的事实来源——这些是应用事件(app.subscription.* /
app.trial.*),因此应用默认已订阅这些事件:Salla 会将所有事件自动投递到你的 webhook_url。对于任何 app.* 事件,都不要调用 salla_events action=subscribe。此处唯一要做的是设置接收端:
salla_apps action=connect、app_id、webhook_url、webhook_security_strategy: "signature"— 将应用事件指向你的处理程序(→ 关于完整的连接和密钥同步流程,请参阅 salla-app-lifecycle 第 1 步)。
salla_events action=subscribe 仅适用于非应用(商店)事件——应用希望响应的 order.*、
product.*、customer.*、cart.* 和商店端的 shipment.*。计费完全由应用事件处理,因此此技能无需调用 subscribe。
应用事件会自动投递——设置 webhook_url,然后处理这些事件。 每个计费事件——
app.subscription.started、app.subscription.renewed、app.subscription.expired、
app.subscription.canceled 以及 app.trial.* 事件——触发时都会立即到达你的 webhook_url,
无论它是否出现在 salla_events action=list 目录中(renewed/expired/canceled 甚至不在其中)。你的任务是在 webhook 处理程序(第 4 步)中处理所有这些事件,而不是订阅其中任何事件。应用事件参考
(https://docs.salla.dev/421413m0.md) 将每个事件都列为由平台触发。
检查项:“是否已通过 salla_apps action=connect 设置 webhook_url(应用事件会自动投递到此地址),并且处理程序覆盖了完整的事件系列——app.subscription.started/renewed/expired/
canceled + app.trial.*(第 4 步)?是否没有对任何 app.* 事件调用 salla_events action=subscribe——subscribe 仅用于商店事件。”
第 3 步 — 根据事件跟踪方案状态
安全 — 事件会授予付费访问权限。 伪造或重放订阅/试用 webhook 可能会授予(或撤销)付费功能。更改方案/权益状态前,先验证 webhook 签名并强制执行幂等性——此传输层(签名验证、重放保护、快速返回 2xx)由 salla-webhooks 负责;令牌/OAuth 由 salla-app-auth 负责。只有来自已验证服务器事件或经过协调的 Partners API(第 5 步)的信息,才能作为权威的权益变更依据;绝不能采用客户端上报的方案。通过你自己的经过身份验证的管理员路径进行权益读取/写入,并且不要将签名密钥和令牌写入日志。
通过 salla-app-lifecycle 接入这些事件。此处需要关注的变化:
- 根据
item_type分支处理 —"plan"更新方案状态;"addon"更新附加组件权益 (同一类 payload;item_slug用于标识具体的附加组件)。 - 试用事件的 payload 结构相同 — 也要处理
app.trial.started/.expired/.canceled, 否则试用开始/到期事件会被遗漏,导致商家的访问控制出现错误。 - 在
started/renewed事件中保存end_date ?? renew_date;在expired/canceled事件中将订阅标记为非活动状态。 - 当
store_type !== "live"时跳过计费逻辑(开发/演示商店)。
payload 字段和完整示例 → references/subscription-events.md。实时文档 — 应用订阅 Webhook 事件:https://docs.salla.dev/2213496m0.md;应用事件参考(包含 payload 示例的生命周期 webhook):https://docs.salla.dev/421413m0.md。
检查项:“演示商店的订阅事件是否会以正确的状态更新已存储方案?”
第 4 步 — 处理续订、到期和试用
这三种都是应用事件——会自动发送到你的 webhook_url(你无需订阅任何 app.* 事件——参见第 2 步);你需要处理这些事件:
app.subscription.renewed— 确认订阅仍处于有效状态;保存新的end_date/renew_date。不要假定结束日期没有变化。app.subscription.expired— 方案已失效(未续订)。限制访问;可考虑显示宽限期横幅。app.subscription.canceled— 商家已取消。根据你的策略限制访问。- 试用会以
app.trial.started/.expired/.canceled事件发送(零价格的plan_type: once)。收到trial.started时启用试用功能并记录试用结束时间;到期或取消时降级。商家转为付费订阅后,会触发app.subscription.started。
根据保存的 status + end_date 控制功能访问,而不要根据事件到达的时刻来判断(事件可能延迟或重复——处理时应确保幂等)。第 5 步可作为漏收事件时的补救措施。
检查点:“续订会更新 end_date;到期或取消会限制访问;试用门控是否正确?”
第 5 步 — 通过应用订阅详情 API 对账
服务中断后(错过 Webhook),使用 salla_apps action=subscriptions 对账,该命令会调用真实的 应用订阅详情端点:
GET https://api.salla.dev/admin/v2/apps/{app_id}/subscriptions
Authorization: Bearer <access_token> # OAuth, offline_access
{app_id} 是你的 Salla 应用 ID(Salla Partners → My Apps → Your App)。此调用使用商家的访问令牌,因此 data[] 中是该商家为此应用订阅的内容——每个有效方案或附加项对应一条记录。文档:
https://docs.salla.dev/5401098e0.md。
响应(200) — { status, success, data: [...] },每一项都是订阅详情(可从中读取方案状态、权益和用量余额)。读取 item_type(筛选 "plan" 以获取方案状态)、plan_type、start_date / end_date、features[]
({ key, quantity })以及 subscription_balance(用量余额,通过第 5b 步写入)。403 表示调用方没有此应用的权限。
完整响应示例及每个字段的含义(以及 POST /apps/balance 的数据结构): 请查看 references/subscription-api.md。
检查点:“对账是否返回商家当前的方案和余额,并与保存的状态一致?”
第 5b 步 — 更新用量余额(按量付费)
对于 on_demand(按量付费)方案,使用真实的更新订阅余额端点,将商家的用量余额写回 Salla:
POST https://api.salla.dev/admin/v2/apps/balance
Authorization: Bearer <access_token> # OAuth, offline_access
Content-Type: application/json
{ "balance": 2399 }
balance(整数)为必填项。文档:https://docs.salla.dev/5401099e0.md。
响应(201) — { status, success, data: { message, code } }。
校验错误(422) — { status: 422, success: false, error: { code, message, fields: { balance: ["..."] } } }。
安全性。 在服务器端根据已验证的用量计算余额,并且只能通过你自己的身份验证管理路径执行
POST——绝不要使用客户端上报的值。通过GET /apps/{app_id}/subscriptions(第 5 步)重新读取余额以确认。(信任模型与第 3 步相同。)
检查点:“余额是否通过 POST /apps/balance 写入,并通过重新读取订阅确认?”
附加项
附加项是在方案之外可额外购买的内容,可以是定期或一次性项目,定义在发布请求的 addons 数组中(第 1 步)。它们通过相同的事件系列发送,且 item_type: "addon"(item_slug 用于标识附加项),因此你应像记录方案一样记录它们,并启用其 features[]。如需应用内购买界面(嵌入式 SDK Checkout),请使用 salla-addon-purchase。
权益门控
将有效方案的 features[] 和每个有效附加项的 features[] 合并为每个商家的一套权益集合。每次收到 app.subscription.* / app.trial.* 事件时都重新计算该集合,并在使用功能时进行门控(查询已保存的权益集合,绝不要直接使用原始事件)。
用量余额(按使用量付费)
对于 on_demand 方案,将每项可计费操作计入商家的
subscription_balance(商家欠下的应用服务费用):操作前检查
余额,随着使用量产生而扣减,并在余额耗尽时阻止操作(或发出警告)。使用
GET /apps/{app_id}/subscriptions(步骤 5)读取余额;使用
POST /apps/balance(步骤 5b)写入余额。
步骤 6 — 架构与门控(参考)
subscriptions
merchant_id (PK / FK)
subscription_id
item_type 'plan'
plan_name
plan_type free | once | recurring | on_demand
status active | inactive | trial
start_date
end_date
features json // [{ key, quantity }]
store_type development | demo | live
updated_at
根据上文 Entitlement Gating 一节,将 features[] 映射为权益(将方案和附加项的
功能合并为一个集合)。完整载荷:
references/subscription-events.md。
风险信号
计费事件会授予和撤销付费访问权限,因此在此处走捷径,要么会泄露付费功能, 要么会错误地阻止付费商家访问。如果你的方案中有以下任一做法,请重新阅读指定步骤。
| 容易产生的想法 | 错误原因 |
|---|---|
| “插件上的 slug / price 是可选的——我就不填写了。” | 每个插件条目都必须填写 name、price 和 slug——否则服务端会拒绝不完整的插件。缺少其中任何一项都会导致验证错误(步骤 1)。 |
| “我会直接根据事件负载解锁功能。” | 伪造或重放的 webhook 会授予付费访问权限。先验证签名并去重,再根据已存储的权益状态进行限制(步骤 3)。 |
| “套餐和插件需要使用不同的事件处理程序。” | 它们共用一个 app.subscription.* 系列——根据 item_type 分支处理(插件由 item_slug 标识)。使用不同的处理程序会漏掉一半事件(步骤 3)。 |
| “我只需要订阅事件;试用是另一回事。” | 试用采用相同的负载结构(app.trial.*)。忽略这些事件会导致试用开始或到期时的访问限制处理错误(步骤 3–4)。 |
“我会对订阅/试用事件调用 subscribe。” | 它们是应用事件——应用默认会订阅自己的应用事件,因此 Salla 会自动将整个系列(started/renewed/expired/canceled + app.trial.*)发送到你的 webhook_url。设置 webhook_url 并处理这些事件;salla_events action=subscribe 仅用于店铺事件(步骤 2、4)。 |
| “我会信任客户端发送的余额。” | 在服务端计算用量,并通过你自己的身份验证调用 POST /apps/balance;然后读取余额进行确认。客户端报告的余额可以伪造(步骤 5b)。 |
| “事件很可靠——我不需要对账。” | 停机期间漏掉 webhook 会导致存储状态永远过时。GET /apps/{app_id}/subscriptions 可用于兜底(步骤 5)。 |
| “事件到达时,我就立即限制访问。” | 事件可能延迟或重复。根据已存储的 status 和 end_date 管理访问权限,并以幂等方式重新计算(步骤 4)。 |
| “演示店铺的订阅事件也应该像真实事件一样计费。” | 当 store_type !== "live" 时跳过计费逻辑,否则开发环境中的安装会破坏真实套餐状态(步骤 3)。 |
Key Resources
| Resource | URL |
|---|---|
| App Subscription Details (GET) | https://docs.salla.dev/5401098e0.md |
| Update Subscription Balance (POST) | https://docs.salla.dev/5401099e0.md |
| Apps API | https://docs.salla.dev/421412m0.md |
| App Events | https://docs.salla.dev/421413m0.md |
| Lifecycle wiring | salla-app-lifecycle skill |
| Feature gating | this skill — see Entitlement Gating |
| Partners Portal | https://salla.partners |
| Telegram community | https://t.me/salladev |