client-lifecycle

Run the day-to-day client operations on a 3x-ui panel — onboarding with quotas and expiry, handing over links, bulk renewals, usage reports, and cleanup of depleted or orphaned accounts. Use when aske

SKILL.md

GitHub

Client lifecycle

Clients in 3x-ui are email-keyed entities, not properties of an inbound: one client can be attached to several inbounds and moved between them without being recreated. email is just a unique label — it need not be an address.

Units, which cause most of the mistakes

Field Unit
expiry_time (tool parameter) Unix milliseconds. 0 = never
expiryTime (stored) Same. A negative value is a duration, not a date — "start after first use"
total_gb (tool parameter) GB, converted for you
totalGB (stored) Bytes, despite the name
limit_ip Simultaneous source IPs. 0 = unlimited
reset Traffic reset cycle in days. 0 = no cycle

A seconds-precision timestamp passed as expiry_time lands in 1970 and the client is born expired. When someone asks for "30 days", compute from now in milliseconds.

Negative expiry is the right tool for prepaid trials. Store the duration as a negative number of milliseconds and the panel converts it to an absolute deadline of now + duration on the client's first traffic tick — so a 7-day trial handed out in March still gives seven days to someone who first connects in June. The conversion is computed once per email, so a client attached to several inbounds lands on one consistent deadline.

Onboarding

add_client inbound_ids=[7] email="alice" total_gb=100 expiry_time=<now+30d in ms> limit_ip=3

The UUID is generated by the panel when omitted — do not invent one. For Trojan/Shadowsocks the panel generates the key server-side the same way.

sub_id groups clients under one subscription URL: clients sharing a sub_id share a subscription. Give each user their own unless you deliberately want a shared link. group is a free label the client-group tools operate on later, and is worth setting at creation — retrofitting groups across hundreds of clients is the tedious version of the same job.

For many at once, bulk_create_clients creates across the same inbounds, and bulk_attach_clients attaches existing clients to more inbounds afterwards.

Handing over access

get_subscription_links sub_id="<the client's subId>"

A subscription URL is better than a raw link: the user's app refreshes it, so adding an inbound or changing a host group reaches them without a new QR code. get_all_inbound_links is the raw list when a one-off link is genuinely wanted — it summarises and links to the full set unless full=true.

Both carry credentials. Treat them as secrets: they are the account.

Renewals and adjustments

bulk_adjust_clients emails=["alice","bob"] add_days=30 add_gb=50

Deltas, not absolutes, and both may be negative. Passing neither is an error ("no adjustment specified") rather than a no-op.

Clients are skipped rather than forced, each with a reason, and the reasons are worth knowing because "renew everyone" can quietly renew almost nobody:

Skip reason Who it hits
unlimited expiry expiryTime is 0 — adding days to "never" is meaningless
unlimited traffic totalGB is 0 — same logic for quota
reduction exceeds remaining time A negative add_days that would push the deadline into the past
reduction exceeds delay window A negative add_days larger than an unstarted trial's whole duration

Read that skipped list back to whoever asked instead of reporting a clean success.

Unstarted trials behave the way you would hope: adding days to a client with a negative (not-yet-started) expiry lengthens the trial duration and leaves it unstarted, rather than converting it to a fixed date. A 7-day trial topped up by 7 becomes a 14-day trial that still starts on first use.

For a single client, update_client overlays only the fields passed and preserves everything else, the UUID above all. Never rebuild a client to change one field: delete_client + add_client issues a new UUID and breaks the user's app.

Automatic renewal is a schedule on the client, not a job you run: reset (every N days), reset_day (a day of the month) or, from panel v3.9.0, reset_weekday (1-7, Mon-Sun), capped by reset_max. Weekly cannot be combined with the other two; update_client clears the mode being left. Before promising anyone a date, ask the panel — it computes in its own time zone:

preview_client_renewal email="alice" reset_weekday=1

bulk_enable_clients / bulk_disable_clients suspend without deleting — the right move for non-payment, since re-enabling restores the same credentials.

Reporting

list_clients_paged   # server-side filter, sort and page
get_client_traffic email="alice"
get_online_clients
get_last_online

list_clients_paged does the filtering on the panel; use it rather than pulling every client and filtering locally, which on a large panel is slow and floods the context. get_last_online is the fastest way to find accounts that are paid for but unused, and get_online_clients the fastest way to find one credential shared across many devices — cross-check with get_client_ips and list_client_devices.

On a multi-node panel, get_online_clients_by_node and get_client_ips_by_node attribute the same data to the node that actually served it.

Cleanup

Both of these are destructive and panel-wide:

delete_depleted_clients   # exhausted quota or past expiry
delete_orphan_clients     # attached to no inbound at all

"Depleted" has an exact definition, and it is narrower than it sounds — the panel deletes a client only when

reset = 0 AND ((total > 0 AND up + down >= total) OR (0 < expiryTime <= now))

Three groups are therefore safe by construction: clients on a reset cycle (reset non-zero) are never touched, unlimited clients (total 0, expiryTime 0) cannot match, and a trial that has not started yet (negative expiryTime) is not expired. That makes the call safer than its name suggests — but it also means "clean up the dead accounts" will quietly leave every cyclical client behind, which is usually a surprise to whoever asked.

Enumerate before deleting. list_clients_paged with the matching filter shows what is about to go; present that list, then delete. Orphans usually appear after an inbound was removed without its clients, so a run of delete_orphan_clients right after someone deleted an inbound is expected — one at a random moment is worth questioning.

Lighter cleanups: clear_client_ips after a user legitimately changes network, clear_client_devices or delete_client_device when HWID slots are full and a new device is being refused.

Migration and backup

export_clients produces the {client, inboundIds} array that import_clients consumes, so it round-trips. It carries every UUID and password — keep it behind the resource link rather than inlining it, and handle it as a credential file. import_clients reports existing emails in a skipped list instead of overwriting, so an import cannot clobber a live client.

Take an export (or backup_to_telegram) before any bulk operation. There is no undo for the destructive ones.