Integrations
API reference
The panel exposes three families of HTTP routes: panel routes used by the console itself, admin routes used by the Telegram bot and staff tooling, and the public node plane under
/api/public/* that worker nodes call. All of them live on the panel hostname, so https://panel.example.com/api/status is a complete URL.There is no anonymous write anywhere. Which gate applies depends on who the caller is supposed to be.
| Caller | Gate | How |
|---|---|---|
| Browser (console) | Session JWT | The panel session cookie is forwarded upstream; endpoints call requireAdmin(), which verifies against JWT_SECRET. |
| Telegram bot / internal service | Internal token | Send x-internal-token matching BOT_INTERNAL_TOKEN. Compared in constant time. |
| Worker node agent | HMAC-SHA256 | Send x-timestamp and x-signature signed with the node secret. |
Fail-closed
If
JWT_SECRET is unset or still the default, admin routes answer 401 with configRequired: true instead of allowing the call. If a node secret is missing, signed routes answer 503. Only an explicit GRVPN_ALLOW_INSECURE_DEV=1 bypasses either — never set it in production.Signing a node request
The signature is HMAC_SHA256(secret, "<timestamp>.<raw body>") in lowercase hex, where the timestamp is Unix seconds. Requests older than 300 seconds are rejected as stale, and a signature that has already been seen is rejected as a replay.
sign.sh
BODY='{"nodeId":"node-de-1","cpu":12.4}'
TS=$(date +%s)
SIG=$(printf '%s.%s' "$TS" "$BODY" \
| openssl dgst -sha256 -hmac "$NODE_SHARED_SECRET" -hex \
| awk '{print $2}')
curl -sS https://panel.example.com/api/public/node-heartbeat \
-H "content-type: application/json" \
-H "x-timestamp: $TS" \
-H "x-signature: $SIG" \
-d "$BODY"The secret resolves per node: NODE_SECRET_<NODE_ID> (uppercased, non alphanumerics replaced with _) if present, otherwise NODE_SHARED_SECRET.
§2 — /api/public/*
Public node plane#
This prefix skips the site session so agents can reach it, which is exactly why every handler authenticates the caller itself.
| Endpoint | Method | Auth | Purpose |
|---|---|---|---|
/api/public/node-handshake | POST | HMAC | Pin a node's TLS fingerprint on first pairing; body carries nodeId, fingerprint (64 hex), agentPort, version. |
/api/public/node-handshake | GET | Session | Panel-side lookup ?nodeId= returning { paired, agentPort, pairedAt, updatedAt, version } without leaking the fingerprint. |
/api/public/node-heartbeat | POST | HMAC | The beat: presence, load, NIC counters and per-account usage deltas. |
/api/public/node-heartbeat | GET | Session | Live fleet feed. Upgrades to WebSocket where supported, otherwise long-polls with ?rev= and answers 426 to refused upgrades. |
/api/public/node-rpc | POST | Admin | Panel → node command proxy (provision, restart, inspect). Includes a circuit breaker and a force-repin control method. |
/api/public/usage-reset | POST | Admin | Purge accumulated usage counters (also available as DELETE on the heartbeat route). |
/api/public/master-baseline | GET / POST | HMAC | Read and publish the master's baseline config so a node can converge on it. |
Soft failures
Missing node secrets and unreachable nodes return HTTP
200 with ok: false plus configRequired or cached. That is deliberate: a configuration state should not blank the console through an error boundary.| Endpoint | Method | Purpose |
|---|---|---|
/api/status | GET | System status and version; falls back to a safe default when upstream is down. |
/api/traffic | GET | Aggregate traffic series for the dashboard. |
/api/nodes/$id/traffic | GET | Per-node traffic series. |
/api/panel/state | GET / PUT | Read or replace the panel's persisted UI/runtime state blob. |
/api/panel/kv/$key | GET / PUT | Small key-value store for panel preferences and cached fragments. |
/api/admin/users | GET | Staff-facing user listing. |
/api/admin/bot/users | GET | Telegram-side user listing for the bot. |
/api/admin/bot/users/$id | GET | One Telegram user with wallet and account context. |
/api/settings/bot | GET | Bot configuration the storefront needs at runtime. |
/api/wallet/txns | GET | Ledger rows for a track: ?track=points|money|prepaid. |
/api/referrals/overview | GET | Referral leaderboard and totals, ?limit= capped. |
/api/referrals/user/$id | GET | One user's referral tree and earned rewards. |
Calling from a script
bash
curl -sS https://panel.example.com/api/admin/users \
-H "x-internal-token: $BOT_INTERNAL_TOKEN"
curl -sS "https://panel.example.com/api/wallet/txns?track=money" \
-H "x-internal-token: $BOT_INTERNAL_TOKEN"- Content type
- Always
application/jsonunless the route streams. - Success
- Either the resource itself or
{ ok: true, ... }. - Handled failure
- HTTP 200 with
{ ok: false, error }. - Auth failure
- 401
unauthorized, or 503 when a required secret is absent. - Validation failure
- 400 with the Zod message in
error. - Caching
- Live feeds send
cache-control: no-store.
- Timestamps are ISO-8601 strings; money is always integer cents.
- List endpoints accept a limit and return newest-first.
- Never expose the internal token or a node secret to a browser — both are server-side only.
