Adjust reseller credits, action=adjust_credits

If you are starting a new integration instead of migrating, prefer the native v1 API with the official SDKs.

Apply a signed delta to a reseller's balance in the classic Xtream Codes envelope. The compat layer translates this call to the native POST /panel-api/v1/resellers/{id}/billing/adjust and wraps the result in the XC {"status": "STATUS_SUCCESS", "data": {...}} shape.

The endpoint targets the right column automatically: for a reseller in credits mode it moves credits, for a reseller in users mode it moves max_users. You never pass the mode explicitly. The update runs as a single atomic relative SQL statement, so two concurrent adjustments sum instead of racing, and the balance read used to compose the response is the row value, not any cached copy. 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.

Only admin API keys can call this action.

Endpoint

POST https://<your-panel-domain>/panel-api/xc/{accesscode}/admin/index.php?action=adjust_credits

The {accesscode} segment is decorative. Any non-empty value works. Security is enforced through the API key, not the path.

Authentication

Send the API key either as api_key=<your-api-key> in the query string, as Authorization: Bearer <your-api-key> in the header, or as a form field in the POST body. Admin key only. See Xtream Codes compatibility for the full auth contract.

Required scope

resellers:write. Reseller keys never carry this scope, so a reseller-issued key always sees STATUS_NO_PERMISSIONS with insufficient_scope.

Idempotency

Every write action in the XC dialect accepts an optional rid parameter (in the query string or the POST body). Repeating the exact same request with the same rid and the same body replays the original response instead of running twice. Reusing the same rid with a different body returns STATUS_FAILURE with error: "idempotency_conflict". For a monthly billing job, use the invoice ID as the rid so that a retry after a network timeout never stacks a second charge on top. See Idempotency via rid for the full contract.

Request body

XC integrations POST form-encoded (application/x-www-form-urlencoded).

Field Type Required Default Description
id int yes Numeric reg-user ID of the reseller.
credits float yes Signed change. Positive to top up, negative to claw back. In users mode the value is cast to int on the server. Translated to the native delta field.
note string no "" Free-form label written to the audit log. Truncated to 100 characters. Translated to the native reason field.
rid string no Idempotency token (see above). Strongly recommended for automated billing.

credits is parsed strictly. PHP's default (float) "1,000" is 1.0, so a top-up of 1000 credits used to be silently accredited as 1. The compat layer now rejects any value that fails is_numeric(), including thousands separators ("1,000"), unit suffixes ("12abc"), and non-numeric text, with STATUS_INVALID_DATA. Valid: "1000", "10.5", "-7", 1000, 10.5. Invalid: "1,000", "10,50", "abc".

Response

The data payload is the billing snapshot after the adjustment, in the same shape as action=get_user's billing object.

For a credits-mode reseller:

{
  "status": "STATUS_SUCCESS",
  "data": {
    "mode": "credits",
    "credits": 150,
    "max_users": null,
    "active_users": null,
    "billing_expires": null
  }
}

For a users-mode reseller:

{
  "status": "STATUS_SUCCESS",
  "data": {
    "mode": "users",
    "credits": null,
    "max_users": 260,
    "active_users": 231,
    "billing_expires": 1793520000
  }
}

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.

HTTP status is always 200. Your client must branch on body.status, not on the HTTP code.

Examples

cURL

# Top up 50 credits
curl -X POST "https://<your-panel-domain>/panel-api/xc/panel_api/admin/index.php?api_key=<your-api-key>&action=adjust_credits" \
  -d "id=100262261" \
  -d "credits=50" \
  -d "note=Manual top-up 2026-01-15" \
  -d "rid=topup-262261-2026-01-15"

# Claw back 10 credits
curl -X POST "https://<your-panel-domain>/panel-api/xc/panel_api/admin/index.php?api_key=<your-api-key>&action=adjust_credits" \
  -d "id=100262261" \
  -d "credits=-10" \
  -d "note=Refund reversal INV-00821" \
  -d "rid=refund-INV-00821"

PHP raw

$url = 'https://<your-panel-domain>/panel-api/xc/panel_api/admin/index.php'
     . '?' . http_build_query([
         'api_key' => '<your-api-key>',
         'action'  => 'adjust_credits',
       ]);
$ch = curl_init($url);
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_POST => true,
    CURLOPT_POSTFIELDS => http_build_query([
        'id'      => 100262261,
        'credits' => 50,
        'note'    => 'Manual top-up 2026-01-15',
        'rid'     => 'topup-262261-2026-01-15',
    ]),
]);
$body = json_decode(curl_exec($ch), true);
curl_close($ch);
if ($body['status'] !== 'STATUS_SUCCESS') {
    throw new RuntimeException($body['data']['message'] ?? 'adjust_credits failed');
}
$newBalance = $body['data']['credits'];

Python raw

import requests

r = requests.post(
    "https://<your-panel-domain>/panel-api/xc/panel_api/admin/index.php",
    params={"api_key": "<your-api-key>", "action": "adjust_credits"},
    data={
        "id":      100262261,
        "credits": 50,
        "note":    "Manual top-up 2026-01-15",
        "rid":     "topup-262261-2026-01-15",
    },
    timeout=30,
)
r.raise_for_status()
body = r.json()
if body["status"] != "STATUS_SUCCESS":
    raise RuntimeError(body["data"].get("message", "adjust_credits failed"))
new_balance = body["data"]["credits"]

Errors

HTTP is always 200 for this dialect. The status field is the branch signal, and data.error carries the exact slug.

status Error slug When it happens How to fix
STATUS_INVALID_DATA validation_error id or credits is missing, or credits fails is_numeric() (thousands separators, unit suffixes, non-numeric text). The message carries the reason. Send id and a strictly numeric credits value. Strip thousands separators before sending.
STATUS_INVALID_DATA invalid_body The native handler received a body without the translated delta field (should not happen when the request went through this action). Contact support.
STATUS_FAILURE invalid_key The api_key (or Authorization: Bearer) is missing, malformed, or unknown. Send a live admin key.
STATUS_NO_PERMISSIONS insufficient_scope The key does not carry resellers:write. Reseller-issued keys always land here. Use an admin key.
STATUS_NO_PERMISSIONS admin_only_endpoint A key that carried the scope but is still tagged as a reseller reached the handler. Use an admin key.
STATUS_FAILURE not_found No reseller exists with that ID. Verify the ID with action=get_user first.
STATUS_FAILURE idempotency_conflict The rid was reused with a different body. Generate a new rid or resend the original body.
STATUS_FAILURE negative_balance_not_allowed A negative credits value in credits mode is larger than the current balance. The balance is untouched; no partial deduction happens. XC has no dedicated status for this rejection, so it surfaces as STATUS_FAILURE. Lower the delta or top up first.
STATUS_FAILURE cap_below_active_users A negative credits value 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.
STATUS_FAILURE negative_cap_not_allowed A negative credits value in users mode would take max_users below zero. Lower the delta.
STATUS_FAILURE mismatched_mode The reseller is stored in a billing mode this endpoint does not recognize (should not happen on a healthy panel). Contact support.
STATUS_FAILURE rate_limited The per-key request budget for the current minute is spent. Back off and retry after the minute rolls over.

See also