Adjust reseller billing

Apply a signed delta to a reseller's balance. The endpoint targets the right column automatically: in credits mode it moves credits, in users mode it moves max_users. You never pass the mode explicitly.

The update runs as a single atomic relative SQL statement (credits = credits + delta, or the max_users equivalent with a cast to SIGNED). Two concurrent adjustments sum instead of racing. The read path never goes through the 60-second cache used by other reseller endpoints, so a burst of top-ups from a monthly job cannot silently overwrite each other. Every successful adjustment is written to reg_userlog with the type panel_api_billing_adjust, so it appears in the same audit trail as CMS-driven changes.

Endpoint

POST https://<your-panel-domain>/panel-api/v1/resellers/{id}/billing/adjust

Authentication

Send the API key in the Authorization: Bearer <your-api-key> header. Both key types can call this endpoint, and the same body means something different for each:

  • Admin key: an adjustment. The delta is added to (or removed from) the reseller's balance or slot cap. No other account is touched.
  • Reseller key: a transfer. Credits move between the caller and one of its sub-resellers, and the sum of the two balances never changes. See Reseller keys: transfers below.

Required scope

  • Admin key: resellers:write
  • Reseller key: subresellers:credits

Reseller keys: transfers

This is the same operation a reseller does in the panel under Resellers Network → Manage → Credits & Subscription, with the same rules:

  • Positive delta: the amount leaves the caller's balance and is added to the target. If the caller does not have enough credits, 402 insufficient_credits and nothing changes.
  • Negative delta: the amount is taken back from the target and added to the caller's balance. Only for the caller's direct sub-resellers (403 debit_direct_only otherwise), only when the panel administrator enabled Reseller credit debit in Settings (403 sub_reseller_debit_disabled otherwise), and never more than the target has (422 negative_balance_not_allowed).
  • Targets: the caller's own sub-resellers and theirs, the accounts List resellers returns for the same key. Any other ID, including the caller's own, returns 404 not_found: a reseller can never add credits to its own account.
  • Credits mode only: if the caller or the target is sold by slots (users mode), 422 mismatched_mode.
  • Permission: the caller's member group needs Can Create Subresellers (403 sub_reseller_view_not_allowed).

The same transfer (same target, same amount) sent again within 5 seconds with a different Idempotency-Key is refused with 409 duplicate_transfer, like a double click in the panel.

The debit, the credit and the entry in the credit history happen in one transaction: either all of them are applied or none. The response is the target's billing snapshot, the same shape as for an admin key. The caller's new balance is available from GET /me.

Idempotency

Every POST must include an Idempotency-Key header. Reusing the same key with the same body replays the cached response; reusing it with a different body returns 409 idempotency_conflict. For a monthly billing job, use the invoice ID as the key so that a retry after a timeout never stacks a second charge on top.

Path parameters

Name Type Description
id int The reg-user ID of the reseller.

Request body

Field Type Required Default Description
delta float yes Signed change. Positive to top up, negative to claw back. In users mode the value is cast to int on the server.
reason string no "" Free-form label written to the audit log. Truncated to 100 characters.
{
  "delta": 1,
  "reason": "docs sample adjust +1"
}

Response

The response is the billing snapshot after the adjustment, in the same shape as GET /resellers/{id}/billing.

{
  "mode": "credits",
  "credits": 11,
  "max_users": null,
  "active_users": null,
  "billing_expires": null
}

Fractional credits are preserved. Adding 0.10 to a balance of 0.25 produces 0.35, not 0. Do not floor or round in your own code before sending the delta.

Examples

cURL

curl -X POST https://<your-panel-domain>/panel-api/v1/resellers/262260/billing/adjust \
  -H "Authorization: Bearer <your-api-key>" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: invoice-INV-2026-00814" \
  -d '{"delta":1,"reason":"docs sample adjust +1"}'

PHP SDK

require __DIR__ . '/api-panel-php-sdk-1.3.0/autoload.php';
use XtreamAI\PanelApi\PanelApiClient;

$client = new PanelApiClient(baseUrl: 'https://<your-panel-domain>', token: '<your-api-key>');
$billing = $client->resellers->adjustBilling(
    id:             262260,
    delta:          1,
    reason:         'docs sample adjust +1',
    idempotencyKey: 'invoice-INV-2026-00814',
);
echo $billing->credits, PHP_EOL;

Python SDK

from xtream_ai_panel_api import PanelApiClient

client = PanelApiClient(base_url="https://<your-panel-domain>", token="<your-api-key>")
billing = client.resellers.adjust_billing(
    262260,
    delta=1,
    reason="docs sample adjust +1",
    idempotency_key="invoice-INV-2026-00814",
)
print(billing.credits)

Errors

HTTP Error slug When it happens How to fix
400 missing_idempotency_key The Idempotency-Key header was not sent. Add the header on every POST.
400 invalid_body The delta field is missing from the body. Include delta as a signed number.
401 invalid_key Missing, malformed, or unknown API key. Send a live key in Authorization: Bearer <token>.
402 insufficient_credits Reseller key: the caller's balance is lower than the positive delta. Transfer less, or top up the caller first.
403 insufficient_scope An admin key without resellers:write, or a reseller key without subresellers:credits. Issue a new key with the scope named in details.required_scope.
403 sub_reseller_view_not_allowed Reseller key whose member group does not have Can Create Subresellers. Ask the panel administrator to enable it.
403 sub_reseller_debit_disabled Reseller key, negative delta, and the panel does not allow resellers to take credits back. Ask the panel administrator to enable it, or transfer a positive amount only.
403 debit_direct_only Reseller key, negative delta, on a sub-reseller that is not a direct child of the caller. Take the credits back from the direct parent of that account.
404 not_found No reseller exists with that ID or, on a reseller key, the ID is outside the caller's sub-resellers. Verify the ID with GET /resellers/{id} first.
409 duplicate_transfer Reseller key: the same transfer (same sub-reseller, same amount) was processed less than 5 seconds ago with a different Idempotency-Key. Nothing was changed. If it was a double click, do nothing. If you really want a second transfer, wait 5 seconds. Retries of the same request should reuse the same Idempotency-Key.
409 idempotency_conflict The Idempotency-Key was reused with a different body. Generate a new key or resend the original body.
422 negative_balance_not_allowed A negative delta in credits mode is larger than the current balance. The balance is untouched; no partial deduction happens. Lower the delta or top up first.
422 cap_below_active_users A negative delta in users mode would drop max_users below the currently used slot count. No lines are auto-disabled to make room. Free the slots first (disable or delete lines), then retry.
422 negative_cap_not_allowed A negative delta in users mode would take max_users below zero. Lower the delta.
422 mismatched_mode Admin key: the reseller is stored in a billing mode this endpoint does not recognize (should not happen on a healthy panel). Reseller key: the caller or the target is in users mode, where there is nothing to transfer. Admin: contact support. Reseller: manage the sub-reseller's slots from the panel.
422 validation_error Reseller key: delta is not a number, is zero, or is not finite. Send a non-zero number.
500 transfer_failed Reseller key: the database refused the transfer. Nothing was changed. Retry with the same Idempotency-Key after a few seconds.
429 rate_limited The per-key request budget for this minute is spent. Back off and retry after the minute rolls over.

See also