子账户自动充值(自定义支付提供商)
如果您更倾向于通过自己的提供商(银行转账、本地支付网关、自定义计费系统)而非 Stripe 处理子账户额度支付,本平台提供了一对 Webhook + API,以便您自行运行完整的“扣费与授权”流程。非技术性概述请参阅 代理账户 页面;本页面涵盖了具体的 Webhook 有效载荷以及随后用于授予额度的 API 调用。
Webhook 只是自动充值付款方式中的一种。如果您在 SaaS 模式下连接了 Stripe 或 PayPal,平台会自动向客户保存的银行卡或 PayPal 账户扣款并授予额度,因此您无需在此页面进行任何开发。仅当您希望自行处理付款时,才需要继续阅读。
流程概览
- 子账户的额度余额低于其自动充值阈值。
- 平台调用您的 Webhook URL,并附带子账户详情及所需额度数量。
- 您的服务器通过您使用的任何提供商向客户扣费。
- 您的服务器调用 授予额度 API 为该子账户添加额度。
- 您的服务器响应
200以确认收到 Webhook。
1. Webhook: agency_sub_account_auto_recharge
通过点击主侧边栏中的 SaaS 模式,选择 改用自定义支付提供商(或在配置完成后打开 自定义支付提供商 卡片),并填写 自动充值 Webhook 字段来配置 Webhook URL。
触发时机
当子账户的额度余额低于其配置的自动充值阈值时触发。
有效载荷
{
"event": "agency_sub_account_auto_recharge",
"sub_account_id": "<sub-account-id>",
"sub_account_email": "customer@example.com",
"sub_account_name": "John Doe",
"agency_id": "<agency-id>",
"credits_requested": 500,
"current_balance": 42,
"threshold": 100,
"price_per_credit_cents": 10,
"price_per_credit_currency": "usd",
"total_amount_cents": 5000,
"timestamp": "2026-04-13T12:00:00.000Z",
"idempotency_key": "auto_recharge_abc123_1681387200000"
}
| 字段 | 描述 |
|---|---|
event |
对于此 Webhook,始终为 agency_sub_account_auto_recharge。 |
sub_account_id |
需要额度的子账户的唯一 ID。 |
sub_account_email |
子账户的电子邮件地址。 |
sub_account_name |
子账户的显示名称。 |
agency_id |
您代理账户的唯一 ID。 |
credits_requested |
要授予的额度数量。这是您配置的充值金额,但如果余额低于阈值的程度超过了该金额所能覆盖的范围,它会自动增加到足以一次性将余额恢复到阈值以上的金额。请始终按照负载中的 credits_requested 值进行收费(并授予额度)——不要硬编码您的充值金额。 |
current_balance |
发送 Webhook 时子账户的额度余额。 |
threshold |
触发充值的余额阈值。 |
price_per_credit_cents |
您配置的每额度价格(以分为单位)。 |
price_per_credit_currency |
价格的货币(例如 usd)。 |
total_amount_cents |
总收费金额(以分为单位)(credits_requested × price_per_credit_cents)。 |
timestamp |
发送 Webhook 的时间(ISO 8601 格式)。 |
idempotency_key |
此特定充值请求的唯一密钥。如果您的服务器多次收到相同的 Webhook,请使用此密钥防止重复授予额度。 |
重要注意事项
- 5 分钟冷却时间 — 在针对某个子账户进行充值尝试后,即使其余额进一步下降,平台在至少 5 分钟内也不会为该子账户发送另一个 Webhook。这可以防止处理过程中的重复扣费。
- 使用幂等键 — 在授予额度前,请务必检查
idempotency_key。如果您的服务器在授予额度后但在响应前崩溃,平台可能会在额度再次下降时重新发送 Webhook。 - 故障是安全且可自愈的 — 如果您的 Webhook URL 无法访问或返回错误,则不会授予任何额度。只要子账户保持在阈值以下,平台就会持续重新发送 Webhook(每个 5 分钟冷却周期一次)——它不要求余额必须先回升到阈值以上再下降。单次漏掉的扣费不会再导致子账户永久无法充值。
- 将充值金额设置在阈值或以上 — 您配置的充值金额必须大于或等于自动充值阈值,这样一次充值就能始终将余额恢复到阈值以上。(如果您需要修复某个远低于阈值的子账户,平台会自动补足全部差额——请参阅上文的
credits_requested。)
2. 授予额度 API
在您的服务器处理完支付后,调用此端点将额度添加到子账户。
请求
POST https://api.dmchamp.com/v1/subaccounts/credits
X-API-Key: YOUR_API_KEY
Content-Type: application/json
{
"email": "customer@example.com",
"amount": 500,
"description": "Auto-recharge via webhook"
}
| 字段 | 必填 | 描述 |
|---|---|---|
email |
是 | 子账户的电子邮件地址(必须与您代理下的现有子账户匹配)。 |
amount |
是 | 要授予的额度数量。 |
description |
否 | 描述添加额度原因的备注(显示在额度交易历史记录中)。 |
身份验证: 在请求头中发送您的 API 密钥 — 可以是 X-API-Key: YOUR_API_KEY 或 Authorization: Bearer YOUR_API_KEY。推荐使用请求头,因为 URL 中的密钥最终会出现在浏览器历史记录、代理日志和服务器访问日志中。?apiKey=YOUR_API_KEY 查询参数和 JSON 正文中的 apiKey 字段仍然有效,因此旧的集成可以保持不变继续运行。
响应
{
"success": true,
"sub_account_id": "abc123xyz",
"credits_added": 500,
"new_balance": 542
}
提示: 存储来自 Webhook 负载的 idempotency_key,并在调用此端点之前对其进行检查。这可以防止在您的服务器多次收到同一 Webhook 时意外重复授予额度。
每月重置时已授予的额度会发生什么
这取决于子账户的设置方式:
- 转售(子账户向您支付额度费用):您在此处授予的额度会被记录为已购买额度,并会在每月结转。子账户未使用的任何额度都将保留在余额中。
- 分配(您为子账户提供每月限额):余额会在重置日期重新填充至每月限额,因此未使用的任何额度都不会结转。这是有意设计的 — 每月都会重新授予限额。
如果您希望子账户的剩余余额叠加到其新的每月限额之上,而不是替换它,请开启额度结转功能:
PUT https://api.dmchamp.com/v1/subaccounts/{subAccountUid}/limits
X-API-Key: YOUR_API_KEY
Content-Type: application/json
{
"usageLimits": { "roll_over_to_next_month": true }
}
如果子账户(或其所属的套餐)设置了结转上限或有效期,则重置操作会在这些规则生效时执行:结转的额度会被修剪至您允许的月份数,且超过有效期未使用的额度会在添加新额度之前被清除。通过此端点授予的额度、自动充值以及客户自行进行的充值均为一次性额度,不会被修剪。请参阅 限制结转额度。
端到端示例
典型的处理程序如下所示:
on POST /your-recharge-webhook:
payload = request.body
if seen(payload.idempotency_key):
return 200 // already processed, ack and exit
charge_result = your_payment_provider.charge(
email = payload.sub_account_email,
amount_cents = payload.total_amount_cents,
currency = payload.price_per_credit_currency,
)
if not charge_result.ok:
return 500 // platform will retry on next balance drop
api.post("/v1/subaccounts/credits", {
email = payload.sub_account_email,
amount = payload.credits_requested,
description = "Auto-recharge via " + your_provider_name,
})
mark_seen(payload.idempotency_key)
return 200