# Operator HTTP API (https://portunus.bybee.dev/en/docs/api/operator-http)



`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 [#authentication]

Web UI users authenticate through local password login:

```http
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:

```http
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:

| Status                                                                   | Body                                                            | Cause                                               |
| ------------------------------------------------------------------------ | --------------------------------------------------------------- | --------------------------------------------------- |
| `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_granted` | RBAC envelope rejection (see [RBAC](/en/docs/features/rbac))    |                                                     |
| `403 csrf_*`                                                             | Cookie-authenticated write failed CSRF checks                   |                                                     |
| `429 rate_limited`                                                       | Too many login, onboarding, or password-reset attempts          |                                                     |
| `503 bootstrap_required`                                                 | Server has no superadmin yet — complete Web onboarding          |                                                     |

## Endpoints by area [#endpoints-by-area]

### Auth [#auth]

| Method | Path                  | Notes                                                                                 |
| ------ | --------------------- | ------------------------------------------------------------------------------------- |
| `GET`  | `/v1/auth/status`     | Public status: `{ onboarding_required }`                                              |
| `POST` | `/v1/auth/onboarding` | Public first-admin creation; requires setup token while no active `superadmin` exists |
| `POST` | `/v1/auth/login`      | Public local password login; sets `portunus_session`                                  |
| `POST` | `/v1/auth/logout`     | Session logout; requires session cookie and CSRF headers                              |

### Self [#self]

| Method | Path                    | Notes                                                                                         |
| ------ | ----------------------- | --------------------------------------------------------------------------------------------- |
| `GET`  | `/v1/users/me`          | `{ user_id, role, display_name }` for the caller (used by SPA AuthGate)                       |
| `POST` | `/v1/users/me/password` | Change own password; body includes `current_password`, `new_password`, `new_password_confirm` |

### Clients [#clients]

| Method   | Path                            | Notes                                                          |
| -------- | ------------------------------- | -------------------------------------------------------------- |
| `POST`   | `/v1/client-enrollments`        | Create a one-time enrollment command for a new client          |
| `POST`   | `/v1/clients/{name}/enrollment` | Create a one-time re-enrollment command for an existing client |
| `GET`    | `/v1/clients`                   | List clients; superadmin sees all                              |
| `PUT`    | `/v1/clients/{name}`            | Update the client's advertised address; body `{address}`       |
| `POST`   | `/v1/clients/{name}/revoke`     | Revoke + disconnect                                            |
| `DELETE` | `/v1/clients/{name}`            | Delete a revoked client row                                    |

### Rules [#rules]

| Method   | Path                                        | Notes                                                                                         |
| -------- | ------------------------------------------- | --------------------------------------------------------------------------------------------- |
| `POST`   | `/v1/rules`                                 | Push a rule (single-target, multi-target, port-range, with caps, with SNI)                    |
| `GET`    | `/v1/rules`                                 | List 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}/stats`                 | Aggregate stats                                                                               |
| `GET`    | `/v1/rules/{rule_id}/stats?per_port=true`   | Per-port detail (range rules)                                                                 |
| `GET`    | `/v1/rules/{rule_id}/stats?per_target=true` | Per-target detail (multi-target rules)                                                        |
| `GET`    | `/v1/rules/{rule_id}/stats/stream`          | SSE stream, 5-second cadence; accepts `?per_target=true` (v0.6+)                              |

### Users [#users]

| Method   | Path                      | Notes                                                                                     |
| -------- | ------------------------- | ----------------------------------------------------------------------------------------- |
| `POST`   | `/v1/users`               | Add user (superadmin); accepts optional `initial_password` and `password_change_required` |
| `GET`    | `/v1/users`               | List users (superadmin)                                                                   |
| `GET`    | `/v1/users/{id}`          | User detail                                                                               |
| `DELETE` | `/v1/users/{id}`          | Cascading remove (superadmin)                                                             |
| `POST`   | `/v1/users/{id}/password` | Reset user password (superadmin); revokes sessions and API tokens by default              |

### Credentials [#credentials]

| Method   | Path                                          | Notes                       |
| -------- | --------------------------------------------- | --------------------------- |
| `POST`   | `/v1/users/{id}/credentials`                  | Issue a credential          |
| `GET`    | `/v1/users/{id}/credentials`                  | List                        |
| `DELETE` | `/v1/users/{id}/credentials/{cred_id}`        | Revoke                      |
| `POST`   | `/v1/users/{id}/credentials/{cred_id}/rotate` | Rotate (self or superadmin) |

### Grants [#grants]

| Method   | Path              | Notes                    |
| -------- | ----------------- | ------------------------ |
| `POST`   | `/v1/grants`      | Add grant (superadmin)   |
| `GET`    | `/v1/grants`      | List grants (superadmin) |
| `DELETE` | `/v1/grants/{id}` | Cascading revoke         |

### Rate limiting (v0.11+) [#rate-limiting-v011]

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

| Method   | Path                                                   | Notes                                    |
| -------- | ------------------------------------------------------ | ---------------------------------------- |
| `GET`    | `/v1/clients/{client_id}/owners`                       | List owners with rules under this client |
| `GET`    | `/v1/clients/{client_id}/owners/{owner_id}/rate-limit` | Read the per-owner envelope              |
| `PUT`    | `/v1/clients/{client_id}/owners/{owner_id}/rate-limit` | Set the per-owner envelope               |
| `DELETE` | `/v1/clients/{client_id}/owners/{owner_id}/rate-limit` | Remove the per-owner envelope            |

### Traffic quotas (v0.13+) [#traffic-quotas-v013]

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).

| Method   | Path                                                                    | Notes                                            |
| -------- | ----------------------------------------------------------------------- | ------------------------------------------------ |
| `GET`    | `/v1/users/{user_id}/quotas`                                            | List 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}/status`                       | Current 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}/quotas`                                      | List 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+) [#settings-v014]

| Method | Path                               | Notes                                                                            |
| ------ | ---------------------------------- | -------------------------------------------------------------------------------- |
| `GET`  | `/v1/settings/advertised-endpoint` | Read the advertised-endpoint override + effective value (superadmin)             |
| `PUT`  | `/v1/settings/advertised-endpoint` | Set/clear the override; host must be covered by the server cert SAN (superadmin) |

### Audit (v0.6+) [#audit-v06]

| Method | Path                                                                | Notes                                          |
| ------ | ------------------------------------------------------------------- | ---------------------------------------------- |
| `GET`  | `/v1/audit?limit=N&outcome=allow\|deny`                             | v0.7-shape JSON array (back-compat)            |
| `GET`  | `/v1/audit?since=<RFC3339>&until=<RFC3339>&cursor=<base64>&limit=N` | v0.8 envelope `{entries, next_cursor?, count}` |

### Metrics (v0.6+) [#metrics-v06]

| Method | Path          | Notes                                                           |
| ------ | ------------- | --------------------------------------------------------------- |
| `GET`  | `/v1/metrics` | RBAC-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 [#status-code-semantics]

| Code  | Meaning                                                                                                                      |
| ----- | ---------------------------------------------------------------------------------------------------------------------------- |
| `200` | Success (read)                                                                                                               |
| `201` | Resource created                                                                                                             |
| `204` | Success, no body                                                                                                             |
| `400` | Validation error                                                                                                             |
| `401` | `unauthenticated`                                                                                                            |
| `403` | RBAC denial, CSRF denial, or `password_change_required`                                                                      |
| `404` | Resource not found                                                                                                           |
| `409` | Conflict (e.g. `conflict.legacy_to_sni_unsupported`)                                                                         |
| `422` | Capability gate (e.g. `multi_target_unsupported_by_client`, `sni_unsupported_by_client`, `rate_limit_unsupported_by_client`) |
| `429` | Authentication throttle                                                                                                      |
| `503` | `bootstrap_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`](https://github.com/ZingerLittleBee/Portunus/tree/main/specs).

## Example: push a rule with caps [#example-push-a-rule-with-caps]

```sh
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
    }
  }'
```
