---
title: "Adjust reseller credits, action=adjust_credits"
description: "Apply a signed delta to a reseller's credits (credits mode) or slot cap (users mode) in the XC dialect. Admin-only. Maps to POST /panel-api/v1/resellers/{id}/billing/adjust."
---

# Adjust reseller credits, action=adjust_credits

> [!NOTE]
> If you are starting a new integration instead of migrating, prefer the [native v1 API](/docs/?page=xai-ref-resellers-create) with the [official SDKs](/docs/?page=panel-api-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`](/docs/?page=xai-ref-resellers-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](/docs/?page=panel-api-xtream-codes-compatibility#base-url-and-authentication) 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](/docs/?page=panel-api-xtream-codes-compatibility#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. |

> [!WARNING]
> `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`](/docs/?page=xc-ref-users-get)'s `billing` object.

For a `credits`-mode reseller:

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

For a `users`-mode reseller:

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

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

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

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

- [Get one sub-reseller, action=get_user](/docs/?page=xc-ref-users-get)
- [Update a sub-reseller, action=edit_user](/docs/?page=xc-ref-users-edit)
- [Xtream Codes compatibility overview](/docs/?page=panel-api-xtream-codes-compatibility)
- [Native v1: adjust reseller billing](/docs/?page=xai-ref-resellers-billing-adjust)
- [Rate limits and idempotency](/docs/?page=panel-api-rate-limits-idempotency)
