Skip to content

Modern API

Helitha Guruge edited this page Jun 15, 2026 · 2 revisions

Modern API Guide

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/.

Table of Contents


Setup

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();

Login compatibility (old vs. new panels)

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/login first. It automatically handles the /panel/api/csrf-token CSRF 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.


Clients

Get all clients

const result = await client.getClients();
// GET /panel/api/clients/list
// Returns: { success: true, obj: [ ...clients ] }

Get paginated 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, ... }
// } }

Get client by email

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]
// } }

Get client traffic by email

const result = await client.getClientTraffic('alice_vpn');
// GET /panel/api/clients/traffic/alice_vpn
// Returns: { success: true, obj: { up, down, total, expiryTime, ... } }

Get subscription links by subscription ID

const result = await client.getSubLinks('550e8400-e29b-41d4-a716-446655440000');
// GET /panel/api/clients/subLinks/:subId
// Returns subscription URI strings for supported clients

Get connection links by email

const 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.

Add a client

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 }

Update a client by email

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_vpn

Delete a client by email

const result = await client.deleteModernClient('alice_vpn');
// POST /panel/api/clients/del/alice_vpn

Attach client to inbounds

const result = await client.attachClientToInbounds('alice_vpn', {
    inboundIds: [1, 2, 4]
});
// POST /panel/api/clients/alice_vpn/attach

Detach client from inbounds

const result = await client.detachClientFromInbounds('alice_vpn', {
    inboundIds: [4]
});
// POST /panel/api/clients/alice_vpn/detach

Get client IPs

const 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 exist

Clear client IPs

const result = await client.clearModernClientIps('alice_vpn');
// POST /panel/api/clients/clearIps/alice_vpn

Reset traffic for a client

const result = await client.resetModernClientTrafficByEmail('alice_vpn');
// POST /panel/api/clients/resetTraffic/alice_vpn

Update client traffic counters

const result = await client.updateModernClientTrafficByEmail('alice_vpn', {
    up: 0,
    down: 0
});
// POST /panel/api/clients/updateTraffic/alice_vpn

Reset traffic for all clients

const result = await client.resetAllModernClientTraffics();
// POST /panel/api/clients/resetAllTraffics

Delete depleted (expired/over-limit) clients

const result = await client.deleteDepletedModernClients();
// POST /panel/api/clients/delDepleted
// Returns: { success: true, obj: { deleted: 0 } }

Get currently online clients

const result = await client.getOnlines();
// POST /panel/api/clients/onlines
// Returns: { success: true, obj: [...emails] } - list of emails currently connected

Get last online time for all clients

const result = await client.getModernLastOnline();
// POST /panel/api/clients/lastOnline
// Returns map of { email: lastSeenTimestamp }

Bulk create clients

// 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 }] } }

Bulk adjust clients (traffic/expiry deltas)

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" }, ...] } }

Bulk delete clients

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" }, ...] } }

Bulk attach clients to inbounds

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", ...] } }

Bulk detach clients from inbounds

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: [...] } }

Bulk reset traffic

const result = await client.bulkResetTrafficModernClients({
    emails: ['user_001', 'user_002']
});
// POST /panel/api/clients/bulkResetTraffic
// Returns: { success: true, obj: { affected: 2 } }

Client Groups

Groups let you tag clients with labels and apply bulk operations to all members of a group.

Get all groups

const result = await client.getGroups();
// GET /panel/api/clients/groups
// Returns: { success: true, obj: [ { name: 'premium', clientCount: 3, trafficUsed: 1073741824 }, ... ] }

Get emails in a group

const result = await client.getGroupEmails('premium');
// GET /panel/api/clients/groups/premium/emails
// Returns: { success: true, obj: ['alice_vpn', 'bob_vpn', ...] }

Create a group

const result = await client.createGroup({ name: 'premium' });
// POST /panel/api/clients/groups/create

Rename a group

const result = await client.renameGroup({
    oldName: 'premium',
    newName: 'vip'
});
// POST /panel/api/clients/groups/rename

Delete a group

const result = await client.deleteGroup({ name: 'trial' });
// POST /panel/api/clients/groups/delete

Add clients to a group (bulk)

// 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 } }

Remove clients from a group (bulk)

// 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

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.

Get all nodes

const result = await client.getNodes();
// GET /panel/api/nodes/list
// Returns: { success: true, obj: [ ...nodes ] } - see "Node object shape" below

Get a specific node

const result = await client.getNode(2);
// GET /panel/api/nodes/get/2
// Returns: { success: true, obj: { ...node } } - see "Node object shape" below

Get node history metrics

const 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 time

Add a node

const 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".

Update a node

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 }

Delete a node

const result = await client.deleteNode(2);
// POST /panel/api/nodes/del/2
// Returns: { success: true, msg: "Delete node", obj: null }

Enable/disable a node

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 }

Test node connectivity

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
// } }

Probe a node (health check)

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 } }

Node object shape

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
}

Server-Side Generators

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.

Generate a new UUID

const result = await client.getNewUUID();
// GET /panel/api/server/getNewUUID
// Returns: { success: true, obj: { uuid: '...' } }

Generate an X25519 key pair (Reality)

const result = await client.getNewX25519Cert();
// GET /panel/api/server/getNewX25519Cert
// Returns: { success: true, obj: { privateKey: '...', publicKey: '...' } }

Generate an ML-DSA-65 key pair

const result = await client.getNewmldsa65();
// GET /panel/api/server/getNewmldsa65
// Returns: { success: true, obj: { seed: '...', verify: '...' } }

Generate an ML-KEM-768 key pair

const result = await client.getNewmlkem768();
// GET /panel/api/server/getNewmlkem768
// Returns: { success: true, obj: { seed: '...', client: '...' } }

Generate VLESS encryption settings

const result = await client.getNewVlessEnc();
// GET /panel/api/server/getNewVlessEnc
// Returns: { success: true, obj: { auths: [ { id, label, decryption, encryption }, ... ] } }

Generate an ECH certificate

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: '...' } }

Get web certificate file paths

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

Custom Geo lets you define and manage custom geographic IP/domain lists used in Xray routing rules.

Get all custom geo entries

const result = await client.getCustomGeos();
// GET /panel/api/custom-geo/list
// Returns: { success: true, obj: [ { id, type, alias, url, localPath, lastUpdatedAt, lastModified, createdAt, updatedAt }, ... ] }

Get geo aliases

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

Add a custom geo entry

// 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.

Update a custom geo entry

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 }

Delete a custom geo entry

const result = await client.deleteCustomGeo(3);
// POST /panel/api/custom-geo/delete/3

Download a custom geo file

const result = await client.downloadCustomGeo(3);
// POST /panel/api/custom-geo/download/3
// Triggers download/export of the geo file

Update all custom geo files

const 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 } }

Navigation

Previous Next
← Authentication Guide Inbound Management →

Last updated: June 2026

Clone this wiki locally