API Reference
Complete endpoint reference for server v1.50+. Practical integration recipes live in the separate API_GUIDE.md; only desktop-client-specific flows have their own chapter in CLIENT-API.md.
Conventions:
- All admin endpoints are reachable under
/api/v1/*. The path/api/*exists as a backward-compatible alias and is rewritten client-side (inpublic/js/app.js) to/api/v1/*. - Responses follow the pattern
{ ok: true, … }or{ ok: false, error: "…" }. Deviations are noted at the endpoint. - All examples use
gate.example.comas a placeholder for your GateControl domain.
Table of Contents
- Authentication
- Rate-Limiting
- Error responses
- Admin API
- Gateway-Companion API
- Desktop-Client API
- Public endpoints
- Quick-start examples
Authentication
GateControl knows three authentication paths. Different rules for CSRF, rate limits and input scope apply depending on the path.
Session Authentication (Browser)
Used by the admin UI. Login via POST /login sets a session cookie.
curl -c cookies.txt -X POST https://gate.example.com/login \
-d "username=admin&password=…"
curl -b cookies.txt https://gate.example.com/api/v1/peers
State-changing methods (POST/PUT/DELETE/PATCH) additionally require a CSRF token in the X-CSRF-Token header. The token is in window.GC.csrfToken (injected by the server into every template via res.locals.csrfToken) or in the _csrf body field.
API Token (Automation)
For scripts and integrations. Tokens are created under Settings → API Tokens. Bearer header:
curl -H "Authorization: Bearer gc_…" https://gate.example.com/api/v1/peers
Alternatively X-API-Token or X-API-Key. Token-authenticated requests bypass CSRF.
Token scopes
| Scope | Access |
|---|---|
full-access |
All endpoints (read + write) |
read-only |
Only GET requests |
peers |
/api/v1/peers/* completely |
routes |
/api/v1/routes/* completely |
settings |
/api/v1/settings/* + /api/v1/smtp/* |
webhooks |
/api/v1/webhooks/* |
logs |
/api/v1/logs/* |
system |
/api/v1/system/*, /api/v1/wg/*, /api/v1/caddy/* |
Gateway Token (Companion Container)
Each home gateway peer has its own bearer token, delivered in the gateway.env file. Applies exclusively to endpoints under /api/v1/gateway/*. Auth is done via Authorization: Bearer <apiToken>. The token is bound to a specific peer (see requireGateway middleware), CSRF does not apply.
Rate-Limiting
Built-in limiters per IP:
| Scope | Window | Limit |
|---|---|---|
/login |
15 min | 10 |
/api/v1/* (admin) |
15 min | 1000 |
/api/v1/client/* (peer agent) |
15 min | 100 |
/api/v1/client/peer/hostname |
1 min | 3 |
/api/v1/gateway/heartbeat |
— | no explicit limit (token auth) |
Response headers Ratelimit-Limit, Ratelimit-Remaining, Ratelimit-Reset are always set.
Error responses
{ "ok": false, "error": "Human-readable message", "fields": { "domain": "Required field" } }
fields is optional — only on schema validation errors (HTTP 400). Status codes:
| Code | Meaning |
|---|---|
| 200 | OK |
| 201 | Created |
| 304 | Not Modified (e.g. /gateway/config/check) |
| 400 | Validation error — details usually in fields |
| 401 | Not authenticated |
| 403 | CSRF missing/invalid or scope insufficient |
| 404 | Resource not found |
| 409 | Conflict (e.g. peer group with name already exists) |
| 429 | Rate limit |
| 500 | Server error |
Admin API
All following endpoints are behind requireAuth + apiLimiter (/api/v1/*). Writing operations require CSRF on session auth.
Dashboard
GET /api/v1/dashboard/stats
Returns the numbers for the 5 dashboard stat cards.
{
"ok": true,
"peers": { "total": 6, "online": 4 },
"routes": { "active": 7 },
"monitoring": { "total": 3, "up": 2, "down": 1 },
"traffic": { "today": 3200000000, "todayUpload": 1200000000, "todayDownload": 2000000000,
"uploadRate": 41200, "downloadRate": 128000 },
"wireguard": { "running": true },
"latency": 23
}
GET /api/v1/dashboard/traffic?period=1h|24h|7d
Chart data points for the traffic graph.
System
GET /api/v1/system/resources
CPU, RAM, uptime, disk. Called by the dashboard among others.
{ "ok": true,
"cpu": { "cores": 4, "percent": 8, "model": "…" },
"memory": { "total": 8e9, "used": 2e9, "percent": 25 },
"uptime": { "seconds": 1234, "days": 0, "hours": 0, "minutes": 20, "formatted": "20m", "bootTime": "2026-04-22" },
"disk": { "total": …, "free": …, "used": … }
}
GET /api/v1/system/dns/status · Feature internal_dns
Internal DNS service state: hosts-file metadata, peer count by hostname source (admin/agent/stale).
GET /api/v1/system/dns/records · Feature internal_dns
Snapshot of the entire internal DNS zone: static records (gateway.<domain>, server.<domain>), all peer records with source.
Logs
GET /api/v1/logs/activity?limit=100&offset=0&severity=info
Full activity log. Filters: severity, source, event_type, since, until (ISO date or epoch-ms).
GET /api/v1/logs/recent?limit=10
Activity feed for the dashboard.
GET /api/v1/logs/access?limit=100&offset=0
Caddy access log (filtered from /data/caddy/access.log).
GET /api/v1/logs/activity/export?format=csv|json
GET /api/v1/logs/access/export?format=csv|json
As file download.
Peers
GET /api/v1/peers?limit=250&offset=0
List of all peers.
{ "ok": true, "peers": [
{ "id": 73, "name": "NAS2", "peer_type": "regular", "enabled": 1, "allowed_ips": "10.8.0.3/32",
"tags": "NAS, Heimnetz", "group_id": 2, "hostname": "nas2", "hostname_source": "admin",
"expires_at": null, "total_rx": …, "total_tx": …, "latestHandshake": 1776…,
"isOnline": true }
] }
peer_type is either "regular" or "gateway".
GET /api/v1/peers/:id
Single peer.
POST /api/v1/peers
Creates a peer. Body:
{ "name": "Laptop", "description": "Travel device", "tags": "Desktop",
"group_id": 1, "expires_at": "2026-12-31", "dns": "1.1.1.1",
"is_gateway": false, "api_port": 9876 }
is_gateway: true creates a gateway peer and responds with { peer, gateway: { apiToken, pushToken, envContent } } (plaintext tokens only once; envContent contains a ready-to-use gateway.env).
PUT /api/v1/peers/:id
Partial update. All fields optional.
DELETE /api/v1/peers/:id
Removes the peer. Breaks existing WG handshakes.
PUT /api/v1/peers/:id/toggle
Enable/disable without deletion.
POST /api/v1/peers/batch
Body { action: "enable"|"disable"|"delete", ids: [1,2,3] }.
GET /api/v1/peers/:id/config
WireGuard config as plaintext body (Content-Type text/plain). Query ?download=1 triggers an attachment download.
GET /api/v1/peers/:id/qr
QR code as data URL + raw config.
{ "ok": true, "name": "…", "qr": "data:image/png;base64,…", "config": "[Interface]\n…" }
PATCH /api/v1/peers/:id/hostname · Feature internal_dns
Admin sets hostname for internal DNS. Body { hostname: "nas2" }. Empty string or null removes. Sticky-admin: overrides agent-reported names, agent writes cannot replace an admin value.
GET /api/v1/peers/:id/gateway-info
Only for peer_type === "gateway". Parsed last_health payload + state machine status + API port.
{ "ok": true, "gateway": {
"peer_id": 79, "status": "online", "api_port": 9876, "last_seen_at": 1776…,
"health": { "uptime_s": 4200, "wg_handshake_age_s": 45,
"route_reachability": [ { "route_id": 43, "reachable": true, "latency_ms": 2 } ],
"telemetry": { "gateway_version": "1.4.0", "cpu_cores": 4, "mem_used": 234000000, … } }
} }
GET /api/v1/peers/:id/traffic?period=24h|7d|30d
Chart data for per-peer traffic.
POST /api/v1/peers/:id/gateway-env/rotate
Rotates gateway API token + push token for a gateway peer. Invalidates the running companion immediately. Response contains fresh tokens + newly built envContent.
Peer groups
GET /api/v1/peer-groups
All groups with peer count.
{ "ok": true, "groups": [{ "id": 2, "name": "Heimnetz", "color": "#64748b", "description": "", "peer_count": 3 }] }
POST /api/v1/peer-groups — Body { name, color?, description? }
PUT /api/v1/peer-groups/:id
DELETE /api/v1/peer-groups/:id
Deletion sets group_id of all members to NULL.
Tags
Peer tags are a CSV on peers.tags. Since v1.48 there is also a registry table (tags) that manages tag names independently from peer assignments.
GET /api/v1/tags
Merged view: registry + distinct tokens from all peer CSVs.
{ "ok": true, "tags": [
{ "id": 3, "name": "server", "peer_count": 2, "registered": true },
{ "id": null, "name": "Pixel-only", "peer_count": 1, "registered": false }
] }
POST /api/v1/tags — Body { name }
Idempotent (INSERT OR IGNORE). Rejected on ,<>"\n\r\t or > 64 characters.
DELETE /api/v1/tags/:name
Deletes registry entry and strips the token from all peers.tags CSVs (case-insensitive, whole-word — prod does not wipe prod-backup).
Routes
Reverse-proxy configuration. Route types: http, l4. Targets: peer (direct WG peer) or gateway (via home gateway container).
GET /api/v1/routes?limit=250&offset=0&type=http|l4
GET /api/v1/routes/:id
POST /api/v1/routes
Body fields (excerpt):
{ "domain": "app.example.com",
"route_type": "http",
"target_kind": "peer",
"peer_id": 73,
"target_port": "5000",
"https_enabled": true,
"backend_https": false,
"compress_enabled": false,
"bot_blocker_enabled": false,
"bot_blocker_mode": "block",
"bot_blocker_config": {},
"monitoring_enabled": false,
"ip_filter_enabled": false,
"ip_filter_mode": "whitelist",
"ip_filter_rules": [],
"acl_enabled": false,
"acl_peer_ids": [],
"rate_limit_enabled": false,
"rate_limit_requests": 100,
"rate_limit_window": "1m",
"retry_enabled": false,
"retry_count": 3,
"retry_status_codes": "502,503,504",
"circuit_breaker_enabled": false,
"circuit_breaker_threshold": 5,
"circuit_breaker_timeout": 30,
"mirror_enabled": false,
"mirror_targets": [],
"debug_enabled": false,
"basic_auth_user": "",
"basic_auth_password": "",
"user_ids": [1, 2] }
Gateway routes: target_kind: "gateway", target_peer_id: <gateway-peer-id>, target_lan_host, target_lan_port (+ optional wol_enabled, wol_mac).
L4 routes: additionally l4_protocol (tcp|udp), l4_listen_port, l4_tls_mode (none|passthrough|terminate).
PUT /api/v1/routes/:id — partial update
DELETE /api/v1/routes/:id
PUT /api/v1/routes/:id/toggle
POST /api/v1/routes/batch — Body { action, ids }
POST /api/v1/routes/check-dns — Body { domain }
DNS check: loads A records, compares against the public IP of the server.
{ "ok": true, "matches": true, "resolvedIps": ["1.2.3.4"], "expectedIp": "1.2.3.4" }
GET /api/v1/routes/peers
List of available peers for the route wizard (online status, gateway flag, group). Query ?peer_type=gateway filters to gateway peers.
POST /api/v1/routes/:id/check
Trigger an immediate uptime monitoring check.
POST /api/v1/routes/:id/circuit-breaker/reset · Feature circuit_breaker
Resets a stuck circuit breaker manually to closed (zeroes cb_failure_count, clears cb_opened_at, re-renders Caddy). Necessary because the breaker state is persisted in SQLite and survives restarts — without a reset an open breaker waits for the monitoring's timeout and half-open cycle. Responds 400 if the circuit breaker is not enabled on the route.
POST /api/v1/routes/:id/branding/logo (Multipart file)
DELETE /api/v1/routes/:id/branding/logo
POST /api/v1/routes/:id/branding/bg-image (Multipart file)
DELETE /api/v1/routes/:id/branding/bg-image
Custom branding for the route-auth login page.
GET /api/v1/routes/:id/trace
Request tracing: returns the last N requests for the route. Details see features/request-tracing.md.
Route-Auth
Sub-resource to routes. Gateway is /:id/auth.
GET /api/v1/routes/:id/auth
POST /api/v1/routes/:id/auth
Body configures the auth method: email_password, email_code, totp; optional two_factor_enabled + two_factor_method. Fields: email, password, session_max_age (ms).
DELETE /api/v1/routes/:id/auth
POST /api/v1/routes/:id/auth/totp-setup
POST /api/v1/routes/:id/auth/totp-verify — Body { token }
RDP routes
RDP is its own route type with its own API under /api/v1/rdp/*. License-gated (remote_desktop).
GET /api/v1/rdp
List of all RDP routes.
POST /api/v1/rdp — Body (excerpt)
{ "name": "Office-PC",
"access_mode": "internal" | "external" | "both" | "gateway",
"target_host": "192.168.2.10", "target_port": 3389,
"listen_port": 13389,
"gateway_peer_id": 79, "gateway_listen_port": 3389,
"gateway_host": "rdg.example.com", "gateway_port": 443,
"credentials": { "username": "mka", "password": "…", "domain": "" },
"wol_enabled": true, "wol_mac": "AA:BB:CC:DD:EE:FF",
"network_level_auth": true, "audio_mode": "local", "clipboard_enabled": true,
"resolution": "1920x1080", "color_depth": 32,
"maintenance_windows": [] }
access_mode (from VALID_ACCESS_MODES in src/services/rdp.js):
internal— target is a direct WG peer in the tunnel (client's LAN IP + RDP port)external— target is reachable via public IP/domain (e.g. your own RDP server with port forwarding)both— both paths possible, desktop client chooses by reachabilitygateway— connection runs through a home gateway container; the server automatically creates a linked L4-TCP route (gateway_l4_route_idFK toroutes.id). See features/rdp-via-gateway.md.
Microsoft RD-Gateway (TSGateway) is not a dedicated mode but an additional field pair: gateway_host + gateway_port are passed through to the desktop client as .rdp config with gatewayhostname/gatewayport and can be configured additionally with any access mode.
GET /api/v1/rdp/:id
PATCH /api/v1/rdp/:id
DELETE /api/v1/rdp/:id
PUT /api/v1/rdp/:id/toggle
POST /api/v1/rdp/batch
GET /api/v1/rdp/status
Aggregated status of all RDP routes.
GET /api/v1/rdp/pubkey
Server public key for credential encryption (client-side).
GET /api/v1/rdp/:id/credentials
PUT /api/v1/rdp/:id/credentials — Body { credentials: { …E2EE-encrypted… } }
DELETE /api/v1/rdp/:id/credentials
Ciphertext in/out — password is never stored in plaintext. See src/utils/crypto.js.
POST /api/v1/rdp/:id/wol
Sends Wake-on-LAN magic packet via gateway (when gateway-typed) or directly (peer-typed). Body optional { timeout_ms }.
GET /api/v1/rdp/:id/wol/status
GET /api/v1/rdp/:id/status
POST /api/v1/rdp/:id/sessions/disconnect-all
Disconnects all active sessions (server-side kill via RDP admin protocol, if available — otherwise marker).
GET /api/v1/rdp/:id/history?limit=100
GET /api/v1/rdp/history/export?format=csv|json
Per-route session history.
GET /api/v1/rdp/:id/maintenance
PUT /api/v1/rdp/:id/maintenance — Body { windows: [{ from, to, note }] }
GET /api/v1/rdp/rotation/pending
POST /api/v1/rdp/:id/rotation/ack
Credential rotation workflow — which routes are waiting for a new password (because the master key was rotated), admin confirms after re-entry.
Gateways (admin view)
GET /api/v1/gateways
Consolidated list of all gateway peers. One call feeds the entire "Home Gateways" section on /peers.
{ "ok": true, "gateways": [{
"peer_id": 79, "name": "Home Gateway", "hostname": "nas1", "ip": "10.8.0.8",
"api_port": 9876, "status": "online", "last_seen_at": 1776…,
"health": { …complete last_health JSON including telemetry… },
"routes": [ { "id": 43, "domain": "nas.domaincaster.com", "route_type": "http",
"target_lan_host": "192.168.2.228", "target_lan_port": 5000 } ]
}] }
status comes from the gateway state machine (see src/services/gateways.js): "online", "offline", "unknown". Ground truth is route_reachability — details in the guide for _isHeartbeatHealthy (tests/gateway_health_definition.test.js).
Settings
All under /api/v1/settings/*.
GET /api/v1/settings/profile
PUT /api/v1/settings/profile — Body { username?, email?, language? }
PUT /api/v1/settings/password — Body { current, next }
POST /api/v1/settings/language — Body { language: "de"|"en" }
GET /api/v1/settings/app
PUT /api/v1/settings/default-theme — Body { theme: "pro"|"default" }
Server-wide default theme, applies to users without their own selection.
POST /api/v1/settings/clear-logs
Clears activity and access logs (confirmation via CSRF).
SMTP
GET /api/v1/smtp
PUT /api/v1/smtp — Body { host, port, secure, user, password, from }
POST /api/v1/smtp/test — Body { to }
Sends a test email to the given address.
Backup & Restore
GET /api/v1/settings/backup
Complete JSON backup (peers, routes, settings, webhooks, route-auth, peer-groups). Query ?format=download as attachment.
POST /api/v1/settings/backup/restore (Multipart)
Loads a backup JSON. Optional ?mode=merge|replace.
Auto-backup jobs
GET /api/v1/settings/autobackupPOST /api/v1/settings/autobackup— Body{ schedule: "daily"|"weekly"|..., keep: 7, target: "local"|"s3", … }PUT /api/v1/settings/autobackup/:idDELETE /api/v1/settings/autobackup/:idPOST /api/v1/settings/autobackup/:id/run— trigger manually
WireGuard
GET /api/v1/wg/status
Current interface status, peer handshakes.
POST /api/v1/wg/restart
wg-quick down wg0 && wg-quick up wg0.
Caddy
GET /api/v1/caddy/status
{ ok, running: bool, pid, uptime_s, admin_api: "127.0.0.1:2019" }.
POST /api/v1/caddy/reload
Forces re-rendering of the Caddy config and admin-API load.
Webhooks
GET /api/v1/webhooksPOST /api/v1/webhooks— Body{ name, url, events: ["peer_online", …], enabled, secret, retry_count? }PUT /api/v1/webhooks/:idDELETE /api/v1/webhooks/:idPUT /api/v1/webhooks/:id/togglePOST /api/v1/webhooks/:id/test— fires a test POST
API tokens
GET /api/v1/tokens— list (token hash only on create)POST /api/v1/tokens— Body{ name, scopes: ["full-access"], expires_at? }→ plain token returned onceDELETE /api/v1/tokens/:id— revoke
License
GET /api/v1/license— current license state + featuresPOST /api/v1/license/activate— Body{ key }POST /api/v1/license/refresh— force revalidation against the license APIDELETE /api/v1/license— remove locally, back to community fallback
Users
Only for role admin. Multi-user table for UI logins (as opposed to API tokens).
GET /api/v1/usersPOST /api/v1/users— Body{ username, email, password, role }PUT /api/v1/users/:idDELETE /api/v1/users/:idPOST /api/v1/users/:id/reset-password
Gateway-Companion API
Endpoints under /api/v1/gateway/*. The purpose of this group is data exchange between a home gateway container (gatecontrol-gateway) and the server. Auth: bearer token from the gateway.env — middleware requireGateway binds the token to a peer ID.
GET /api/v1/gateway/config
Fetches the current gateway configuration (routes, TCP listener ports, Caddy proxy port). Response contains config_hash for efficient polling.
GET /api/v1/gateway/config/check?hash=sha256:…
Long-poll-friendly hash check. Responds 304 when unchanged, otherwise 200 with new hash.
POST /api/v1/gateway/status — Body { rx_bytes, tx_bytes, active_connections }
Traffic counter snapshot. Is not counted as a heartbeat.
POST /api/v1/gateway/probe — Body arbitrary
Echo endpoint for end-to-end connection tests (round-trip time, payload integrity).
POST /api/v1/gateway/heartbeat — Body (excerpt)
{ "uptime_s": 4200,
"http_proxy_healthy": true,
"api_healthy": true,
"tcp_listeners": [{ "port": 13389, "status": "listening" }],
"wg_handshake_age_s": 45,
"dns_resolve_ok": true,
"route_reachability": [{ "route_id": 43, "reachable": true, "latency_ms": 2 }],
"overall_healthy": true,
"hostname": "nas1",
"config_hash": "sha256:…",
"telemetry": { "gateway_version": "1.4.0", "node_version": "20.20.2",
"wg_tools_version": "wireguard-tools v1.0.20210914",
"cpu_cores": 4, "cpu_load_avg": [0.15, 0.22, 0.18],
"mem_total": …, "mem_free": …, "mem_used": …,
"disk": { "total": …, "free": …, "used": … },
"os_platform": "linux", "os_release": "6.1.78", "arch": "arm64",
"dns_resolvers": ["192.168.1.1"], "default_gateway_ip": "192.168.1.1" }
}
Side effects:
gateway_meta.last_healthis stored as complete JSON- State machine reads
route_reachabilityas ground truth (fallback: self-check) hostname+telemetryadditionally updatepeers.hostname(agent-source, sticky-admin)
Desktop-Client API
Path /api/v1/client/*. Auth: bearer token + machine-fingerprint binding. Details: CLIENT-API.md.
Compact list:
| Endpoint | Purpose |
|---|---|
GET /ping |
Liveness check + server version |
GET /permissions |
What is this token allowed to do? |
POST /register |
New client registers a peer slot |
GET /config |
Current WG config |
GET /config/check?hash=… |
Hash-based idle poll |
POST /heartbeat |
Handshake status, rx/tx, optional hostname |
POST /status |
Detailed device status update |
POST /peer/hostname |
Opportunistic hostname report (internal DNS, rate-limited 3/min) |
GET /peer-info?peerId=… |
Lookup of another peer (e.g. for DNS fallback) |
GET /traffic?period=… |
Per-peer traffic history |
GET /services |
Assigned services (routes) for the client |
GET /dns-check?domain=… |
DNS lookup proxy for the UI diagnostic tool |
GET /rdp |
RDP routes this client is authorized for |
GET /rdp/:id/status |
Is the target reachable? |
GET /rdp/:id/connect |
Generate .rdp connect profile |
POST /rdp/:id/session |
Record session start |
PATCH /rdp/:id/session |
Session heartbeat |
DELETE /rdp/:id/session |
Session end |
GET /split-tunnel |
List of IP ranges to tunnel (override possible per token) |
Public endpoints
No auth required.
GET /health
Health check (DB + WG interface). Localhost requests get a detailed structure, remote only { ok }. 503 on error.
GET /api/v1/client/update
Public release info (version, download URL). This allows desktop clients to discover updates even if their token is invalid.
GET /metrics · Feature prometheus_metrics
Prometheus format. Auth via session or token (system/full-access/read-only). Additionally ?token=… allowed as query param.
POST /login · POST /logout
Admin auth endpoints (body see AUTHENTICATION.md).
Quick-start examples
Create a peer with an API token
TOKEN="gc_…"
curl -X POST https://gate.example.com/api/v1/peers \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{"name":"Laptop","description":"Travel device","tags":"Desktop"}' \
| jq .peer.id
Create a gateway peer (download gateway env)
curl -X POST https://gate.example.com/api/v1/peers \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{"name":"NAS1","is_gateway":true,"api_port":9876}' \
| jq -r .gateway.envContent > gateway.env
Query today's traffic (for monitoring dashboard)
curl -H "Authorization: Bearer $TOKEN" \
https://gate.example.com/api/v1/dashboard/stats | jq .traffic.today
Disable a route (home-automation scenario)
curl -X PUT -H "Authorization: Bearer $TOKEN" \
https://gate.example.com/api/v1/routes/7/toggle
Save a full backup as a file
curl -H "Authorization: Bearer $TOKEN" \
"https://gate.example.com/api/v1/settings/backup?format=download" \
-o backup-$(date -u +%F).json
More integration recipes (Home Assistant, Python, Tasker, GitHub Actions …): API_GUIDE.md.