
# 子账户自动充值（自定义支付提供商）

如果您更倾向于通过自己的提供商（银行转账、本地支付网关、自定义计费系统）而非 Stripe 处理子账户额度支付，本平台提供了一对 Webhook + API，以便您自行运行完整的“扣费与授权”流程。非技术性概述请参阅 [代理账户](agency-accounts.md#option-2-custom-payment-provider) 页面；本页面涵盖了具体的 Webhook 有效载荷以及随后用于授予额度的 API 调用。

Webhook 只是自动充值付款方式中的一种。如果您在 SaaS 模式下连接了 [Stripe](agency-accounts.md#option-1-connect-stripe) 或 [PayPal](agency-accounts.md#option-3-connect-paypal)，平台会自动向客户保存的银行卡或 PayPal 账户扣款并授予额度，因此您无需在此页面进行任何开发。仅当您希望自行处理付款时，才需要继续阅读。

***

## 流程概览

1. 子账户的额度余额低于其自动充值阈值。
2. 平台调用您的 **Webhook URL**，并附带子账户详情及所需额度数量。
3. 您的服务器通过您使用的任何提供商向客户扣费。
4. 您的服务器调用 **授予额度 API** 为该子账户添加额度。
5. 您的服务器响应 `200` 以确认收到 Webhook。

***

## 1. Webhook: `agency_sub_account_auto_recharge`

通过点击主侧边栏中的 **SaaS 模式**，选择 **改用自定义支付提供商**（或在配置完成后打开 **自定义支付提供商** 卡片），并填写 **自动充值 Webhook** 字段来配置 Webhook URL。

### 触发时机

当子账户的额度余额低于其配置的自动充值阈值时触发。

### 有效载荷

```json
{
  "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

在您的服务器处理完支付后，调用此端点将额度添加到子账户。

### 请求

```http
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` 字段仍然有效，因此旧的集成可以保持不变继续运行。

### 响应

```json
{
  "success": true,
  "sub_account_id": "abc123xyz",
  "credits_added": 500,
  "new_balance": 542
}
```

::: tip
**提示：** 存储来自 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 }
}
```

如果子账户（或其所属的套餐）设置了结转上限或有效期，则重置操作会在这些规则生效时执行：结转的额度会被修剪至您允许的月份数，且超过有效期未使用的额度会在添加新额度之前被清除。通过此端点授予的额度、自动充值以及客户自行进行的充值均为一次性额度，不会被修剪。请参阅 [限制结转额度](sub-accounts.md#capping-what-rolls-over)。

***

## 端到端示例

典型的处理程序如下所示：

```pseudo
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
```
