ZefiraZEFIRA Docs Changelog Donate GitHub ↗

API Reference

Cookie-authenticated JSON API for bots and dashboards.

Authenticate with POST /api/login (keeps the zefira_session cookie), then send X-Requested-With: XMLHttpRequest on every mutating call. No OpenAPI explorer is exposed.

Bots & integrations: create a long-lived token under Settings → API Tokens with scope full (default, everything) or bot (least-privilege for resellers: GET /api/me, GET /api/stats, GET /api/users, POST /api/users, GET /api/templates) and send it as Authorization: Bearer zfp_… — no CSRF header needed then. Tokens act as the admin that created them (password-gated actions still need the password), are shown once, and can be revoked anytime. Bot tokens can create users including start_on_first_use (bot-safe: expiry is server-computed) but get 403 on deletes, patches, backup/restore, settings, tokens and updates.

Ready-made reseller bots: Python · Node.js — plans keyboard, account creation, subscription link.

Auth & account

MethodEndpointNotes
POST/api/login{username, password}
POST/api/logoutInvalidates the session server-side (all devices, this panel has no per-session tracking)
GET POST DELETE/api/api-tokens… · /api/api-tokens/{id} · /api/api-tokens/self-testBot tokens: list, create (once-only secret), revoke. Creating one requires the admin password; a new token expires after 180 days (expires_in_days, 0 = never, max 20 per admin). self-test POSTs the candidate secret back and reports whether it authenticates, so you can tell a working token from a typo without spending a real call on it.
GET/api/meUsername, plus auth naming the credential that authenticated the call {type, name, scopes} — use it to tell your tokens apart
POST/api/change-passwordStrong-password enforced, kills other sessions (and revokes every API token — bots must re-auth with a new token)

Users

MethodEndpointNotes
GET/api/statsCounters: active, expired, expiring-soon, limited…
GET/api/users?q=…Up to 500, username/note search
GET/api/users/by-username/{username}Exact lookup — bots know usernames, not IDs
POST/api/usersCreate (protocols, volume, days, first-use, device limit) — or template_id alone to apply a saved plan
PATCH/api/users/{id}Change one user. Absolute fields set a value: used_gb, volume_gb, expires_at, note, device_limit, is_active, days (extend). Deltas: add_used_gb, add_volume_gb. Also reset_used: true.
DELETE/api/users/{id}—
POST/api/users/{id}/reset-tokenRotates token + all secrets (bot scope OK)
POST/api/users/{id}/reset-usageZeroes used traffic (for developers/bots)
POST/api/users/{id}/resetDeveloper combo: {"reset_usage": true, "reset_token": false} — one call for renew/top-up flows (token rotation kills the old link)
GET/api/users/{id}/qr · /configSub URL + QR / config file or ZIP (config: full scope only)

Server objects

MethodEndpointNotes
GET PUT/api/settingsDomain, ports (VLESS/VMess/Trojan/SS, Hysteria2, WireGuard, OpenVPN, L2TP, Cisco, SOCKS5), REALITY, obfuscation…
POST/api/reality/generateNew X25519 keypair
GET/api/reality/privateAudit-logged reveal
GET PUT/api/appearanceTheme colors, brand, dashboard note (GET is public, PUT needs session)
GET/theme.cssGenerated theme overrides, text/css
GET PUT/api/ai/settingsProvider, model, key state (key never returned)
POST/api/ai/testOne cheap completion that checks provider, base URL, model and key together. Run it right after saving the settings: it costs a single chat turn, and it turns a dead config into an error at the settings page instead of at the first real question.
POST/api/ai/chat{messages[≤12]} → scoped assistant reply (30/hr). The assistant can also act: create/extend/top-up/reset/pause users, find users, stats, sub links (max 3 ops/turn, audit-logged; delete/reset/backup/update/settings/tokens never)
GET/api/update/statusCurrent vs latest commit + incoming changelog
POST/api/update/applyPassword-confirmed pull + pip + restart
GET/api/ssl/statusCertbot presence, domains, expiry
POST/api/ssl/issue · /ssl/renew{domain, subdomain?, email}
GET POST DELETE/api/server-nodes…Register servers, health, latency, uptime
POST/api/server-nodes/{id}/checkOn-demand probe
GET POST DELETE/api/inbounds · /api/inbounds/{id}Extra ports per protocol (optional node pin)
GET POST DELETE/api/templates… · /api/nodes… · /api/blocklist… · /api/blocklist/pornPlans, tunnels, blocked sites. /api/blocklist/porn POSTs {porn_enabled: bool} and flips the built-in adult-domain preset in one call — the same switch as the button on the Site Blocker page.
POST/api/nodes/{id}/check · /guide · /reveal-token · /regen-tokenTunnel ops
POST/api/backupFull JSON export, password-confirmed (download). {"encrypt": true} returns a scrypt + Fernet blob instead of plaintext
POST/api/restorePassword-confirmed re-import (weak admin hashes / zero-volume users skipped, scopes preserved)
POST/api/restore-encrypted{password_confirm, salt, payload, backup_password?} — restores an encrypted backup
GET/api/audit · /api/system · /api/telegram… · /api/telegram/testLogs, CPU/RAM/disk, notifications. /api/telegram/test POSTs the token and chat id you are about to save and reports what the Bot API answered, so a bad chat id surfaces as a message from Telegram instead of as silence later.
GET PUT/api/tunnel-settingsThe two BackPack fields: public_url (the address the outer server dials back on) and trusted_proxies (CIDRs whose X-Forwarded-For is believed for rate limiting and audit logs). Same page as Settings → Tunnel; a mis-set proxy list makes every client look like one address.

Public

MethodEndpointNotes
GET/sub/{token}Browsers get the user dashboard; clients get Base64 sub, or Clash YAML with ?format=clash. Expired, disabled and out-of-volume (used_gb >= volume_gb) users 404 in all modes; device_limit is advisory only

Example: create a user

curl -c jar.txt -H 'Content-Type: application/json' \
     -H 'X-Requested-With: XMLHttpRequest' \
     -d '{"username":"admin","password":"…"}' \
     https://vpn.example.com/api/login

curl -b jar.txt -H 'Content-Type: application/json' \
     -H 'X-Requested-With: XMLHttpRequest' \
     -d '{"username":"buyer1","protocols":["vless","reality"],"volume_gb":50,"days":30}' \
     https://vpn.example.com/api/users

# Or create straight from a saved plan: template_id fills protocols, volume,
# days, start_on_first_use and device_limit. Explicit body fields win.
curl -b jar.txt -H 'Content-Type: application/json' \
     -H 'X-Requested-With: XMLHttpRequest' \
     -d '{"username":"buyer2","template_id":3,"days":60}' \
     https://vpn.example.com/api/users

# PATCH sets absolute values; add_* are deltas. An unknown-only patch is a
# 422 (never a silent 200), and sending both forms of one field is a 422.
curl -X PATCH -b jar.txt -H 'Content-Type: application/json' \
     -H 'X-Requested-With: XMLHttpRequest' \
     -d '{"used_gb": 12.5, "volume_gb": 100, "days": 30, "note": "VIP"}' \
     https://vpn.example.com/api/users/1

curl -X PATCH -b jar.txt -H 'Content-Type: application/json' \
     -H 'X-Requested-With: XMLHttpRequest' \
     -d '{"add_volume_gb": 20, "add_used_gb": -2.5}' \
     https://vpn.example.com/api/users/1

Example: bot renewal flow (bot-scoped token)

No cookies, no CSRF header — just Authorization: Bearer zfp_… from a bot-scoped token. Lookup by username, renew usage + link in one call:

# 1) find the buyer by username (bots know tg123, not numeric IDs)
curl -H 'Authorization: Bearer zfp_…' \
     https://vpn.example.com/api/users/by-username/tg123

# 2) renew: zero the meter AND rotate token+secrets (old link dies)
curl -X POST -H 'Authorization: Bearer zfp_…' \
     -H 'Content-Type: application/json' \
     -d '{"reset_usage": true, "reset_token": true}' \
     https://vpn.example.com/api/users/12/reset

# 3) usage-only top-up (link keeps working)
curl -X POST -H 'Authorization: Bearer zfp_…' \
     -H 'Content-Type: application/json' \
     -d '{"reset_usage": true, "reset_token": false}' \
     https://vpn.example.com/api/users/12/reset
bot tokens get 200 on lookup, QR, reset-usage and reset — and 403 on deletes, patches, backup/restore, settings, tokens and updates.