Base URLs
https://admin.vsl247.com
https://admin2.vsl247.com
Authorization
Protected endpoints authenticate a portal account before checking its role.
- Recommended: create an access token at
POST /api/admin/token, then sendAuthorization: Bearer ACCESS_TOKEN. - Compatibility:
/api/v1routes also accept HTTP Basic authentication with the portal username and password. - The API token is valid for up to 30 days. Generate a new token after it expires.
- The browser login session and API authentication are separate; do not send the browser session cookie as an API credential.
Quick start
1. Create a token. 2. Copy access_token. 3. Send it in the Authorization header.
curl -X POST 'https://admin.vsl247.com/api/admin/token' \
-H 'Content-Type: application/x-www-form-urlencoded' \
-d 'username=admin_username&password=admin_password'
curl -H 'Authorization: Bearer ACCESS_TOKEN' \
https://admin.vsl247.com/api/users
Use a superadmin account for superadmin-only endpoints. A normal admin token is authenticated successfully but receives 403 Forbidden when its role is not allowed.
Authentication
Create a token and inspect the authenticated portal admin.
POST /api/admin/token Create a PasarGuard-style access token.
Authenticate with the portal username and password. The endpoint accepts either form data or a JSON object.
POST /api/admin/token
Content-Type: application/x-www-form-urlencoded
username=admin_username&password=admin_password
POST /api/admin/token
Content-Type: application/json
{
"username": "admin_username",
"password": "admin_password"
}
{
"access_token": "...",
"token_type": "bearer"
}
curl -H 'Authorization: Bearer ACCESS_TOKEN' \
'https://admin.vsl247.com/api/v1/me'
- Use the exact token returned in
access_token; do not include quotation marks around it. - Invalid credentials return
401 Unauthorized. - An inactive normal admin cannot receive an API token and returns
403 Forbidden.
GET /api/admin Return the authenticated admin.
{
"username": "admin_username",
"is_sudo": false,
"telegram_id": null,
"discord_webhook": null,
"users_usage": 0
}
HTTP Error responses Common authentication, authorization, and validation responses.
400 Bad Request— the request is authenticated but a body, query parameter, or value is invalid.401 Unauthorized— credentials are missing, invalid, or the Bearer token has expired. The response includes aWWW-Authenticateheader.403 Forbidden— the credentials identify a valid account, but that account's role is not allowed to use the endpoint.- Errors use the JSON shape
{"ok": false, "error": "..."}on portal-native/api/v1routes.
Users
Manage only the clients owned by the authenticated portal admin.
GET /api/users List owned clients with filters and pagination.
GET /api/users?offset=0&limit=10&username=client&status=active
{
"users": [],
"total": 0
}
POST /api/user Create a user using the admin's remaining quota.
{
"username": "client_username",
"data_limit": 21474836480,
"duration_days": 30,
"status": "active",
"note": "optional",
"group_id": 1
}
{
"username": "client_username",
"status": "active",
"used_traffic": 0,
"data_limit": 21474836480,
"subscription_url": "https://sub.vsl247.com/sub/..."
}
group_id is optional and accepts exactly one PasarGuard group id. The legacy group, groups, and group_ids aliases remain accepted only when they resolve to one id; a list containing more than one id is rejected with 400 Bad Request.
Omit it to use the admin's superadmin-assigned default group (falling back to the panel's group1 convention, or its first group, if the admin has no default configured).
If it is supplied, the id must be in the admin's superadmin-assigned allowed-groups list, or the request fails with 400 Bad Request and an error naming the allowed groups; an admin with no allowed-groups list configured yet has any supplied value ignored and falls back to the default, same as omitting it.
Not enough remaining GB in group 'vip': 12.5 GB left, 50.0 GB requested.
GET /api/user/<username> Return one owned client.
{
"username": "client_username",
"status": "active",
"used_traffic": 1073741824,
"data_limit": 21474836480,
"expire": 1780000000,
"subscription_url": "https://sub.vsl247.com/sub/..."
}
PUT /api/user/<username> Increase quota or update expiry and status.
{
"data_limit": 32212254720,
"expire": 1780000000,
"status": "active"
}
{
"username": "client_username",
"status": "active",
"data_limit": 32212254720,
"expire": 1780000000
}
data_limit bills the difference to the single group the client already belongs to — the client's group is never changed by this endpoint. Existing clients were migrated to group 1. If that group's balance is too small the request fails with 400 Bad Request naming the group and its remaining GB.
DELETE /api/user/<username> Delete an owned client through the panel API.
{
"detail": "User successfully deleted"
}
POST /api/user/<username>/reset Reset the client's used traffic.
POST /api/user/client_username/reset
Authorization: Bearer ACCESS_TOKEN
{
"username": "client_username",
"status": "active",
"used_traffic": 0,
"data_limit": 21474836480
}
POST /api/user/<username>/revoke_sub Revoke and regenerate the subscription link.
POST /api/user/client_username/revoke_sub
Authorization: Bearer ACCESS_TOKEN
{
"username": "client_username",
"status": "active",
"subscription_url": "https://sub.vsl247.com/sub/NEW_TOKEN"
}
Activity
Inspect subscription fetches and traffic usage.
GET /api/user/<username>/sub_update Return subscription fetch history.
GET /api/user/client_username/sub_update?offset=0&limit=10
{
"updates": [
{
"created_at": "2026-05-29T10:25:55Z",
"user_agent": "v2rayNG/1.8.5"
}
]
}
GET /api/user/<username>/usage Return usage records for one owned client.
GET /api/user/client_username/usage?start=2026-06-01T00:00:00Z&end=2026-06-06T00:00:00Z
{
"username": "client_username",
"usages": [
{
"node_id": 6,
"used_traffic": 268435456,
"created_at": "2026-06-05T12:00:00Z"
}
]
}
GET /api/users/usage Return usage records for all owned clients.
GET /api/users/usage?start=2026-06-01T00:00:00Z&end=2026-06-06T00:00:00Z
{
"usages": [
{
"username": "client_username",
"node_id": 6,
"used_traffic": 268435456,
"created_at": "2026-06-05T12:00:00Z"
}
]
}
Superadmin
Reporting and admin-management endpoints restricted to authenticated superadmins.
GET /api/v1/superadmin/sales Return GB sales for a time range, broken down by admin and by PasarGuard group.
admins) and per PasarGuard group (groups). This is read-only reporting: it does not modify users, quotas, or audit records.
superadmin
A normal admin token receives HTTP 403.
Query parameters
| Name | Type | Required | Description | Example |
|---|---|---|---|---|
start |
string or integer |
Required | Beginning of the range, inclusive. Accepts an ISO 8601 date/time or a Unix timestamp in seconds or milliseconds. | 2026-08-01T00:00:00+03:30 |
end |
string or integer |
Required | End of the range, exclusive. It must be later than start. |
2026-08-02T00:00:00+03:30 |
group_id |
integer |
Optional | Restrict admins and totals to sales attributable to this PasarGuard group only. groups always lists every group regardless of this filter, so you can see the full breakdown either way. |
2 |
Asia/Tehran. For predictable integrations, send an explicit offset or use UTC with Z. The interval includes start and excludes end.
Request examples
curl --get 'https://admin.vsl247.com/api/v1/superadmin/sales' \
-H 'Authorization: Bearer SUPERADMIN_ACCESS_TOKEN' \
--data-urlencode 'start=2026-08-01T00:00:00+03:30' \
--data-urlencode 'end=2026-08-02T00:00:00+03:30'
--data-urlencode safely encodes the + in the timezone offset.
curl --get 'https://admin.vsl247.com/api/v1/superadmin/sales' \
--user 'superadmin_username:superadmin_password' \
--data-urlencode 'start=2026-08-01' \
--data-urlencode 'end=2026-08-02'
Basic authentication is supported for /api/v1 routes. The account must still be a superadmin.
curl --get 'https://admin.vsl247.com/api/v1/superadmin/sales' \
-H 'Authorization: Bearer SUPERADMIN_ACCESS_TOKEN' \
--data-urlencode 'start=2026-08-01T00:00:00+03:30' \
--data-urlencode 'end=2026-08-02T00:00:00+03:30' \
--data-urlencode 'group_id=2'
admins and totals now cover only sales attributed to group 2; groups is unchanged and still lists every group.
{
"ok": true,
"range": {
"start": "2026-07-31T20:30:00Z",
"end": "2026-08-01T20:30:00Z",
"start_timestamp": 1785529800,
"end_timestamp": 1785616200,
"end_exclusive": true,
"naive_input_timezone": "Asia/Tehran"
},
"filter": {
"group_id": null
},
"totals": {
"sales_gb": 125.5,
"new_client_gb": 100.0,
"added_gb": 25.5,
"new_clients": 8,
"add_operations": 3,
"unresolved_events": 0,
"charged_gb": 500.0
},
"admins": [
{
"admin_id": 42,
"username": "portal_admin",
"is_active": true,
"sales_gb": 55.5,
"new_client_gb": 40.0,
"added_gb": 15.5,
"new_clients": 3,
"add_operations": 2,
"unresolved_events": 0,
"charged_gb": 500.0
}
],
"groups": [
{
"group_id": 1,
"group_name": "group1",
"sales_gb": 90.0,
"new_client_gb": 75.0,
"added_gb": 15.0,
"new_clients": 6,
"add_operations": 2,
"unresolved_events": 0,
"charged_gb": 500.0,
"has_own_pool": true,
"quota_gb": 1000.0,
"allocated_gb": 90.0,
"remaining_gb": 910.0
},
{
"group_id": 2,
"group_name": "vip",
"sales_gb": 40.0,
"new_client_gb": 30.0,
"added_gb": 10.0,
"new_clients": 2,
"add_operations": 1,
"unresolved_events": 0,
"charged_gb": 0.0,
"has_own_pool": true,
"quota_gb": 200.0,
"allocated_gb": 40.0,
"remaining_gb": 160.0
},
{
"group_id": null,
"group_name": "No group recorded (client predates group tracking)",
"sales_gb": 0.0,
"new_client_gb": 0.0,
"added_gb": 0.0,
"new_clients": 0,
"add_operations": 0,
"unresolved_events": 0,
"charged_gb": 0.0,
"has_own_pool": false,
"quota_gb": null,
"allocated_gb": null,
"remaining_gb": null
}
]
}
Response fields
| Field | Type | Description |
|---|---|---|
range | object | Normalized UTC range and the Unix-second boundaries used by the report. |
range.start, range.end | string | Normalized UTC ISO 8601 values. start is inclusive; end is exclusive. |
range.start_timestamp, range.end_timestamp | integer | Unix timestamps in seconds. |
range.end_exclusive | boolean | Always true for this endpoint. |
range.naive_input_timezone | string | Timezone applied to ISO input without an explicit offset: Asia/Tehran. |
filter.group_id | integer or null | Echoes the group_id query parameter. null when it was not supplied, meaning admins/totals cover every group. |
totals | object | Combined totals across the returned admins rows (after any group_id filter is applied). |
totals.sales_gb | number | Gross sales in GB: new_client_gb + added_gb. |
totals.new_client_gb, totals.added_gb | number | GB assigned at client creation and GB added to existing clients. |
totals.new_clients, totals.add_operations | integer | Counts of client-creation and GB-addition sale events. |
totals.unresolved_events | integer | Legacy events whose amount could not be reconstructed. Non-zero means the report is incomplete for those events. |
totals.charged_gb | number | GB the superadmin handed out to admins in this window (the opposite direction of a sale). Not part of sales_gb. |
admins[] | array | One row per admin. With no group_id filter, every current admin appears, including zero-sale admins; historical deleted actors may appear as admin #ID. With group_id set, only admins with activity in that group appear. |
admins[].* | object | Each row contains admin_id, username, is_active, the GB totals, and the event counts. |
groups[] | array | One row per PasarGuard group that has ever had a sale or that any admin holds GB in, plus a group_id: null row for sales whose client has no group recorded. Always covers every group, independent of the group_id query filter. Sorted by sales_gb descending. |
groups[].group_id | integer or null | The PasarGuard group id, or null for sales on clients with no group recorded. |
groups[].group_name | string | The group's current name from the panel, best-effort (falls back to "group {id}" if the panel is unreachable when the report runs), or the fixed no-group label for group_id: null. |
groups[].charged_gb | number | GB the superadmin added to that group's balances in this window, across all admins. |
groups[].has_own_pool | boolean | true when at least one admin holds GB in this group. false means nobody can currently sell there. |
groups[].quota_gb, groups[].allocated_gb, groups[].remaining_gb | number or null | Current standing of that group's balances, summed over every admin holding one: total GB, GB already allocated to clients, and GB still sellable. All three are null when has_own_pool is false. These describe the balance right now — unlike the sales figures, they are not scoped to the requested time range. |
groups[].* | object | Same GB sales totals and event counts as admins[], scoped to that group. |
Sales calculation
sales_gb = new_client_gb + added_gb
The report uses client-creation and GB-addition audit events. Deleted users or later quota changes do not retroactively remove the original sale event.
group_id: null bucket remains only as a defensive fallback for malformed or externally imported records.
charged_gb tracks GB flowing into an admin's balance (the superadmin topping them up), while sales_gb tracks GB flowing out to clients. They are reported side by side but never summed together. Unlike sales, a charge is never split across groups: it always names the exact group whose balance it credited.
Validation errors
| Status | When it happens | Example body |
|---|---|---|
| 400 Bad Request | Missing or malformed time values, a non-positive range, or an out-of-range timestamp. | {"ok": false, "error": "end must be later than start"} |
| 401 Unauthorized | Credentials are missing, invalid, or an access token has expired. | {"detail": "Not authenticated"} |
| 403 Forbidden | The credentials are valid, but the account is not a superadmin. A normal admin token receives HTTP 403 here. | {"detail": "Insufficient role"} |
PATCH /api/v1/admins/{admin_id} Charge an admin, reduce quota, or update admin settings.
admin_id. Quota changes are write operations and are recorded in the superadmin audit log.
superadmin
Normal admin accounts receive 403.
{"ok": true}.
Path parameter
| Name | Type | Required | Description |
|---|---|---|---|
admin_id | integer | Required | ID of the portal admin to update. Get IDs from GET /api/v1/admins. |
Quota actions
| Field | Type | Required | Effect | Rules |
|---|---|---|---|---|
add_gb |
number |
Optional | Charges the admin by adding this much GB to one group's balance. | Must be greater than 0. It does not directly change any client allocation. Lands in the admin's primary group unless group_id names another. |
reduce_gb |
number |
Optional | Takes this much GB out of one group's balance. | Must be greater than 0. The balance cannot drop below the GB already allocated to clients in that group. Applies to the admin's primary group unless group_id names another. |
group_id |
integer |
Optional | Chooses which group's balance add_gb / reduce_gb moves. |
Charging a group the admin holds no GB in opens a balance there. Reducing a group they hold no GB in is rejected. Omitted, it means the admin's primary group — which is where a request that never mentions groups has always put the GB. |
group_quotas |
array |
Optional | Sets pool sizes outright (rather than adding to them). Each entry is {"group_id": 1, "quota_gb": 1000}. |
Sending "quota_gb": null removes that group's balance entirely, which is only allowed once no clients remain in it. A balance cannot be set below the GB already allocated in that group. |
80 GB is allocated in group1, that group's balance cannot be reduced below 80 GB, and the API rejects a reduction that would violate this.
1000 GB in group1 and 200 GB in group2 at the same time — and nothing is shared between them: spending group1 never touches group2, and a group the admin holds no balance in has no GB at all. Requests that never mention a group keep working exactly as before, because they act on the admin's primary group (group1 by default), which is where their GB already was.
Other supported updates
| Field | Type | Effect |
|---|---|---|
is_active | boolean | Activate or deactivate the admin account. |
min_client_gb | number | Set the minimum GB allowed when the admin creates a client; must be greater than 0. |
subscription_host | string | Set the subscription hostname, such as sub.example.com. |
hide_branding | boolean | Enable or disable branded subscription output. |
portal_password | string | Change the portal login password. |
marzban_username, marzban_password | string | Update the linked panel credentials; the credentials are validated before saving. |
Request examples
curl -X PATCH 'https://admin.vsl247.com/api/v1/admins/42' \
-H 'Authorization: Bearer SUPERADMIN_ACCESS_TOKEN' \
-H 'Content-Type: application/json' \
-d '{"add_gb": 25}'
curl -X PATCH 'https://admin.vsl247.com/api/v1/admins/42' \
-H 'Authorization: Bearer SUPERADMIN_ACCESS_TOKEN' \
-H 'Content-Type: application/json' \
-d '{"reduce_gb": 10}'
curl -X PATCH 'https://admin.vsl247.com/api/v1/admins/42' \
-H 'Authorization: Bearer SUPERADMIN_ACCESS_TOKEN' \
-H 'Content-Type: application/json' \
-d '{"add_gb": 100, "group_id": 2}'
Only group 2's balance grows by 100 GB; every other group is untouched.
curl -X PATCH 'https://admin.vsl247.com/api/v1/admins/42' \
-H 'Authorization: Bearer SUPERADMIN_ACCESS_TOKEN' \
-H 'Content-Type: application/json' \
-d '{"group_quotas": [
{"group_id": 1, "quota_gb": 1000},
{"group_id": 2, "quota_gb": 200},
{"group_id": 3, "quota_gb": null}
]}'
Group 1 is set to 1000 GB, group 2 to 200 GB, and group 3's balance is removed so the admin can no longer sell there.
{
"ok": true
}
Errors
| Status | When it happens | Example |
|---|---|---|
| 400 Bad Request | The body is not a JSON object, a GB value is missing/invalid/non-positive, the admin does not exist, or a reduction would go below the GB allocated in that group. | Cannot reduce below the 80.0 GB already allocated in that group |
| 400 Bad Request | reduce_gb named a group_id the admin holds no GB in, or a group_quotas entry would remove a balance that still has clients in it. | This admin has no GB pool for that group |
| 401 Unauthorized | Credentials are missing, invalid, or expired. | {"ok": false, "error": "Authentication required"} |
| 403 Forbidden | The credentials are valid, but the account is not a superadmin. | {"ok": false, "error": "Forbidden"} |
group_quotas batch, or change one admin setting. Mixing mutations is rejected before anything changes. Every group_quotas entry is validated first and the complete batch commits or rolls back together.
Portal-Native API
Legacy routes that use the portal's ok response wrapper.
GET /api/v1/me Return the current portal account and quota.
{
"ok": true,
"user": {
"username": "admin_username",
"role": "admin",
"quota_bytes": 107374182400,
"remaining_bytes": 85899345920,
"group_ids": [1, 2],
"default_group_id": 1,
"group_quotas": [
{
"group_id": 1,
"group_name": "group1",
"quota_gb": 1000.0,
"allocated_gb": 90.0,
"remaining_gb": 910.0,
"quota_bytes": 1073741824000,
"allocated_bytes": 96636764160,
"remaining_bytes": 977105059840
}
]
}
}
quota_bytes / remaining_bytes are the account-wide totals across every group, kept for compatibility — they are a sum, not a balance anything is spent from. group_quotas is where the real balances are: one entry per group the admin holds GB in. A group missing from that list has no GB, so the admin cannot create clients in it. For an admin who only ever sells in one group, the single entry there equals the account totals.
GET /api/v1/clients Return the portal-native client list.
GET /api/v1/clients?page=1&per_page=10&search=client&live=1
{
"ok": true,
"page": 1,
"per_page": 10,
"total": 1,
"has_next": false,
"clients": []
}
POST /api/v1/clients Create a client using the portal-native response wrapper.
{
"username": "client_username",
"data_limit_gb": 20,
"duration_days": 30,
"group_id": 1
}
{
"ok": true,
"client": {
"username": "client_username",
"status": "active",
"data_limit": 21474836480,
"subscription_url": "https://sub.vsl247.com/sub/..."
}
}
group_id is optional and accepts exactly one PasarGuard group id. The legacy group, groups, and group_ids aliases remain accepted only when they resolve to one id; multiple ids are rejected with 400 Bad Request.
Omit it to use the admin's superadmin-assigned default group (falling back to the panel's group1 convention, or its first group, if the admin has no default configured).
If it is supplied, the id must be in the admin's superadmin-assigned allowed-groups list, or the request fails with 400 Bad Request and an error naming the allowed groups; an admin with no allowed-groups list configured yet has any supplied value ignored and falls back to the default, same as omitting it.
Not enough remaining GB in group 'vip': 12.5 GB left, 50.0 GB requested.
POST /api/v1/admins Create a portal admin and link a non-sudo panel admin.
{
"username": "portal_admin",
"portal_password": "portal-password",
"quota_gb": 100,
"min_client_gb": 0.5,
"subscription_host": "sub.example.com",
"marzban_admin_mode": "existing",
"marzban_username": "panel_admin",
"marzban_password": "panel-password",
"group_ids": [1, 2],
"default_group_id": 1,
"group_quotas": [
{"group_id": 1, "quota_gb": 1000},
{"group_id": 2, "quota_gb": 200}
]
}
{
"ok": true,
"admin_id": 42
}
group_ids (optional; accepts a single id or a list, alias groups) is the set of PasarGuard groups this admin is allowed to create clients in. Omit it, or leave it empty, to leave the admin unrestricted-by-list: their client-creation requests can't pick a group and always fall back to the default described below.
default_group_id (optional) is the group used when the admin's own client-creation request doesn't specify one; it must be one of group_ids if that list is non-empty, or the request fails with 400 Bad Request. Leave it unset to fall back to the panel's group1 convention (or its first group).
Both can be changed later from the superadmin web UI's per-admin "Allowed groups" panel; there is currently no API route to update an existing admin's group assignment.
quota_gb opens the admin's balance in their primary group — default_group_id if given, otherwise group1. group_quotas (optional) funds each group it lists; an entry for the primary group replaces quota_gb rather than adding to it. With the body above the admin can sell 1000 GB into group 1 and 200 GB into group 2 independently, with nothing shared between them. Each entry needs both group_id and quota_gb.
A group left out has no GB, so the admin cannot create clients in it. Omitting the field entirely produces an admin who sells only in their primary group, which is exactly what this endpoint has always created. Balances can be added, resized, or removed afterwards with PATCH /api/v1/admins/{admin_id}.
No matching endpoints.
Compatibility Notes
- PasarGuard-compatible routes manage only clients owned by the authenticated portal admin.
- Client creation and data-limit increases follow portal quota rules.
- An admin's GB is held separately per PasarGuard group and is never shared between them. Every client belongs to exactly one group, and creating or topping it up bills only that group's balance. Existing clients are pinned to group 1; changing the admin default affects only new clients.
- Deleting a user never returns quota to the admin.
- PasarGuard-compatible responses do not use the portal-native
okwrapper. - Unsupported panel administration and system routes are not proxied.