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.mdskills/client-lifecycle/SKILL.md
GitHubClient 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.