Portunus
API Reference

Operator HTTP API

The /v1 surface served on operator_http_listen. Loopback by default, session-or-bearer authenticated, RBAC-gated.

portunus-server exposes its operator surface as an HTTP API on operator_http_listen (loopback by default). Most /v1/* requests require either a Web session cookie or an API bearer token.

Authentication

Web UI users authenticate through local password login:

POST /v1/auth/login
Host: 127.0.0.1:7080
Content-Type: application/json

{"user_id":"admin","password":"correct horse battery staple"}

Successful login returns {"password_change_required": false} and sets an HttpOnly portunus_session cookie. API clients and CLI automation continue to use bearer credentials:

GET /v1/clients
Host: 127.0.0.1:7080
Authorization: Bearer T006-quickstart-token

Cookie-authenticated mutating requests (POST, PUT, PATCH, DELETE) must include an exact same-origin Origin header, X-Portunus-CSRF: 1, and JSON content type when a body is present. Bearer-token requests do not need the CSRF header.

Responses on missing / invalid authentication:

StatusBodyCause
401 unauthenticated{"error":{"code":"unauthenticated","message":"..."}}Missing / invalid bearer or Web session
403 not_owner{"error":{"code":"not_owner","message":"..."}}Authenticated, but caller does not own the resource
403 password_change_required{"error":{"code":"password_change_required","message":"..."}}Web session must change a temporary password first
403 client_not_granted / port_outside_grant / protocol_not_grantedRBAC envelope rejection (see RBAC)
403 csrf_*Cookie-authenticated write failed CSRF checks
429 rate_limitedToo many login, onboarding, or password-reset attempts
503 bootstrap_requiredServer has no superadmin yet — complete Web onboarding

Endpoints by area

Auth

MethodPathNotes
GET/v1/auth/statusPublic status: { onboarding_required }
POST/v1/auth/onboardingPublic first-admin creation; requires setup token while no active superadmin exists
POST/v1/auth/loginPublic local password login; sets portunus_session
POST/v1/auth/logoutSession logout; requires session cookie and CSRF headers

Self

MethodPathNotes
GET/v1/users/me{ user_id, role, display_name } for the caller (used by SPA AuthGate)
POST/v1/users/me/passwordChange own password; body includes current_password, new_password, new_password_confirm

Clients

MethodPathNotes
POST/v1/client-enrollmentsCreate a one-time enrollment command for a new client
POST/v1/clients/{name}/enrollmentCreate a one-time re-enrollment command for an existing client
GET/v1/clientsList clients; superadmin sees all
PUT/v1/clients/{name}Update the client's advertised address; body {address}
POST/v1/clients/{name}/revokeRevoke + disconnect
DELETE/v1/clients/{name}Delete a revoked client row

Rules

MethodPathNotes
POST/v1/rulesPush a rule (single-target, multi-target, port-range, with caps, with SNI)
GET/v1/rulesList rules; non-superadmin sees only owned rules. Optional ?client=<name> filter
GET/v1/rules?owner=<user_id>Superadmin filter by owner
PUT/v1/rules/{rule_id}Hot-update rate_limit only; client, listen, protocol, and targets must stay unchanged
DELETE/v1/rules/{rule_id}Drain + remove
GET/v1/rules/{rule_id}/statsAggregate stats
GET/v1/rules/{rule_id}/stats?per_port=truePer-port detail (range rules)
GET/v1/rules/{rule_id}/stats?per_target=truePer-target detail (multi-target rules)
GET/v1/rules/{rule_id}/stats/streamSSE stream, 5-second cadence; accepts ?per_target=true (v0.6+)

Users

MethodPathNotes
POST/v1/usersAdd user (superadmin); accepts optional initial_password and password_change_required
GET/v1/usersList users (superadmin)
GET/v1/users/{id}User detail
DELETE/v1/users/{id}Cascading remove (superadmin)
POST/v1/users/{id}/passwordReset user password (superadmin); revokes sessions and API tokens by default

Credentials

MethodPathNotes
POST/v1/users/{id}/credentialsIssue a credential
GET/v1/users/{id}/credentialsList
DELETE/v1/users/{id}/credentials/{cred_id}Revoke
POST/v1/users/{id}/credentials/{cred_id}/rotateRotate (self or superadmin)

Grants

MethodPathNotes
POST/v1/grantsAdd grant (superadmin)
GET/v1/grantsList grants (superadmin)
DELETE/v1/grants/{id}Cascading revoke

Rate limiting (v0.11+)

Per-rule caps travel on the POST / PUT /v1/rules body (see rate_limit above). The endpoints below set a per-(owner, client) envelope.

MethodPathNotes
GET/v1/clients/{client_id}/ownersList owners with rules under this client
GET/v1/clients/{client_id}/owners/{owner_id}/rate-limitRead the per-owner envelope
PUT/v1/clients/{client_id}/owners/{owner_id}/rate-limitSet the per-owner envelope
DELETE/v1/clients/{client_id}/owners/{owner_id}/rate-limitRemove the per-owner envelope

Traffic quotas (v0.13+)

Per-(user, client) monthly traffic quotas and historical traffic queries. Traffic queries take from and to (Unix seconds, required) plus an optional bucket (sample granularity).

MethodPathNotes
GET/v1/users/{user_id}/quotasList a user's quotas
PUT/v1/users/{user_id}/quotas/{client_name}Set the monthly quota
PATCH/v1/users/{user_id}/quotas/{client_name}Update monthly_bytes and/or clear period usage
DELETE/v1/users/{user_id}/quotas/{client_name}Remove the quota
GET/v1/users/{user_id}/quotas/{client_name}/statusCurrent period usage vs. cap
GET/v1/users/{user_id}/traffic?from=<unix>&to=<unix>&bucket=<size>Historical traffic for a user
GET/v1/clients/{client_name}/quotasList a client's quotas
GET/v1/clients/{client_name}/traffic?from=<unix>&to=<unix>&bucket=<size>Historical traffic for a client
GET/v1/traffic/global?from=<unix>&to=<unix>&bucket=<size>Historical traffic across all rules

Settings (v0.14+)

MethodPathNotes
GET/v1/settings/advertised-endpointRead the advertised-endpoint override + effective value (superadmin)
PUT/v1/settings/advertised-endpointSet/clear the override; host must be covered by the server cert SAN (superadmin)

Audit (v0.6+)

MethodPathNotes
GET/v1/audit?limit=N&outcome=allow|denyv0.7-shape JSON array (back-compat)
GET/v1/audit?since=<RFC3339>&until=<RFC3339>&cursor=<base64>&limit=Nv0.8 envelope {entries, next_cursor?, count}

Metrics (v0.6+)

MethodPathNotes
GET/v1/metricsRBAC-gated mirror of the standalone /metrics; superadmin only

The standalone scraper-facing endpoint at metrics_listen:7081/metrics is unchanged (Prometheus continues scraping without bearer).

Status code semantics

CodeMeaning
200Success (read)
201Resource created
204Success, no body
400Validation error
401unauthenticated
403RBAC denial, CSRF denial, or password_change_required
404Resource not found
409Conflict (e.g. conflict.legacy_to_sni_unsupported)
422Capability gate (e.g. multi_target_unsupported_by_client, sni_unsupported_by_client, rate_limit_unsupported_by_client)
429Authentication throttle
503bootstrap_required

The full code → exit-code → HTTP-status mapping is frozen at v1; see the per-spec contract docs in specs/*/contracts/operator-api.md.

Example: push a rule with caps

curl -sS -X POST http://127.0.0.1:7080/v1/rules \
  -H "Authorization: Bearer $PORTUNUS_OPERATOR_TOKEN" \
  -H 'Content-Type: application/json' \
  -d '{
    "client": "edge-01",
    "listen_port": 8443,
    "targets": [
      {"host": "primary.local", "port": 443, "priority": 1},
      {"host": "backup.local",  "port": 443, "priority": 2}
    ],
    "protocol": "tcp",
    "sni_pattern": "*.example.com",
    "rate_limit": {
      "bandwidth_in_bps": 1048576,
      "concurrent_connections": 1000
    }
  }'

On this page