-
Notifications
You must be signed in to change notification settings - Fork 1
Modern API
This guide covers the 48 new API routes added in v3.1.0 for 3x-ui v2.x/v3.x panels. These routes use the /panel/api/clients/, /panel/api/nodes/, and /panel/api/custom-geo/ endpoints.
Version requirement: These routes require 3x-ui v2.x or later. For older panels, use the legacy API methods under
/panel/api/inbounds/.
const ThreeXUI = require('3xui-api-client');
// With API token (3x-ui v3.0.2+)
const client = new ThreeXUI('https://your-panel.com:2053', {
token: 'your-api-token'
});
// With username/password (all versions)
const client = new ThreeXUI('https://your-panel.com:2053', 'admin', 'password');
await client.login();The library features automatic Dual Panel Support to detect which generation of panel you're talking to:
-
Modern (React-based) panels: The client tries
/panel/api/loginfirst. It automatically handles the/panel/api/csrf-tokenCSRF protection and uses the modern endpoints. -
Legacy (Vue-based) panels: If the modern login endpoint returns a 404, it seamlessly falls back to
/login(pure cookie-based auth without CSRF).
The detected panelType is cached in the session so subsequent logins and API calls are instantly routed to the correct endpoints without re-detection. You can also explicitly specify { panelVersion: 'modern' | 'legacy' | 'auto' } in the constructor options.
const result = await client.getClients();
// GET /panel/api/clients/list
// Returns: { success: true, obj: [ ...clients ] }const result = await client.getPagedClients({
page: 1, // page number (default: 1)
size: 20, // items per page (default: 10)
sort: 'email', // sort field: 'email' | 'expireTime' | 'totalGB'
order: 'asc', // 'asc' | 'desc'
email: 'alice' // filter by email (partial match)
});
// GET /panel/api/clients/list/paged?page=1&size=20&sort=email&order=asc
// Returns: { success: true, obj: {
// items: [ { email, subId, enable, totalGB, expiryTime, limitIp, reset, inboundIds, traffic: {...}, createdAt, updatedAt }, ... ],
// total: 1, filtered: 1, page: 1, pageSize: 25, summary: { total: 1, ... }
// } }const result = await client.getClient('alice_vpn');
// GET /panel/api/clients/get/alice_vpn
// Returns: { success: true, obj: {
// client: { id, email, subId, uuid, password, flow, limitIp, totalGB, expiryTime, enable, group, ... },
// inboundIds: [1]
// } }const result = await client.getClientTraffic('alice_vpn');
// GET /panel/api/clients/traffic/alice_vpn
// Returns: { success: true, obj: { up, down, total, expiryTime, ... } }const result = await client.getSubLinks('550e8400-e29b-41d4-a716-446655440000');
// GET /panel/api/clients/subLinks/:subId
// Returns subscription URI strings for supported clientsconst result = await client.getClientLinks('alice_vpn');
// GET /panel/api/clients/links/alice_vpn
// Returns protocol-specific connection strings (vless://, vmess://, etc.)Important
Data Limits (totalGB): Pass data limits in GIGABYTES. The library automatically converts to bytes internally. For example, totalGB: 50 sets a 50 GB limit (converted to 53687091200 bytes internally). This automatic conversion prevents the silent quota error that existed in earlier versions.
const result = await client.addModernClient({
inboundIds: [1, 3], // which inbounds to attach to
client: {
email: 'alice_vpn',
id: '550e8400-e29b-41d4-a716-446655440000', // UUID for VLESS/VMess
totalGB: 50, // data limit in GB (auto-converted to bytes, 0 = unlimited)
expiryTime: 1751500800000, // Unix ms timestamp (0 = never)
enable: true,
flow: 'xtls-rprx-vision',
limitIp: 2
}
});
// POST /panel/api/clients/add
// Returns: { success: true, msg: "Inbound client(s) have been added.", obj: null }const result = await client.updateModernClient('alice_vpn', {
totalGB: 100, // Update to 100 GB (auto-converted to bytes)
expiryTime: 0, // 0 = never expire
enable: true,
limitIp: 3
});
// POST /panel/api/clients/update/alice_vpnconst result = await client.deleteModernClient('alice_vpn');
// POST /panel/api/clients/del/alice_vpnconst result = await client.attachClientToInbounds('alice_vpn', {
inboundIds: [1, 2, 4]
});
// POST /panel/api/clients/alice_vpn/attachconst result = await client.detachClientFromInbounds('alice_vpn', {
inboundIds: [4]
});
// POST /panel/api/clients/alice_vpn/detachconst result = await client.getModernClientIps('alice_vpn');
// POST /panel/api/clients/ips/alice_vpn
// Returns: { success: true, obj: [...ipAddresses] } when IPs are recorded,
// or { success: true, obj: "No IP Record" } (string, not array) when none existconst result = await client.clearModernClientIps('alice_vpn');
// POST /panel/api/clients/clearIps/alice_vpnconst result = await client.resetModernClientTrafficByEmail('alice_vpn');
// POST /panel/api/clients/resetTraffic/alice_vpnconst result = await client.updateModernClientTrafficByEmail('alice_vpn', {
up: 0,
down: 0
});
// POST /panel/api/clients/updateTraffic/alice_vpnconst result = await client.resetAllModernClientTraffics();
// POST /panel/api/clients/resetAllTrafficsconst result = await client.deleteDepletedModernClients();
// POST /panel/api/clients/delDepleted
// Returns: { success: true, obj: { deleted: 0 } }const result = await client.getOnlines();
// POST /panel/api/clients/onlines
// Returns: { success: true, obj: [...emails] } - list of emails currently connectedconst result = await client.getModernLastOnline();
// POST /panel/api/clients/lastOnline
// Returns map of { email: lastSeenTimestamp }// NOTE: the request body is a raw array, not wrapped in a `clients` object
const result = await client.bulkCreateModernClients([
{ inboundIds: [1], client: { email: 'user_001', id: '...uuid...', totalGB: 0, expiryTime: 0, enable: true } },
{ inboundIds: [1], client: { email: 'user_002', id: '...uuid...', totalGB: 0, expiryTime: 0, enable: true } }
]);
// POST /panel/api/clients/bulkCreate
// Returns: { success: true, obj: { created: 2 } }
// or, if some emails already exist: { success: true, obj: { created: 1, skipped: [{ email, reason }] } }const result = await client.bulkAdjustModernClients({
emails: ['user_001', 'user_002'],
addDays: 30, // shifts expiryTime by this many days (can be negative)
addBytes: 10737418240 // shifts totalGB by this many bytes (can be negative)
});
// POST /panel/api/clients/bulkAdjust
// Returns: { success: true, obj: { adjusted: 2 } }
// Clients with unlimited expiry (expiryTime: 0) are skipped for addDays,
// and clients with unlimited traffic (totalGB: 0) are skipped for addBytes:
// { success: true, obj: { adjusted: 0, skipped: [{ email, reason: "unlimited expiry" }, ...] } }const result = await client.bulkDeleteModernClients({
emails: ['user_001', 'user_002']
});
// POST /panel/api/clients/bulkDel
// Returns: { success: true, obj: { deleted: 2 } }
// or: { success: true, obj: { deleted: 0, skipped: [{ email, reason: "client not found" }, ...] } }const result = await client.bulkAttachModernClients({
emails: ['user_001', 'user_002'],
inboundIds: [1, 2]
});
// POST /panel/api/clients/bulkAttach
// Returns: { success: true, obj: { attached: [...]|null, skipped: [...]|null, errors: ["user_001: record not found", ...] } }const result = await client.bulkDetachModernClients({
emails: ['user_001', 'user_002'],
inboundIds: [2]
});
// POST /panel/api/clients/bulkDetach
// Returns: { success: true, obj: { detached: [...]|null, skipped: [...]|null, errors: [...] } }const result = await client.bulkResetTrafficModernClients({
emails: ['user_001', 'user_002']
});
// POST /panel/api/clients/bulkResetTraffic
// Returns: { success: true, obj: { affected: 2 } }Groups let you tag clients with labels and apply bulk operations to all members of a group.
const result = await client.getGroups();
// GET /panel/api/clients/groups
// Returns: { success: true, obj: [ { name: 'premium', clientCount: 3, trafficUsed: 1073741824 }, ... ] }const result = await client.getGroupEmails('premium');
// GET /panel/api/clients/groups/premium/emails
// Returns: { success: true, obj: ['alice_vpn', 'bob_vpn', ...] }const result = await client.createGroup({ name: 'premium' });
// POST /panel/api/clients/groups/createconst result = await client.renameGroup({
oldName: 'premium',
newName: 'vip'
});
// POST /panel/api/clients/groups/renameconst result = await client.deleteGroup({ name: 'trial' });
// POST /panel/api/clients/groups/delete// NOTE: `group` is a single group name (string), not a `groups` array
const result = await client.bulkAddGroups({
emails: ['alice_vpn', 'bob_vpn'],
group: 'premium'
});
// POST /panel/api/clients/groups/bulkAdd
// Returns: { success: true, obj: { affected: 2 } }// NOTE: only `emails` is accepted - this removes each client from
// whatever group it currently belongs to. There is no `group`/`groups` field.
const result = await client.bulkRemoveGroups({
emails: ['alice_vpn']
});
// POST /panel/api/clients/groups/bulkRemove
// Returns: { success: true, obj: { affected: 1 } }Nodes represent additional servers in a multi-server 3x-ui cluster. Each node is itself a 3x-ui panel (or another panel's API), reachable over HTTP(S) and authenticated with an API token.
const result = await client.getNodes();
// GET /panel/api/nodes/list
// Returns: { success: true, obj: [ ...nodes ] } - see "Node object shape" belowconst result = await client.getNode(2);
// GET /panel/api/nodes/get/2
// Returns: { success: true, obj: { ...node } } - see "Node object shape" belowconst result = await client.getNodeHistory(2, 'cpu', 60);
// GET /panel/api/nodes/history/2/cpu/60
// metric: 'cpu' | 'memory' | ... (whatever metrics the node reports)
// bucket: number of data points/seconds to return
// Returns: { success: true, obj: [ { t: 1781106540, v: 2.1624436777220635 }, ... ] }
// t = unix timestamp (seconds), v = metric value at that timeconst result = await client.addNode({
name: 'SG Node 1',
remark: 'Singapore secondary panel',
scheme: 'https', // 'http' | 'https'
address: 'sg-node.example.com',
port: 2053,
basePath: '/AbCdEfGhIj/', // the node panel's web base path, must end with '/'
apiToken: 'the-remote-nodes-api-token', // required - the API token configured on the target node's panel
enable: true,
allowPrivateAddress: false, // set true to allow private/loopback addresses (e.g. self-registration)
tlsVerifyMode: 'verify', // optional, defaults to 'verify'
pinnedCertSha256: '' // optional, used when pinning a self-signed cert
});
// POST /panel/api/nodes/add
// Returns: { success: true, msg: "Add node", obj: { ...node } }
// Unless `allowPrivateAddress: true` is set, `address` must be a public/non-private
// address reachable from the panel, otherwise the panel rejects it with
// "blocked private/internal address".const result = await client.updateNode(2, {
name: 'SG Node 1 (updated)',
remark: 'Singapore secondary panel',
scheme: 'https',
address: 'sg-node.example.com',
port: 2053,
basePath: '/AbCdEfGhIj/',
apiToken: 'the-remote-nodes-api-token',
enable: true,
allowPrivateAddress: false
});
// POST /panel/api/nodes/update/2
// Returns: { success: true, msg: "Update node", obj: null }const result = await client.deleteNode(2);
// POST /panel/api/nodes/del/2
// Returns: { success: true, msg: "Delete node", obj: null }const result = await client.setNodeEnable(2, false); // explicitly disable
const result2 = await client.setNodeEnable(2, true); // explicitly enable
const result3 = await client.setNodeEnable(2); // toggles the current state
// POST /panel/api/nodes/setEnable/2
// Body (when `enable` is a boolean): { enable: true|false }
// Returns: { success: true, msg: "Update node", obj: null }const result = await client.testNode({
scheme: 'https',
address: 'sg-node.example.com',
port: 2053,
basePath: '/AbCdEfGhIj/',
apiToken: 'the-remote-nodes-api-token'
});
// POST /panel/api/nodes/test
// Returns: { success: true, obj: {
// status: 'online' | 'offline', latencyMs, xrayVersion, panelVersion,
// cpuPct, memPct, uptimeSecs, error, xrayState, xrayError
// } }const result = await client.probeNode(2);
// POST /panel/api/nodes/probe/2
// Returns the same shape as testNode():
// { success: true, obj: { status, latencyMs, xrayVersion, panelVersion, cpuPct, memPct, uptimeSecs, error, xrayState, xrayError } }getNodes(), getNode(id), and addNode() all return node objects with this shape:
{
id: 3,
name: 'SG Node 1',
remark: 'Singapore secondary panel',
scheme: 'https',
address: 'sg-node.example.com',
port: 2053,
basePath: '/AbCdEfGhIj/',
apiToken: 'the-remote-nodes-api-token',
enable: true,
allowPrivateAddress: false,
tlsVerifyMode: 'verify',
pinnedCertSha256: '',
guid: '',
status: 'unknown', // 'unknown' | 'online' | 'offline' (updated by probe/heartbeat)
lastHeartbeat: 0,
latencyMs: 0,
xrayVersion: '',
panelVersion: '',
cpuPct: 0,
memPct: 0,
uptimeSecs: 0,
lastError: '',
xrayState: '',
xrayError: '',
configDirty: false,
configDirtyAt: 0,
inboundCount: 0,
clientCount: 0,
onlineCount: 0,
depletedCount: 0,
createdAt: 1781106907531,
updatedAt: 1781106907531
}These routes ask the panel itself to generate keys/certs (e.g. for Reality, ECH, post-quantum key exchange) so the client never needs a local crypto implementation.
const result = await client.getNewUUID();
// GET /panel/api/server/getNewUUID
// Returns: { success: true, obj: { uuid: '...' } }const result = await client.getNewX25519Cert();
// GET /panel/api/server/getNewX25519Cert
// Returns: { success: true, obj: { privateKey: '...', publicKey: '...' } }const result = await client.getNewmldsa65();
// GET /panel/api/server/getNewmldsa65
// Returns: { success: true, obj: { seed: '...', verify: '...' } }const result = await client.getNewmlkem768();
// GET /panel/api/server/getNewmlkem768
// Returns: { success: true, obj: { seed: '...', client: '...' } }const result = await client.getNewVlessEnc();
// GET /panel/api/server/getNewVlessEnc
// Returns: { success: true, obj: { auths: [ { id, label, decryption, encryption }, ... ] } }const result = await client.getNewEchCert(); // without SNI
const result2 = await client.getNewEchCert('example.com'); // with SNI
// POST /panel/api/server/getNewEchCert
// `sni` (when provided) is sent as application/x-www-form-urlencoded,
// matching how the panel reads it via c.PostForm.
// Returns: { success: true, obj: { echConfigList: '...', echServerKeys: '...' } }const result = await client.getWebCertFiles();
// GET /panel/api/server/getWebCertFiles
// Returns: { success: true, obj: { webCertFile: '/path/to/fullchain.pem', webKeyFile: '/path/to/privkey.pem' } }Custom Geo lets you define and manage custom geographic IP/domain lists used in Xray routing rules.
const result = await client.getCustomGeos();
// GET /panel/api/custom-geo/list
// Returns: { success: true, obj: [ { id, type, alias, url, localPath, lastUpdatedAt, lastModified, createdAt, updatedAt }, ... ] }const result = await client.getGeoAliases();
// GET /panel/api/custom-geo/aliases
// Returns: { success: true, obj: { geosite: [...] | null, geoip: [...] | null } }
// alias mappings for built-in geo files// NOTE: only `type`, `alias`, and `url` are accepted - there is no `name`/`data` field
const result = await client.addCustomGeo({
type: 'geosite', // 'geosite' | 'geoip' (NOT 'domain'/'ip')
alias: 'my-blocked-sites-alias',
url: 'https://example.com/path/to/geosite.dat' // remote file the panel downloads
});
// POST /panel/api/custom-geo/add
// The panel downloads `url` and validates it as a geosite/geoip data file -
// a URL that doesn't resolve to one will fail with "Add custom geo (Download failed)".
// Returns: { success: true, msg: "Add custom geo", obj: null }
// To get the new entry's id, call getCustomGeos() afterwards and match by alias.const result = await client.updateCustomGeo(3, {
type: 'geosite',
alias: 'my-blocked-sites-alias',
url: 'https://example.com/path/to/geosite.dat'
});
// POST /panel/api/custom-geo/update/3
// Returns: { success: true, msg: "...", obj: null }const result = await client.deleteCustomGeo(3);
// POST /panel/api/custom-geo/delete/3const result = await client.downloadCustomGeo(3);
// POST /panel/api/custom-geo/download/3
// Triggers download/export of the geo fileconst result = await client.updateAllCustomGeo();
// POST /panel/api/custom-geo/update-all
// Fetches latest versions of all subscribed geo lists
// Returns: { success: true, obj: { succeeded: [...]|null, failed: [...]|null } }| Previous | Next |
|---|---|
| ← Authentication Guide | Inbound Management → |
Last updated: June 2026