
# Sub-Account Auto-Recharge (Custom Payment Provider)

If you prefer to handle sub-account credit payments through your own provider instead of Stripe (bank transfers, local payment gateways, custom billing systems), the platform exposes a webhook + API pair so you can run the full charge-and-grant loop yourself. The non-technical overview lives on the [Agency Accounts](agency-accounts.md#option-2-custom-payment-provider) page; this page covers the exact webhook payload and the API call you make to grant credits afterwards.

The webhook is only one of the ways automatic top-ups get paid for. If you connected [Stripe](agency-accounts.md#option-1-connect-stripe) or [PayPal](agency-accounts.md#option-3-connect-paypal) in SaaS Mode, the platform charges the client's saved card or PayPal account itself and grants the credits, so there is nothing on this page for you to build. Read on only if you want to handle the payment yourself.

***

## The flow at a glance

1. A sub-account's credit balance drops below their auto-recharge threshold.
2. The platform calls your **webhook URL** with the sub-account details and how many credits they need.
3. Your server charges the customer through whichever provider you use.
4. Your server calls the **Grant Credits API** to add credits to that sub-account.
5. Your server responds `200` to acknowledge the webhook.

***

## 1. Webhook: `agency_sub_account_auto_recharge`

Configure the webhook URL by clicking **SaaS Mode** in the main sidebar, choosing **Use a Custom Payment Provider Instead** (or, once configured, opening the **Custom Payment Provider** card), and filling in the **Auto-Recharge Webhook** field.

### When it fires

When a sub-account's credit balance drops below their configured auto-recharge threshold.

### Payload

```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"
}
```

| Field | Description |
|---|---|
| `event` | Always `agency_sub_account_auto_recharge` for this webhook. |
| `sub_account_id` | The unique ID of the sub-account that needs credits. |
| `sub_account_email` | The sub-account's email address. |
| `sub_account_name` | The sub-account's display name. |
| `agency_id` | Your agency account's unique ID. |
| `credits_requested` | How many credits to grant. This is your configured recharge amount, but if the balance has fallen further below the threshold than that amount would cover, it is automatically raised to whatever is needed to bring the balance back above the threshold in one go. Always charge for (and grant) the `credits_requested` value from the payload — don't hard-code your recharge amount. |
| `current_balance` | The sub-account's credit balance at the time the webhook was sent. |
| `threshold` | The balance threshold that triggered the recharge. |
| `price_per_credit_cents` | Your configured price per credit, in cents. |
| `price_per_credit_currency` | The currency for the price (e.g., `usd`). |
| `total_amount_cents` | The total amount to charge, in cents (`credits_requested` × `price_per_credit_cents`). |
| `timestamp` | When the webhook was sent (ISO 8601 format). |
| `idempotency_key` | A unique key for this specific recharge request. Use this to prevent granting credits twice if your server receives the same webhook more than once. |

### Important notes

- **5-minute cooldown** — After a recharge attempt for a sub-account, the platform will not send another webhook for that sub-account for at least 5 minutes, even if their balance drops further. Prevents duplicate charges during processing.
- **Use the idempotency key** — Always check `idempotency_key` before granting credits. If your server crashed after granting credits but before responding, the platform may resend the webhook on the next credit drop.
- **Failures are safe and self-healing** — If your webhook URL is unreachable or returns an error, no credits are granted. The platform keeps re-sending the webhook (once per 5-minute cooldown) as long as the sub-account stays below the threshold — it does **not** require the balance to climb back above the threshold and drop again first. A single missed charge can no longer leave a sub-account permanently un-recharged.
- **Set your recharge amount at or above the threshold** — Your configured recharge amount must be greater than or equal to the auto-recharge threshold, so one recharge always restores the balance above the threshold. (If you ever need to fix a sub-account that drifted far below the threshold, the platform tops it up by the full deficit automatically — see `credits_requested` above.)

***

## 2. Grant Credits API

After your server has processed payment, call this endpoint to add the credits to the sub-account.

### Request

```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"
}
```

| Field | Required | Description |
|---|---|---|
| `email` | Yes | The sub-account's email address (must match an existing sub-account under your agency). |
| `amount` | Yes | The number of credits to grant. |
| `description` | No | A note describing why credits were added (shown in the credit transaction history). |

**Authentication:** Send your API key in a request header — either `X-API-Key: YOUR_API_KEY` or `Authorization: Bearer YOUR_API_KEY`. Headers are the recommended way, because a key in the URL ends up in browser history, proxy logs and server access logs. The `?apiKey=YOUR_API_KEY` query parameter and an `apiKey` field in the JSON body also still work, so older integrations keep running unchanged.

### Response

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

::: tip
**Tip:** Store the `idempotency_key` from the webhook payload and check it before calling this endpoint. This prevents accidentally granting credits twice if your server receives the same webhook more than once.
:::


### What happens to granted credits at the monthly reset

This depends on how the sub-account is set up:

- **Reselling** (the sub-account pays you for credits): credits you grant here are recorded as purchased credits and **carry over** every month. Whatever the sub-account has not spent stays on the balance.
- **Allocation** (you give the sub-account a monthly allowance): the balance is refilled to the monthly allowance on the reset date, so anything unspent is not carried over. This is intentional — the allowance is granted fresh each month.

If you want a sub-account's leftover balance to be added on top of its new monthly allowance instead of replacing it, turn on credit roll-over:

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

If the sub-account — or the plan it is on — also has a roll-over cap or an expiry set, the reset is the moment those apply: the carried-over allowance is trimmed to the months you allow, and allowance left unused for longer than the expiry window is dropped, before the new allowance is added. Credits granted through this endpoint, auto-recharges and the client's own top-ups are one-time credits and are never trimmed. See [Capping what rolls over](sub-accounts.md#capping-what-rolls-over).

***

## End-to-end example

A typical handler looks like this:

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