{
    "product": "Cenvero Stratum",
    "generated_at": "2026-08-03T07:16:49+00:00",
    "format": "cenvero-docs-v1",
    "document_count": 1,
    "documents": [
        {
            "slug": "api",
            "title": "Management API Reference",
            "category": null,
            "url": "https://www.stratum.cenvero.com/docs/api",
            "headings": [
                {
                    "level": 1,
                    "text": "Management API Reference"
                },
                {
                    "level": 2,
                    "text": "Getting started"
                },
                {
                    "level": 3,
                    "text": "1. The API is off until you set a token"
                },
                {
                    "level": 3,
                    "text": "2. Authenticate with a bearer token"
                },
                {
                    "level": 3,
                    "text": "3. Verify connectivity"
                },
                {
                    "level": 3,
                    "text": "Licence"
                },
                {
                    "level": 2,
                    "text": "Conventions"
                },
                {
                    "level": 3,
                    "text": "Error responses"
                },
                {
                    "level": 3,
                    "text": "Rate limits"
                },
                {
                    "level": 3,
                    "text": "Plan-gated subsystems"
                },
                {
                    "level": 1,
                    "text": "REST endpoint reference"
                },
                {
                    "level": 2,
                    "text": "Private networks"
                },
                {
                    "level": 3,
                    "text": "List networks"
                },
                {
                    "level": 3,
                    "text": "Create a network"
                },
                {
                    "level": 3,
                    "text": "Get a network"
                },
                {
                    "level": 3,
                    "text": "Delete a network"
                },
                {
                    "level": 3,
                    "text": "List a network's endpoints"
                },
                {
                    "level": 3,
                    "text": "Attach an endpoint"
                },
                {
                    "level": 3,
                    "text": "Change an endpoint's MAC address"
                },
                {
                    "level": 3,
                    "text": "Detach an endpoint"
                },
                {
                    "level": 2,
                    "text": "IP address management (IPAM)"
                },
                {
                    "level": 3,
                    "text": "List pools"
                },
                {
                    "level": 3,
                    "text": "Create a pool"
                },
                {
                    "level": 3,
                    "text": "List allocations"
                },
                {
                    "level": 3,
                    "text": "Allocate an IP"
                },
                {
                    "level": 3,
                    "text": "Release an IP"
                },
                {
                    "level": 3,
                    "text": "Delete a pool"
                },
                {
                    "level": 2,
                    "text": "Bridges"
                },
                {
                    "level": 2,
                    "text": "Network interfaces (NICs)"
                },
                {
                    "level": 2,
                    "text": "MAC bindings"
                },
                {
                    "level": 2,
                    "text": "DHCP leases"
                },
                {
                    "level": 2,
                    "text": "DNS"
                },
                {
                    "level": 3,
                    "text": "List zones"
                },
                {
                    "level": 3,
                    "text": "Create a zone"
                },
                {
                    "level": 3,
                    "text": "Delete a zone"
                },
                {
                    "level": 3,
                    "text": "List records"
                },
                {
                    "level": 3,
                    "text": "Create a record"
                },
                {
                    "level": 3,
                    "text": "Update a record"
                },
                {
                    "level": 3,
                    "text": "Delete a record"
                },
                {
                    "level": 3,
                    "text": "DNSSEC material for a zone"
                },
                {
                    "level": 2,
                    "text": "Static & policy routing"
                },
                {
                    "level": 3,
                    "text": "List routes"
                },
                {
                    "level": 3,
                    "text": "Create a route"
                },
                {
                    "level": 3,
                    "text": "Delete a route"
                },
                {
                    "level": 3,
                    "text": "List policy rules"
                },
                {
                    "level": 3,
                    "text": "Create a policy rule"
                },
                {
                    "level": 3,
                    "text": "Delete a policy rule"
                },
                {
                    "level": 2,
                    "text": "Port forwarding"
                },
                {
                    "level": 2,
                    "text": "VLAN lockdown"
                },
                {
                    "level": 3,
                    "text": "List VLAN policies"
                },
                {
                    "level": 3,
                    "text": "Set a VLAN policy"
                },
                {
                    "level": 3,
                    "text": "Clear a VLAN policy"
                },
                {
                    "level": 2,
                    "text": "VXLAN overlays"
                },
                {
                    "level": 3,
                    "text": "List overlay networks"
                },
                {
                    "level": 3,
                    "text": "Create an overlay network"
                },
                {
                    "level": 3,
                    "text": "Delete an overlay network"
                },
                {
                    "level": 3,
                    "text": "List peers"
                },
                {
                    "level": 3,
                    "text": "Add a peer"
                },
                {
                    "level": 3,
                    "text": "Remove a peer"
                },
                {
                    "level": 3,
                    "text": "Forwarding database"
                },
                {
                    "level": 2,
                    "text": "Firewall rules & connection tracking"
                },
                {
                    "level": 2,
                    "text": "Load balancer"
                },
                {
                    "level": 2,
                    "text": "BGP & route filtering"
                },
                {
                    "level": 3,
                    "text": "Prefix-lists"
                },
                {
                    "level": 3,
                    "text": "Route-maps"
                },
                {
                    "level": 3,
                    "text": "Apply import/export policy"
                },
                {
                    "level": 2,
                    "text": "NIC bonds"
                },
                {
                    "level": 2,
                    "text": "Gateway HA & floating IPs"
                },
                {
                    "level": 3,
                    "text": "Gateway HA"
                },
                {
                    "level": 3,
                    "text": "Source NAT (masquerade)"
                },
                {
                    "level": 3,
                    "text": "Floating IPs"
                },
                {
                    "level": 2,
                    "text": "Traffic visibility"
                },
                {
                    "level": 3,
                    "text": "Flows"
                },
                {
                    "level": 3,
                    "text": "Accounting"
                },
                {
                    "level": 3,
                    "text": "Bandwidth limits, pools & quotas"
                },
                {
                    "level": 2,
                    "text": "Cluster"
                },
                {
                    "level": 2,
                    "text": "Multi-tenancy"
                },
                {
                    "level": 3,
                    "text": "Create / list tenants"
                },
                {
                    "level": 3,
                    "text": "Update a tenant"
                },
                {
                    "level": 3,
                    "text": "Tenant quota"
                },
                {
                    "level": 3,
                    "text": "Tenant-scoped API keys"
                },
                {
                    "level": 2,
                    "text": "Billing automation (operator hook)"
                },
                {
                    "level": 2,
                    "text": "Container networking"
                },
                {
                    "level": 2,
                    "text": "Operations"
                },
                {
                    "level": 3,
                    "text": "Alerting"
                },
                {
                    "level": 3,
                    "text": "Self-healing"
                },
                {
                    "level": 3,
                    "text": "Backups"
                },
                {
                    "level": 3,
                    "text": "Node dashboard"
                },
                {
                    "level": 3,
                    "text": "Not implemented"
                },
                {
                    "level": 3,
                    "text": "Event log (Server-Sent Events)"
                },
                {
                    "level": 2,
                    "text": "Real-time WebSocket"
                },
                {
                    "level": 3,
                    "text": "1. Connect & authenticate"
                },
                {
                    "level": 3,
                    "text": "2. Subscribe"
                },
                {
                    "level": 3,
                    "text": "3. Receive events"
                },
                {
                    "level": 3,
                    "text": "Example — `wscat`"
                },
                {
                    "level": 3,
                    "text": "Example — browser"
                },
                {
                    "level": 2,
                    "text": "Event webhooks"
                },
                {
                    "level": 3,
                    "text": "Register a webhook"
                },
                {
                    "level": 3,
                    "text": "Delivery format"
                },
                {
                    "level": 3,
                    "text": "Test a webhook"
                },
                {
                    "level": 2,
                    "text": "Bulk create"
                },
                {
                    "level": 2,
                    "text": "Host addressing"
                },
                {
                    "level": 2,
                    "text": "DHCP scopes"
                },
                {
                    "level": 2,
                    "text": "Reservations, node identity and services"
                },
                {
                    "level": 3,
                    "text": "Static DHCP reservations"
                },
                {
                    "level": 3,
                    "text": "Node identity"
                },
                {
                    "level": 3,
                    "text": "Services"
                },
                {
                    "level": 2,
                    "text": "Subsystem reads"
                },
                {
                    "level": 2,
                    "text": "Metrics scrape"
                },
                {
                    "level": 2,
                    "text": "gRPC endpoint"
                },
                {
                    "level": 2,
                    "text": "Every endpoint, ready to paste"
                },
                {
                    "level": 3,
                    "text": "Accounting & billing data"
                },
                {
                    "level": 3,
                    "text": "Host addressing (default interface)"
                },
                {
                    "level": 3,
                    "text": "Alerting"
                },
                {
                    "level": 3,
                    "text": "Operator API keys"
                },
                {
                    "level": 3,
                    "text": "Audit"
                },
                {
                    "level": 3,
                    "text": "Backups"
                },
                {
                    "level": 3,
                    "text": "Bandwidth & quotas"
                },
                {
                    "level": 3,
                    "text": "BGP"
                },
                {
                    "level": 3,
                    "text": "Billing automation"
                },
                {
                    "level": 3,
                    "text": "NIC bonds"
                },
                {
                    "level": 3,
                    "text": "Bridges"
                },
                {
                    "level": 3,
                    "text": "Cluster"
                },
                {
                    "level": 3,
                    "text": "Configuration"
                },
                {
                    "level": 3,
                    "text": "Container networking"
                },
                {
                    "level": 3,
                    "text": "Dashboard"
                },
                {
                    "level": 3,
                    "text": "DHCP"
                },
                {
                    "level": 3,
                    "text": "DNS"
                },
                {
                    "level": 3,
                    "text": "Endpoint index"
                },
                {
                    "level": 3,
                    "text": "Event stream"
                },
                {
                    "level": 3,
                    "text": "Floating IPs"
                },
                {
                    "level": 3,
                    "text": "Flow tracking"
                },
                {
                    "level": 3,
                    "text": "Port forwarding"
                },
                {
                    "level": 3,
                    "text": "Gateway & NAT"
                },
                {
                    "level": 3,
                    "text": "Geneve tunnels"
                },
                {
                    "level": 3,
                    "text": "Self-healing"
                },
                {
                    "level": 3,
                    "text": "Health"
                },
                {
                    "level": 3,
                    "text": "Address pools"
                },
                {
                    "level": 3,
                    "text": "Layer-7 load balancer"
                },
                {
                    "level": 3,
                    "text": "Layer-4 load balancer"
                },
                {
                    "level": 3,
                    "text": "Licence"
                },
                {
                    "level": 3,
                    "text": "MAC bindings"
                },
                {
                    "level": 3,
                    "text": "Metrics"
                },
                {
                    "level": 3,
                    "text": "NAT64"
                },
                {
                    "level": 3,
                    "text": "Networks & endpoints"
                },
                {
                    "level": 3,
                    "text": "Interfaces & addressing"
                },
                {
                    "level": 3,
                    "text": "Node identity"
                },
                {
                    "level": 3,
                    "text": "Plugins"
                },
                {
                    "level": 3,
                    "text": "Routing"
                },
                {
                    "level": 3,
                    "text": "Firewall rules"
                },
                {
                    "level": 3,
                    "text": "Services"
                },
                {
                    "level": 3,
                    "text": "Status"
                },
                {
                    "level": 3,
                    "text": "Tenants"
                },
                {
                    "level": 3,
                    "text": "TLS"
                },
                {
                    "level": 3,
                    "text": "Topology"
                },
                {
                    "level": 3,
                    "text": "VLAN lockdown"
                },
                {
                    "level": 3,
                    "text": "VRF devices"
                },
                {
                    "level": 3,
                    "text": "VXLAN overlays"
                },
                {
                    "level": 3,
                    "text": "Webhooks"
                },
                {
                    "level": 2,
                    "text": "Feedback"
                },
                {
                    "level": 2,
                    "text": "See also"
                }
            ],
            "word_count": 15931,
            "markdown": "# Management API Reference\n\nEvery Stratum node runs a **local management API** so you can manage that node's\nnetworking yourself, from your own tooling. The full management surface is the\n**REST** API over HTTPS on port **`7070`**, alongside a **gRPC** health endpoint\non **`7071`** and a **WebSocket** event stream on **`7072`** — sharing one\nauthentication model and one TLS certificate.\n\nThis page is the complete REST reference, plus the WebSocket event stream and the\ngRPC health endpoint. The base URL is:\n\n```\nhttps://<node-ip>:7070\n```\n\nAll REST paths below are relative to this base and live under the `/api/v1`\nprefix.\n\n---\n\n## Getting started\n\n### 1. The API is off until you set a token\n\nThe management API is **disabled by default** and **fails closed**: the agent\nwill *never* serve these endpoints unauthenticated. It starts listening only when\nan **API token** is configured. With no token the API is cleanly disabled, and\nyou manage the node through the panel and the local `cenvero-str-ctl` socket\ninstead.\n\nAsk the node for a token whenever you need one:\n\n```bash\nsudo cenvero-str-ctl api-token generate\n```\n\nIt prints the token **once** — store it then, because it is not shown again. To\nchoose the value yourself, pipe it in so it never reaches your shell history:\n\n```bash\nprintf '%s' \"$MY_TOKEN\" | sudo cenvero-str-ctl api-token set\n```\n\n```bash\ncenvero-str-ctl api-token status   # is one set? (never prints the value)\ncenvero-str-ctl api-token clear    # remove it — the API stops at the next restart\n```\n\nThe token is stored root-only on the node, in your local overrides, so a later\npanel sync does not discard it. You can also have the installer mint one up\nfront with `CENVERO_API_TOKEN=auto`.\n\nIt is **not** settable with `cenvero-str-ctl config set` — that command\ndeliberately refuses credential keys, because a value passed as a command\nargument ends up in shell history. `api-token` exists for exactly this reason.\n\nThe API listens only when **all three** hold: a token is configured,\n`service rest` is on, and a TLS certificate is available (TLS is mandatory — the\nagent refuses to serve plaintext). You can move or gate the listener without\nremoving the token:\n\n```bash\ncenvero-str-ctl config set api_bind_address 10.0.0.5   # bind to a management IP\ncenvero-str-ctl service rest off                        # stop serving REST\ncenvero-str-ctl service rest on                         # serve it again\ncenvero-str-ctl service status                          # show each service's state\n```\n\n### 2. Authenticate with a bearer token\n\nSend the token in the `Authorization` header on every protected call:\n\n```\nAuthorization: Bearer <token>\n```\n\nThree credential types are accepted, depending on the transport:\n\n| Credential | How you get it | Scope | Works on |\n|---|---|---|---|\n| **Node API token** | The `api_token` you configured on the node | Full (node-wide) | REST, WebSocket, gRPC |\n| **Operator API key** | `cenvero-str-ctl apikeys mint <label>` (secret shown once) | Full (node-wide) | REST only |\n| **Tenant-scoped key** | `POST /api/v1/tenant/{id}/keys` (see Multi-tenancy below) | Confined to one tenant | REST only |\n\nA **tenant-scoped key** may only act on its own tenant — i.e. `/tenant/{id}/...`\nand `/billing/tenants/{id}/...` where `{id}` is that key's tenant; any other path\nreturns **403**. The node API token and operator keys are never confined. The\n**WebSocket and gRPC** transports accept the **node API token only** (not operator\nor tenant-scoped keys). The local `cenvero-str-ctl` socket needs no token at all —\nit is the node's always-available lifeline and can never be disabled.\n\n### 3. Verify connectivity\n\nTwo endpoints need **no token** and are handy for a connectivity check. Examples\non this page use these shell variables:\n\n```bash\nexport NODE=https://<node-ip>:7070\nexport TOKEN=<your-api-token>\n```\n\n```bash\ncurl -k \"$NODE/api/v1/health\"\n```\n\n```json\n{\n  \"status\": \"healthy\",\n  \"uptime\": \"3h14m22s\"\n}\n```\n\nThis endpoint is **deliberately minimal**. It answers one question — is the agent\nup, and for how long — and nothing more. Because it is reachable without a token,\nanything it returned would be readable by anyone who can reach the port, so it\ndoes not report the build version, the hardware, or any configuration. Point a\nload balancer or an uptime monitor at it and treat a `200` as alive.\n\nFor the node's version and its network-acceleration detail, use the\n**authenticated** status endpoint:\n\n```bash\ncurl -k \"$NODE/api/v1/status\" -H \"Authorization: Bearer $TOKEN\"\n```\n\n```json\n{\n  \"status\": \"healthy\",\n  \"version\": \"1.0.0\",\n  \"uptime\": \"3h14m22s\",\n  \"acceleration\": {\n    \"mode\": \"software\",\n    \"summary\": \"Software (CPU)\",\n    \"detail\": \"Software (CPU) — hardware acceleration not available on this network card\"\n  }\n}\n```\n\n### Licence\n\n`GET /api/v1/license` — the node's licence: who it was issued to, its plan and\nchannel, when it expires, the enforcement state, the plan's speed ceiling and the\ncapability map. Read-only — it never contacts the licence server and never\nchanges anything, so it is safe to poll on a schedule.\n\n```bash\ncurl -k \"$NODE/api/v1/license\" -H \"Authorization: Bearer $TOKEN\"\n```\n\n```json\n{\n  \"installed\": true,\n  \"serial_number\": \"a3f9-...\",\n  \"plan\": \"enterprise\",\n  \"release_channel\": \"stable\",\n  \"valid_until\": \"2026-12-31T00:00:00Z\",\n  \"state\": \"active\",\n  \"days_remaining\": 152,\n  \"expired\": false,\n  \"mutations_allowed\": true,\n  \"hardware_grace\": false,\n  \"max_bandwidth_gbps\": 25,\n  \"features\": { \"bgp\": true, \"cluster\": true, \"ids\": false }\n}\n```\n\n| Field | Meaning |\n|---|---|\n| `state` | `active`, `warning` (expiring soon), `grace` (just expired), `frozen` |\n| `mutations_allowed` | Whether state-changing requests are accepted **right now** |\n| `hardware_grace` | The licence is valid but bound to hardware that no longer matches |\n| `max_bandwidth_gbps` | The plan's aggregate node ceiling; `0` means uncapped |\n| `days_remaining` | Negative once expired |\n\n**Branch on `mutations_allowed`, not on `state`.** Licence enforcement never\nsevers traffic — a frozen node keeps forwarding packets and keeps its workloads\nrunning; what it refuses is *changes*. So this is the field that tells automation\nwhether a write will be accepted.\n\nWith no licence installed the answer is `{\"installed\": false}` with the identity\nfields absent, rather than a document full of blanks that reads like a licence.\n\n`GET /api/v1/docs` (also unauthenticated) returns a short machine-readable index\nof the endpoint paths — the same list published in this reference.\n\nBecause the node's certificate is privately managed, the `curl` examples below\nuse `-k` to skip system trust. To **pin** the certificate instead, fetch its\npublic key from the unauthenticated endpoint `GET /api/v1/tls/pubkey` (returns the\nPEM as `text/plain`).\n\n---\n\n## Conventions\n\n- **Base URL** — `https://<node-ip>:7070`; every REST path is under `/api/v1`.\n- **HTTPS only** — the agent refuses to serve plaintext; your client must trust\n  (or pin) the node's certificate.\n- **JSON only** — every request that carries a body must send\n  `Content-Type: application/json`; anything else is rejected with **415**.\n  Responses are always JSON.\n- **Body cap** — request bodies over **1 MiB** are rejected with **413**.\n- **Status codes** — successful reads/updates return **200**; resource creation\n  returns **201**.\n- **Timestamps** — UTC throughout (RFC 3339, e.g. `2026-06-30T14:05:00Z`), unless\n  a field is explicitly a Unix epoch.\n- **No pagination** — list endpoints return the full collection under a named key\n  (e.g. `{\"networks\": [ ... ]}`); filters are provided per-endpoint via query\n  parameters where noted.\n\n### Error responses\n\nErrors are returned as JSON `{ \"error\": \"...\" }` with a matching HTTP status:\n\n| Status | Meaning |\n|---|---|\n| `400` | Malformed request — bad/invalid body or parameter (e.g. an invalid CIDR). |\n| `401` | Missing or invalid bearer token: `{\"error\":\"unauthorized\"}`. |\n| `403` | Forbidden — see the cases below. |\n| `404` | Unknown resource (e.g. an unknown network or record id). |\n| `413` | Request body over the 1 MiB cap. |\n| `415` | Body sent without `Content-Type: application/json`. |\n| `429` | Rate-limited, or too many failed auth attempts (temporary IP block). |\n| `501` | The operation is intentionally not supported (noted per-endpoint). |\n| `503` | That subsystem is not enabled/wired on this node: `{\"error\":\"<name> service not available\"}`. |\n\nA **403** is returned in any of these situations:\n\n- **Source not allowed** — when an IP allowlist (`api_allowed_ips`) is configured\n  and your address is not on it: `{\"error\":\"forbidden: source address not allowed\"}`.\n- **License frozen** — a *mutating* call (POST/PUT/DELETE) while the license is\n  frozen: `{\"error\":\"license inactive: changes are frozen until the license is\n  renewed; existing workloads keep running\"}`. Reads (GET) are never blocked, and\n  running workloads are untouched.\n- **Feature not in your plan** — see plan-gated subsystems below.\n- **Tenant-scope violation** — a tenant-scoped key used outside its own tenant.\n\n### Rate limits\n\nThe API applies a **per-source-IP** token-bucket limit. The defaults are\n**1000 requests per minute** with a **burst of 100**; exceed it and you get\n**429** `{\"error\":\"rate limit exceeded\"}`. Tune them with `api_rate_limit`\n(requests/minute) and `api_rate_burst`.\n\n### Plan-gated subsystems\n\nSome subsystems are available only if your license plan includes them. For a\nplan-gated subsystem, **every** call — reads as well as writes — returns **403**\nwith a message naming the missing feature (or \"no license installed\" when no\nlicense is active).\n\n| Path prefix | Required feature |\n|---|---|\n| `/networks` | Private Networks |\n| `/dns/*` | DNS |\n| `/dhcp/*` | DHCP |\n| `/vxlan/*` | VXLAN |\n| `/lb`, `/lb/*` | Load Balancer |\n| `/bgp/*` | BGP |\n| `/gateway/*` | Gateway HA |\n| `/bandwidth`, `/bandwidth/*` | Bandwidth shaping (limits + pools + quotas) |\n| `/cluster`, `/cluster/*` | Cluster |\n\nEverything else — IPAM, firewall `/rules`, `/routes`, `/vlan`, `/bridges`,\n`/nics`, `/macbind`, `/forward`, `/bonds`, `/float`, `/flows`, `/accounting`,\n`/tenant`, `/billing`, `/containers`, and the operations endpoints — is available\non any active plan.\n\n---\n\n# REST endpoint reference\n\n## Private networks\n\nManaged private (SDN) networks. You create a network from a CIDR pool, and the\nagent automatically materializes **one endpoint profile per usable host IP** —\neach a fixed IP paired with a system-generated MAC. *Attaching* claims a free\nprofile and programs its address binding into the data plane; *detaching* frees\nit. *(Plan feature: Private Networks.)*\n\n**Network fields**\n\n| Field | Type | Notes |\n|---|---|---|\n| `id` | string | Server-assigned network id. |\n| `name` | string | **Required** on create, unique per node. |\n| `cidr` | string | **Required**, IPv4 only (e.g. `10.20.0.0/24`). |\n| `gateway` | string | Optional gateway IP. |\n| `vlan` | int | Optional VLAN id to tag the network. |\n| `tenant_id` | string | Optional — scopes the network to one of your tenants. |\n| `created_at` | string | UTC timestamp. |\n\n**Endpoint fields:** `id`, `network_id`, `ip`, `mac`, `state` (`free` or\n`bound`), and `bound_at` (set once bound).\n\n### List networks\n\n`GET /api/v1/networks`\n\n```bash\ncurl -k \"$NODE/api/v1/networks\" -H \"Authorization: Bearer $TOKEN\"\n```\n\n```json\n{\n  \"networks\": [\n    {\n      \"id\": \"net-a1b2c3d4\",\n      \"name\": \"app-net\",\n      \"cidr\": \"10.20.0.0/24\",\n      \"gateway\": \"10.20.0.1\",\n      \"vlan\": 100,\n      \"tenant_id\": \"t-acme\",\n      \"created_at\": \"2026-06-30T12:00:00Z\"\n    }\n  ]\n}\n```\n\n### Create a network\n\n`POST /api/v1/networks` — body: `name` and `cidr` required; `gateway`, `vlan`,\n`tenant_id` optional. Returns **201**.\n\n```bash\ncurl -k -X POST \"$NODE/api/v1/networks\" \\\n  -H \"Authorization: Bearer $TOKEN\" -H \"Content-Type: application/json\" \\\n  -d '{\"name\":\"app-net\",\"cidr\":\"10.20.0.0/24\",\"gateway\":\"10.20.0.1\",\"vlan\":100}'\n```\n\n```json\n{\n  \"status\": \"created\",\n  \"network\": {\n    \"id\": \"net-a1b2c3d4\",\n    \"name\": \"app-net\",\n    \"cidr\": \"10.20.0.0/24\",\n    \"gateway\": \"10.20.0.1\",\n    \"vlan\": 100,\n    \"created_at\": \"2026-06-30T12:00:00Z\"\n  }\n}\n```\n\n### Get a network\n\n`GET /api/v1/networks/{id}`\n\n```bash\ncurl -k \"$NODE/api/v1/networks/net-a1b2c3d4\" -H \"Authorization: Bearer $TOKEN\"\n```\n\n```json\n{ \"network\": { \"id\": \"net-a1b2c3d4\", \"name\": \"app-net\", \"cidr\": \"10.20.0.0/24\", \"gateway\": \"10.20.0.1\" } }\n```\n\n### Delete a network\n\n`DELETE /api/v1/networks/{id}`\n\n```bash\ncurl -k -X DELETE \"$NODE/api/v1/networks/net-a1b2c3d4\" -H \"Authorization: Bearer $TOKEN\"\n```\n\n```json\n{ \"status\": \"deleted\", \"id\": \"net-a1b2c3d4\" }\n```\n\n### List a network's endpoints\n\n`GET /api/v1/networks/{id}/endpoints`\n\n```bash\ncurl -k \"$NODE/api/v1/networks/net-a1b2c3d4/endpoints\" -H \"Authorization: Bearer $TOKEN\"\n```\n\n```json\n{\n  \"endpoints\": [\n    { \"id\": \"ep-1111\", \"network_id\": \"net-a1b2c3d4\", \"ip\": \"10.20.0.1\", \"mac\": \"02:1a:4f:14:00:01\", \"state\": \"free\" },\n    { \"id\": \"ep-2222\", \"network_id\": \"net-a1b2c3d4\", \"ip\": \"10.20.0.2\", \"mac\": \"02:1a:4f:14:00:02\", \"state\": \"bound\", \"bound_at\": \"2026-06-30T12:05:00Z\" }\n  ]\n}\n```\n\nAdd `?mac=52:54:00:ab:01:02` to narrow the listing to a single address. This is\nthe efficient way to answer \"which endpoint is this workload on\" when\nreconciling — a /24 otherwise returns 254 rows. Matching ignores case, and an\naddress that is not present returns an empty list rather than an error.\n\n```bash\ncurl -k \"$NODE/api/v1/networks/net-a1b2c3d4/endpoints?mac=52:54:00:ab:01:02\" \\\n  -H \"Authorization: Bearer $TOKEN\"\n```\n\n### Attach an endpoint\n\n`POST /api/v1/networks/{id}/attach` — claims an endpoint and binds it. Body is\noptional: send `{\"ip\":\"10.20.0.2\"}` to claim a specific address, or an empty body\nto take the next free one. Idempotent — attaching an already-bound IP returns that\nsame endpoint. Returns **200**.\n\nAdd `\"mac\"` to give the endpoint a specific hardware address instead of the one\nit was assigned. Use this when the workload already has a fixed address of its\nown — a virtual machine image, or an appliance whose licence is tied to one — so\nthe fabric accepts the address it will actually send from.\n\n```bash\ncurl -k -X POST \"$NODE/api/v1/networks/net-a1b2c3d4/attach\" \\\n  -H \"Authorization: Bearer $TOKEN\" -H \"Content-Type: application/json\" \\\n  -d '{\"ip\":\"10.20.0.2\"}'\n\n# ...or claiming that address for a workload that already has this MAC\ncurl -k -X POST \"$NODE/api/v1/networks/net-a1b2c3d4/attach\" \\\n  -H \"Authorization: Bearer $TOKEN\" -H \"Content-Type: application/json\" \\\n  -d '{\"ip\":\"10.20.0.2\",\"mac\":\"52:54:00:ab:01:02\"}'\n```\n\n```json\n{\n  \"status\": \"attached\",\n  \"endpoint\": { \"id\": \"ep-2222\", \"network_id\": \"net-a1b2c3d4\", \"ip\": \"10.20.0.2\", \"mac\": \"02:1a:4f:14:00:02\", \"state\": \"bound\", \"bound_at\": \"2026-06-30T12:05:00Z\" }\n}\n```\n\n### Change an endpoint's MAC address\n\n`PUT /api/v1/networks/{id}/endpoints/{eid}/mac` — replaces the endpoint's\nhardware address, keeping its IP. Returns **200**.\n\nOn a bound endpoint the anti-spoof binding moves to the new address as part of\nthe change, so the workload is never left able to send from an address the fabric\nwould reject. Setting the address it already has succeeds and changes nothing.\n\n```bash\ncurl -k -X PUT \"$NODE/api/v1/networks/net-a1b2c3d4/endpoints/ep-2222/mac\" \\\n  -H \"Authorization: Bearer $TOKEN\" -H \"Content-Type: application/json\" \\\n  -d '{\"mac\":\"52:54:00:ab:01:02\"}'\n```\n\n```json\n{\n  \"status\": \"updated\",\n  \"endpoint\": { \"id\": \"ep-2222\", \"network_id\": \"net-a1b2c3d4\", \"ip\": \"10.20.0.2\", \"mac\": \"52:54:00:ab:01:02\", \"state\": \"bound\", \"bound_at\": \"2026-06-30T12:05:00Z\" }\n}\n```\n\n**400** is returned for an address that is not a MAC, for a multicast address\n(one can never be a source address, so traffic from it would be dropped), and for\nan address already held by another endpoint — the response names the endpoint\nholding it, since two endpoints sharing an address would collide in the\nanti-spoof binding.\n\n### Detach an endpoint\n\n`POST /api/v1/networks/{id}/endpoints/{eid}/detach` — frees the endpoint and\nreleases its binding.\n\n```bash\ncurl -k -X POST \"$NODE/api/v1/networks/net-a1b2c3d4/endpoints/ep-2222/detach\" \\\n  -H \"Authorization: Bearer $TOKEN\"\n```\n\n```json\n{\n  \"status\": \"detached\",\n  \"endpoint\": { \"id\": \"ep-2222\", \"network_id\": \"net-a1b2c3d4\", \"ip\": \"10.20.0.2\", \"mac\": \"02:1a:4f:14:00:02\", \"state\": \"free\" }\n}\n```\n\n---\n\n## IP address management (IPAM)\n\nAddress pools and the individual IP allocations drawn from them. Creating a\nprivate network registers a matching pool automatically; you can also manage\npools directly.\n\n**Pool fields:** `id`, `name` (**required**), `subnet` (**required** CIDR),\n`gateway`, `range_start`, `range_end`, `is_ipv6`, and an optional `tenant_id`.\n\n### List pools\n\n`GET /api/v1/ipam/pools` — optional `?tenant_id=` filter.\n\n```bash\ncurl -k \"$NODE/api/v1/ipam/pools\" -H \"Authorization: Bearer $TOKEN\"\n```\n\n```json\n{\n  \"pools\": [\n    { \"id\": 1, \"name\": \"app-net\", \"tenant_id\": \"t-acme\", \"subnet\": \"10.20.0.0/24\", \"gateway\": \"10.20.0.1\", \"range_start\": \"10.20.0.1\", \"range_end\": \"10.20.0.254\", \"is_ipv6\": false }\n  ]\n}\n```\n\n### Create a pool\n\n`POST /api/v1/ipam/pools` — `name` and `subnet` required; `gateway`,\n`range_start`, `range_end`, `is_ipv6`, `tenant_id` optional. Returns **201**.\n\n```bash\ncurl -k -X POST \"$NODE/api/v1/ipam/pools\" \\\n  -H \"Authorization: Bearer $TOKEN\" -H \"Content-Type: application/json\" \\\n  -d '{\"name\":\"edge\",\"subnet\":\"10.40.0.0/24\",\"gateway\":\"10.40.0.1\",\"range_start\":\"10.40.0.10\",\"range_end\":\"10.40.0.200\"}'\n```\n\n```json\n{\n  \"status\": \"created\",\n  \"pool\": { \"id\": 2, \"name\": \"edge\", \"subnet\": \"10.40.0.0/24\", \"gateway\": \"10.40.0.1\", \"range_start\": \"10.40.0.10\", \"range_end\": \"10.40.0.200\", \"is_ipv6\": false }\n}\n```\n\n### List allocations\n\n`GET /api/v1/ipam/allocations` — every allocation across all pools; optional\n`?tenant_id=` filter.\n\n```bash\ncurl -k \"$NODE/api/v1/ipam/allocations\" -H \"Authorization: Bearer $TOKEN\"\n```\n\n```json\n{ \"allocations\": [ { \"id\": 10, \"pool_id\": 1, \"ip\": \"10.20.0.5\", \"hostname\": \"web-1\" } ] }\n```\n\n### Allocate an IP\n\n`POST /api/v1/ipam/allocate` — body: `pool_id` (**required**), `hostname`\n(optional). Returns the assigned address.\n\n```bash\ncurl -k -X POST \"$NODE/api/v1/ipam/allocate\" \\\n  -H \"Authorization: Bearer $TOKEN\" -H \"Content-Type: application/json\" \\\n  -d '{\"pool_id\":1,\"hostname\":\"web-2\"}'\n```\n\n```json\n{ \"status\": \"allocated\", \"id\": 11, \"pool_id\": 1, \"ip\": \"10.20.0.6\", \"hostname\": \"web-2\" }\n```\n\n### Release an IP\n\n`POST /api/v1/ipam/release` — body: `pool_id` and `ip` (**both required**).\n\n```bash\ncurl -k -X POST \"$NODE/api/v1/ipam/release\" \\\n  -H \"Authorization: Bearer $TOKEN\" -H \"Content-Type: application/json\" \\\n  -d '{\"pool_id\":1,\"ip\":\"10.20.0.6\"}'\n```\n\n```json\n{ \"status\": \"released\" }\n```\n\n### Delete a pool\n\n`DELETE /api/v1/ipam/pools/{id}` — removes the pool **and every allocation in it**.\nAddresses handed out from the pool stop being reserved, so do this only once\nnothing is using them.\n\n```bash\ncurl -k -X DELETE \"$NODE/api/v1/ipam/pools/1\" -H \"Authorization: Bearer $TOKEN\"\n```\n\n```json\n{ \"status\": \"deleted\", \"id\": 1 }\n```\n\nDeleting a pool that does not exist returns **404**.\n\n---\n\n## Bridges\n\nThe node's software bridges and their member ports. A node ships with two managed\nbridges — `cnv-mgmt-br0` (management) and `cnv-user-br0` (tenant/user traffic) —\nand you can create and wire additional ones.\n\n| Method | Path | Purpose |\n|---|---|---|\n| GET | `/api/v1/bridges` | List bridges and their member interfaces |\n| POST | `/api/v1/bridges` | Create a bridge (`name` required) |\n| DELETE | `/api/v1/bridges/{name}` | Delete a bridge |\n| POST | `/api/v1/bridges/{name}/ports` | Attach an interface (`interface` required) |\n| DELETE | `/api/v1/bridges/{name}/ports/{iface}` | Detach an interface |\n\n```bash\ncurl -k \"$NODE/api/v1/bridges\" -H \"Authorization: Bearer $TOKEN\"\n```\n\n```json\n{ \"bridges\": [ { \"name\": \"cnv-user-br0\", \"interfaces\": [\"cnv-nic-1\"] } ] }\n```\n\n```bash\n# Create a bridge and attach a NIC\ncurl -k -X POST \"$NODE/api/v1/bridges\" \\\n  -H \"Authorization: Bearer $TOKEN\" -H \"Content-Type: application/json\" \\\n  -d '{\"name\":\"cnv-svc-br0\"}'\n\ncurl -k -X POST \"$NODE/api/v1/bridges/cnv-svc-br0/ports\" \\\n  -H \"Authorization: Bearer $TOKEN\" -H \"Content-Type: application/json\" \\\n  -d '{\"interface\":\"cnv-nic-2\"}'\n```\n\n```json\n{ \"status\": \"added\", \"bridge\": \"cnv-svc-br0\", \"interface\": \"cnv-nic-2\" }\n```\n\nDetach a port → `DELETE /api/v1/bridges/cnv-svc-br0/ports/cnv-nic-2` returns\n`{ \"status\": \"removed\", \"bridge\": \"cnv-svc-br0\", \"interface\": \"cnv-nic-2\" }`.\nDelete a bridge → `{ \"status\": \"deleted\", \"name\": \"cnv-svc-br0\" }`.\n\n---\n\n## Network interfaces (NICs)\n\nRead-only inventory of the node's physical interfaces. Renaming is a privileged\nboot-time operation and is not exposed over the API.\n\n`GET /api/v1/nics` — pass `?refresh=1` to re-scan hardware before returning.\n\n```bash\ncurl -k \"$NODE/api/v1/nics\" -H \"Authorization: Bearer $TOKEN\"\n```\n\n```json\n{\n  \"nics\": [\n    { \"original_name\": \"eth0\", \"stratum_name\": \"cnv-nic-0\", \"pci\": \"0000:01:00.0\", \"driver\": \"ixgbe\", \"speed_mbps\": 10000, \"status\": \"up\" }\n  ]\n}\n```\n\n---\n\n## MAC bindings\n\nTie a MAC address to an authorized port/VLAN for the node's anti-spoofing guard.\nManaged-network endpoints get their bindings automatically; use these endpoints to\nmanage bindings for addresses you bridge in from outside.\n\n**Binding fields:** `id`, `mac`, `port_id`, `vlan_id`, and `mode` — `hard` (drop\ntraffic from unbound MACs; the default) or `soft` (log but allow).\n\n| Method | Path | Purpose |\n|---|---|---|\n| GET | `/api/v1/macbind` | List bindings |\n| POST | `/api/v1/macbind` | Create a binding (`mac` required) |\n| DELETE | `/api/v1/macbind/{mac}` | Remove a binding by MAC |\n\n```bash\ncurl -k -X POST \"$NODE/api/v1/macbind\" \\\n  -H \"Authorization: Bearer $TOKEN\" -H \"Content-Type: application/json\" \\\n  -d '{\"mac\":\"02:1a:4f:14:00:09\",\"port_id\":1,\"vlan_id\":100,\"mode\":\"hard\"}'\n```\n\n```json\n{ \"status\": \"created\", \"binding\": { \"id\": 4, \"mac\": \"02:1a:4f:14:00:09\", \"port_id\": 1, \"vlan_id\": 100, \"mode\": \"hard\" } }\n```\n\nRemove → `DELETE /api/v1/macbind/02:1a:4f:14:00:09` returns\n`{ \"status\": \"deleted\", \"mac\": \"02:1a:4f:14:00:09\" }`.\n\n---\n\n## DHCP leases\n\nRead the live DHCP lease table. *(Plan feature: DHCP.)*\n\n`GET /api/v1/dhcp/leases` — optional `?state=` filter, one of `active`,\n`expired`, `revoked`. `expires_at` is a Unix UTC timestamp.\n\n```bash\ncurl -k \"$NODE/api/v1/dhcp/leases?state=active\" -H \"Authorization: Bearer $TOKEN\"\n```\n\n```json\n{\n  \"leases\": [\n    { \"id\": 3, \"mac\": \"52:54:00:de:ad:01\", \"ip\": \"10.30.0.10\", \"state\": \"active\", \"hostname\": \"db-primary\", \"expires_at\": 1782825600 }\n  ]\n}\n```\n\n> Static reservations and early releases are managed from the CLI\n> (`cenvero-str-ctl dhcp reserve` / `dhcp release`).\n\n---\n\n## DNS\n\nThe authoritative DNS manager: managed zones and their records, plus DNSSEC\nmaterial. *(Plan feature: DNS.)*\n\n**Record fields:** `id`, `zone_id`, `name`, `type` (`A`, `AAAA`, `PTR`, `CNAME`,\n`MX`, `TXT`, `NS`, `SOA`, `SRV`), `value`, `ttl` (defaults to `300` when omitted),\nand an optional `source_subnet` for split-horizon answers.\n\n### List zones\n\n`GET /api/v1/dns/zones`\n\n```bash\ncurl -k \"$NODE/api/v1/dns/zones\" -H \"Authorization: Bearer $TOKEN\"\n```\n\n```json\n{ \"zones\": [ { \"id\": 1, \"name\": \"app-net.internal.\", \"soa\": \"ns.app-net.internal.\", \"serial\": 2026063001 } ] }\n```\n\n### Create a zone\n\n`POST /api/v1/dns/zones` — body: `name` (**required**). Returns **201**.\n\n```bash\ncurl -k -X POST \"$NODE/api/v1/dns/zones\" \\\n  -H \"Authorization: Bearer $TOKEN\" -H \"Content-Type: application/json\" \\\n  -d '{\"name\":\"app-net.internal\"}'\n```\n\n```json\n{ \"status\": \"created\", \"zone\": { \"id\": 1, \"name\": \"app-net.internal.\", \"soa\": \"ns.app-net.internal.\", \"serial\": 2026063001 } }\n```\n\n### Delete a zone\n\n`DELETE /api/v1/dns/zones/{id}` — removes the zone **and every record in it**.\nNames under the zone stop resolving immediately.\n\n```bash\ncurl -k -X DELETE \"$NODE/api/v1/dns/zones/1\" -H \"Authorization: Bearer $TOKEN\"\n```\n\n```json\n{ \"status\": \"deleted\", \"id\": 1 }\n```\n\nDeleting a zone that does not exist returns **404**.\n\n### List records\n\n`GET /api/v1/dns/records` — lists all records, or one zone's with `?zone_id=N`.\n\n```bash\ncurl -k \"$NODE/api/v1/dns/records?zone_id=1\" -H \"Authorization: Bearer $TOKEN\"\n```\n\n```json\n{ \"records\": [ { \"id\": 5, \"zone_id\": 1, \"name\": \"api\", \"type\": \"A\", \"value\": \"10.20.0.55\", \"ttl\": 300 } ] }\n```\n\n### Create a record\n\n`POST /api/v1/dns/records` — body: `zone_id`, `name`, `type`, `value` required;\n`ttl` and `source_subnet` optional. Returns **201**.\n\n```bash\ncurl -k -X POST \"$NODE/api/v1/dns/records\" \\\n  -H \"Authorization: Bearer $TOKEN\" -H \"Content-Type: application/json\" \\\n  -d '{\"zone_id\":1,\"name\":\"db\",\"type\":\"A\",\"value\":\"10.20.0.10\",\"ttl\":300}'\n```\n\n```json\n{ \"status\": \"created\", \"record\": { \"id\": 6, \"zone_id\": 1, \"name\": \"db\", \"type\": \"A\", \"value\": \"10.20.0.10\", \"ttl\": 300 } }\n```\n\n### Update a record\n\n`PUT /api/v1/dns/records/{id}` — **in-place update is not supported.** The call\nreturns **501**; delete the record and recreate it instead.\n\n```json\n{ \"error\": \"updating a record in place is not supported; delete and recreate it\" }\n```\n\n### Delete a record\n\n`DELETE /api/v1/dns/records/{id}`\n\n```bash\ncurl -k -X DELETE \"$NODE/api/v1/dns/records/6\" -H \"Authorization: Bearer $TOKEN\"\n```\n\n```json\n{ \"status\": \"deleted\", \"record_id\": 6 }\n```\n\n### DNSSEC material for a zone\n\n`GET /api/v1/dns/dnssec/{zone}` — returns the zone's DNSKEY set and the **DS**\nrecord to publish in the parent zone. Keys are generated and persisted on the\nfirst call, so this is also how you enable DNSSEC for a zone.\n\n```bash\ncurl -k \"$NODE/api/v1/dns/dnssec/app-net.internal\" -H \"Authorization: Bearer $TOKEN\"\n```\n\n```json\n{\n  \"zone\": \"app-net.internal.\",\n  \"enabled\": true,\n  \"dnskey\": [\n    \"app-net.internal.\\t3600\\tIN\\tDNSKEY\\t257 3 15 <base64-public-key>\",\n    \"app-net.internal.\\t3600\\tIN\\tDNSKEY\\t256 3 15 <base64-public-key>\"\n  ],\n  \"ds\": \"app-net.internal.\\t3600\\tIN\\tDS\\t12345 15 2 <hex-digest>\"\n}\n```\n\n---\n\n## Static & policy routing\n\nTwo related surfaces: **routes** placed in the kernel forwarding table (optionally\na non-main table id), and the **policy rules** that steer matched traffic into a\ntable.\n\n**Route fields:** `destination` (**required** CIDR), `gateway`, `interface`,\n`metric`, `table` (`0` = the main table; use a positive id for policy routing).\n\n### List routes\n\n`GET /api/v1/routes` — optional `?table=N` filter.\n\n```bash\ncurl -k \"$NODE/api/v1/routes\" -H \"Authorization: Bearer $TOKEN\"\n```\n\n```json\n{ \"routes\": [ { \"id\": 1, \"destination\": \"10.50.0.0/24\", \"gateway\": \"10.20.0.254\", \"interface\": \"\", \"metric\": 100, \"table\": 0 } ] }\n```\n\n### Create a route\n\n`POST /api/v1/routes` — `destination` required; `gateway`, `interface`, `metric`,\n`table` optional (`table` must be `>= 0`). Returns **201**.\n\n```bash\ncurl -k -X POST \"$NODE/api/v1/routes\" \\\n  -H \"Authorization: Bearer $TOKEN\" -H \"Content-Type: application/json\" \\\n  -d '{\"destination\":\"0.0.0.0/0\",\"gateway\":\"203.0.113.1\",\"table\":100}'\n```\n\n```json\n{ \"status\": \"created\", \"route\": { \"id\": 2, \"destination\": \"0.0.0.0/0\", \"gateway\": \"203.0.113.1\", \"interface\": \"\", \"metric\": 0, \"table\": 100 } }\n```\n\n### Delete a route\n\n`DELETE /api/v1/routes/{id}` → `{ \"status\": \"deleted\", \"id\": 2 }`.\n\n### List policy rules\n\n`GET /api/v1/routes/rules` — the rules that direct matched traffic into a table.\n\n```bash\ncurl -k \"$NODE/api/v1/routes/rules\" -H \"Authorization: Bearer $TOKEN\"\n```\n\n```json\n{ \"rules\": [ { \"id\": 1, \"priority\": 100, \"from\": \"10.20.0.0/24\", \"to\": \"\", \"fwmark\": 0, \"iif\": \"\", \"oif\": \"\", \"table\": 100 } ] }\n```\n\n### Create a policy rule\n\n`POST /api/v1/routes/rules` — `table` must be a **positive** id, and at least one\nselector (`from`, `to`, `fwmark`, `iif`, `oif`) is required. `priority` optional.\nReturns **201**.\n\n```bash\ncurl -k -X POST \"$NODE/api/v1/routes/rules\" \\\n  -H \"Authorization: Bearer $TOKEN\" -H \"Content-Type: application/json\" \\\n  -d '{\"priority\":100,\"from\":\"10.20.0.0/24\",\"table\":100}'\n```\n\n```json\n{ \"status\": \"created\", \"rule\": { \"id\": 1, \"priority\": 100, \"from\": \"10.20.0.0/24\", \"to\": \"\", \"fwmark\": 0, \"iif\": \"\", \"oif\": \"\", \"table\": 100 } }\n```\n\n### Delete a policy rule\n\n`DELETE /api/v1/routes/rules/{id}` → `{ \"status\": \"deleted\", \"id\": 1 }`.\n\n---\n\n## Port forwarding\n\nForward an inbound public address:port to an internal target (destination NAT).\nThis is how you publish an internal service on a public address.\n\n| Method | Path | Purpose |\n|---|---|---|\n| POST | `/api/v1/forward` | Create a port-forward rule |\n| DELETE | `/api/v1/forward/{id}` | Delete a port-forward rule |\n| GET | `/api/v1/forward`, `/api/v1/forward/{id}` | *Not supported* — returns `501` |\n\n**Create** — body: `dest_ip`, `dest_port` (the inbound match), `target_ip`,\n`target_port` (the internal target); all four required. Returns **201**.\n\n```bash\ncurl -k -X POST \"$NODE/api/v1/forward\" \\\n  -H \"Authorization: Bearer $TOKEN\" -H \"Content-Type: application/json\" \\\n  -d '{\"dest_ip\":\"203.0.113.10\",\"dest_port\":443,\"target_ip\":\"10.20.0.10\",\"target_port\":8443}'\n```\n\n```json\n{\n  \"status\": \"created\",\n  \"port_forward\": { \"id\": 5, \"dest_ip\": \"203.0.113.10\", \"dest_port\": 443, \"target_ip\": \"10.20.0.10\", \"target_port\": 8443 }\n}\n```\n\nDelete → `DELETE /api/v1/forward/5` returns `{ \"status\": \"deleted\", \"id\": 5 }`.\nListing/fetching individual rules is not supported and returns **501**; track\nyour rule ids from the create response.\n\n---\n\n## VLAN lockdown\n\nA per-VLAN allow/deny table for the tenant network. An **empty table means every\nVLAN is allowed** (the default). Add a row with `allowed: false` to lock a VLAN\ndown; `allowed: true` records an explicit allow.\n\n### List VLAN policies\n\n`GET /api/v1/vlan`\n\n```bash\ncurl -k \"$NODE/api/v1/vlan\" -H \"Authorization: Bearer $TOKEN\"\n```\n\n```json\n{ \"vlans\": [ { \"vlan_id\": 200, \"allowed\": false } ] }\n```\n\n### Set a VLAN policy\n\n`POST /api/v1/vlan` — body: `vlan_id` (**required**, 1–4094) and `allowed`.\nReturns **201**.\n\n```bash\ncurl -k -X POST \"$NODE/api/v1/vlan\" \\\n  -H \"Authorization: Bearer $TOKEN\" -H \"Content-Type: application/json\" \\\n  -d '{\"vlan_id\":200,\"allowed\":false}'\n```\n\n```json\n{ \"status\": \"set\", \"vlan\": { \"vlan_id\": 200, \"allowed\": false } }\n```\n\n### Clear a VLAN policy\n\n`DELETE /api/v1/vlan/{id}` — removes the row, returning that VLAN to the default.\n\n```bash\ncurl -k -X DELETE \"$NODE/api/v1/vlan/200\" -H \"Authorization: Bearer $TOKEN\"\n```\n\n```json\n{ \"status\": \"cleared\", \"vlan_id\": 200 }\n```\n\n---\n\n## VXLAN overlays\n\nLayer-2 overlay networks keyed by **VNI** (a 24-bit id, 1–16777215) plus their\nremote peers (VTEPs). *(Plan feature: VXLAN.)*\n\n### List overlay networks\n\n`GET /api/v1/vxlan/networks`\n\n```bash\ncurl -k \"$NODE/api/v1/vxlan/networks\" -H \"Authorization: Bearer $TOKEN\"\n```\n\n```json\n{\n  \"vxlan_networks\": [\n    { \"vni\": 1001, \"subnet\": \"10.200.0.0/24\", \"peers\": [ { \"host\": \"node-b\", \"mac\": \"\", \"vtep_ip\": \"198.51.100.20\" } ] }\n  ]\n}\n```\n\n### Create an overlay network\n\n`POST /api/v1/vxlan/networks` — body: `vni` (**required**, 1–16777215) and\n`subnet`. Returns **201**.\n\n```bash\ncurl -k -X POST \"$NODE/api/v1/vxlan/networks\" \\\n  -H \"Authorization: Bearer $TOKEN\" -H \"Content-Type: application/json\" \\\n  -d '{\"vni\":1001,\"subnet\":\"10.200.0.0/24\"}'\n```\n\n```json\n{ \"status\": \"created\", \"vni\": 1001, \"subnet\": \"10.200.0.0/24\" }\n```\n\n### Delete an overlay network\n\n`DELETE /api/v1/vxlan/networks/{vni}` → `{ \"status\": \"deleted\", \"vni\": 1001 }`.\n\n### List peers\n\n`GET /api/v1/vxlan/networks/{vni}/peers`\n\n```json\n{ \"vni\": 1001, \"peers\": [ { \"host\": \"node-b\", \"mac\": \"\", \"vtep_ip\": \"198.51.100.20\" } ] }\n```\n\n### Add a peer\n\n`POST /api/v1/vxlan/networks/{vni}/peers` — body: `host` and `vtep_ip`\n(**both required**), `mac` optional. Returns **201**.\n\n```bash\ncurl -k -X POST \"$NODE/api/v1/vxlan/networks/1001/peers\" \\\n  -H \"Authorization: Bearer $TOKEN\" -H \"Content-Type: application/json\" \\\n  -d '{\"host\":\"node-b\",\"vtep_ip\":\"198.51.100.20\"}'\n```\n\n```json\n{ \"status\": \"added\", \"vni\": 1001, \"host\": \"node-b\" }\n```\n\n### Remove a peer\n\n`DELETE /api/v1/vxlan/networks/{vni}/peers/{host}` →\n`{ \"status\": \"removed\", \"vni\": 1001, \"host\": \"node-b\" }`.\n\n### Forwarding database\n\n`GET /api/v1/vxlan/fdb` — the overlay MAC-to-VTEP forwarding table.\n\n```json\n{ \"fdb\": [ { \"mac\": \"02:1a:4f:c8:00:05\", \"vni\": 1001, \"vtep_ip\": \"198.51.100.20\" } ] }\n```\n\n---\n\n## Firewall rules & connection tracking\n\nManage the node's firewall rule table. Each rule matches on chain, protocol,\nsource/destination IP and port, ingress interface, and optionally a source MAC,\nand applies an action. Setting `stateful: true` on a rule enables connection\ntracking for that rule (return traffic of established connections is matched\nautomatically). To *view* tracked connections, see the **Traffic visibility →\nFlows** section below.\n\n| Method | Path | Purpose |\n|---|---|---|\n| GET | `/api/v1/rules` | List rules (optional `?chain=` filter) |\n| POST | `/api/v1/rules` | Create a rule |\n| GET | `/api/v1/rules/{id}` | Get one rule |\n| PUT | `/api/v1/rules/{id}` | Replace a rule (a new `id` is assigned) |\n| DELETE | `/api/v1/rules/{id}` | Delete a rule |\n\n**Rule fields**\n\n| Field | Type | Notes |\n|---|---|---|\n| `id` | integer | Assigned by the node (response only) |\n| `chain` | string | **Required.** `input`, `output`, `forward`, `prerouting`, `postrouting` |\n| `action` | string | **Required.** `accept`, `drop`, `reject`, `log` |\n| `priority` | integer | Evaluation priority |\n| `protocol` | string | e.g. `tcp`, `udp`, `icmp` (omit for any) |\n| `source_ip` / `dest_ip` | string | Address or CIDR |\n| `source_port` / `dest_port` | integer | |\n| `stateful` | boolean | Enable connection tracking for this rule |\n| `interface` | string | Bind the rule to one ingress device (e.g. `cnv-user-br0`) |\n| `mac` | string | Match a source MAC (`aa:bb:cc:dd:ee:ff`); omit for any |\n| `level` | string | Policy scope: `global` (default), `bridge`, `vlan`, `mac`, `flow`, `private_network` |\n\nEmpty/zero optional fields are omitted from responses; `id`, `chain`, `priority`,\n`action`, and `stateful` always appear.\n\n**Create a rule**\n\n```bash\ncurl -k -X POST \"$NODE/api/v1/rules\" \\\n  -H \"Authorization: Bearer $TOKEN\" -H \"Content-Type: application/json\" \\\n  -d '{\n    \"chain\": \"forward\",\n    \"action\": \"drop\",\n    \"priority\": 100,\n    \"protocol\": \"tcp\",\n    \"source_ip\": \"198.51.100.0/24\",\n    \"dest_port\": 22,\n    \"stateful\": true,\n    \"comment\": \"block ssh from that net\"\n  }'\n```\n\n```json\n{\n  \"status\": \"created\",\n  \"rule\": { \"id\": 7, \"chain\": \"forward\", \"priority\": 100, \"protocol\": \"tcp\", \"source_ip\": \"198.51.100.0/24\", \"dest_port\": 22, \"action\": \"drop\", \"comment\": \"block ssh from that net\", \"stateful\": true }\n}\n```\n\n**List rules** (optionally filter by chain):\n\n```bash\ncurl -k \"$NODE/api/v1/rules?chain=forward\" -H \"Authorization: Bearer $TOKEN\"\n```\n\n```json\n{ \"rules\": [ { \"id\": 7, \"chain\": \"forward\", \"priority\": 100, \"protocol\": \"tcp\", \"source_ip\": \"198.51.100.0/24\", \"dest_port\": 22, \"action\": \"drop\", \"comment\": \"block ssh from that net\", \"stateful\": true } ] }\n```\n\n**Replace a rule.** There is no in-place edit; `PUT` deletes the old rule and\ninserts the replacement, which receives a **new** `id` (returned in the response).\nThe body uses the same fields as create (`chain` and `action` required).\n\n```bash\ncurl -k -X PUT \"$NODE/api/v1/rules/7\" \\\n  -H \"Authorization: Bearer $TOKEN\" -H \"Content-Type: application/json\" \\\n  -d '{\"chain\":\"forward\",\"action\":\"reject\",\"priority\":100,\"protocol\":\"tcp\",\"source_ip\":\"198.51.100.0/24\",\"dest_port\":22}'\n```\n\n```json\n{ \"status\": \"updated\", \"rule\": { \"id\": 8, \"chain\": \"forward\", \"priority\": 100, \"protocol\": \"tcp\", \"source_ip\": \"198.51.100.0/24\", \"dest_port\": 22, \"action\": \"reject\", \"stateful\": false } }\n```\n\n**Delete a rule:** `DELETE /api/v1/rules/8` → `{ \"status\": \"deleted\", \"id\": 8 }`.\n\n---\n\n## Load balancer\n\nL4 virtual IPs (VIPs) with a pool of backends. Create a VIP, attach/detach\nbackends, override backend health, and optionally configure an active health\ncheck. *(Plan feature: Load Balancer.)*\n\n| Method | Path | Purpose |\n|---|---|---|\n| GET | `/api/v1/lb` | List VIPs |\n| POST | `/api/v1/lb` | Create a VIP |\n| GET | `/api/v1/lb/{id}` | Get one VIP |\n| PUT | `/api/v1/lb/{id}` | *Not supported* — returns `501` |\n| DELETE | `/api/v1/lb/{id}` | Delete a VIP |\n| POST | `/api/v1/lb/{id}/backends` | Add a backend |\n| DELETE | `/api/v1/lb/{id}/backends/{bid}` | Remove a backend |\n| POST | `/api/v1/lb/{id}/backends/{bid}/health` | Manually set a backend up/down |\n\n**VIP fields** — `id`, `frontend_ip`, and `algorithm` are **required** on create.\n\n| Field | Type | Notes |\n|---|---|---|\n| `id` | string | Your VIP identifier |\n| `frontend_ip` | string | The virtual IP |\n| `frontend_port` | integer | |\n| `protocol` | string | `tcp` or `udp` |\n| `algorithm` | string | `round-robin`, `least-conn`, `source-hash`, `weighted`, `maglev`, `consistent-hash` |\n| `dsr_enabled` | boolean | Direct server return |\n| `health_check` | object | Optional active check (see below) |\n\n`health_check`: `type` (`tcp` or `http`; empty disables it), `interval_sec`,\n`timeout_sec`, `threshold`, `http_path`.\n\n**Create a VIP with an HTTP health check:**\n\n```bash\ncurl -k -X POST \"$NODE/api/v1/lb\" \\\n  -H \"Authorization: Bearer $TOKEN\" -H \"Content-Type: application/json\" \\\n  -d '{\n    \"id\": \"web-vip\",\n    \"frontend_ip\": \"203.0.113.10\",\n    \"frontend_port\": 443,\n    \"protocol\": \"tcp\",\n    \"algorithm\": \"round-robin\",\n    \"dsr_enabled\": false,\n    \"health_check\": { \"type\": \"http\", \"interval_sec\": 5, \"timeout_sec\": 2, \"threshold\": 3, \"http_path\": \"/healthz\" }\n  }'\n```\n\n```json\n{\n  \"status\": \"created\",\n  \"load_balancer\": { \"id\": \"web-vip\", \"frontend_ip\": \"203.0.113.10\", \"frontend_port\": 443, \"protocol\": \"tcp\", \"algorithm\": \"round-robin\", \"dsr_enabled\": false, \"backends\": [] }\n}\n```\n\n**Add a backend** (`id` and `ip` required; `weight` drives the `weighted`\nalgorithm's share):\n\n```bash\ncurl -k -X POST \"$NODE/api/v1/lb/web-vip/backends\" \\\n  -H \"Authorization: Bearer $TOKEN\" -H \"Content-Type: application/json\" \\\n  -d '{\"id\":\"app-1\",\"ip\":\"10.0.0.11\",\"port\":443,\"weight\":1}'\n```\n\n```json\n{ \"status\": \"added\", \"vip_id\": \"web-vip\", \"backend_id\": \"app-1\" }\n```\n\n**Get a VIP** (shows backends with live health and active connection counts):\n\n```json\n{\n  \"load_balancer\": {\n    \"id\": \"web-vip\", \"frontend_ip\": \"203.0.113.10\", \"frontend_port\": 443, \"protocol\": \"tcp\", \"algorithm\": \"round-robin\", \"dsr_enabled\": false,\n    \"backends\": [ { \"id\": \"app-1\", \"ip\": \"10.0.0.11\", \"port\": 443, \"weight\": 1, \"healthy\": true, \"active_conns\": 12 } ]\n  }\n}\n```\n\n**Override a backend's health** (a configured active check may flip it back on the\nnext probe):\n\n```bash\ncurl -k -X POST \"$NODE/api/v1/lb/web-vip/backends/app-1/health\" \\\n  -H \"Authorization: Bearer $TOKEN\" -H \"Content-Type: application/json\" \\\n  -d '{\"healthy\": false}'\n```\n\n```json\n{ \"status\": \"set\", \"vip_id\": \"web-vip\", \"backend_id\": \"app-1\", \"healthy\": false }\n```\n\n**Remove a backend** → `{ \"status\": \"removed\", \"vip_id\": \"web-vip\", \"backend_id\": \"app-1\" }`.\n**Delete a VIP** → `{ \"status\": \"deleted\", \"id\": \"web-vip\" }`.\n\nA VIP cannot be edited in place. `PUT /api/v1/lb/{id}` returns `501`:\n\n```json\n{ \"error\": \"updating a VIP in place is not supported; delete and recreate it\" }\n```\n\n---\n\n## BGP & route filtering\n\nPeer with upstream routers, advertise and withdraw prefixes, and shape what you\naccept/announce with prefix-lists, route-maps, and per-neighbor import/export\npolicy. *(Plan feature: BGP.)*\n\n| Method | Path | Purpose |\n|---|---|---|\n| GET | `/api/v1/bgp/status` | Engine summary |\n| GET | `/api/v1/bgp/neighbors` | List peers |\n| POST | `/api/v1/bgp/neighbors` | Add a peer |\n| DELETE | `/api/v1/bgp/neighbors/{addr}` | Remove a peer |\n| GET | `/api/v1/bgp/routes` | Route table (`?family=ipv4/ipv6`) |\n| POST | `/api/v1/bgp/announce` | Advertise a prefix |\n| POST | `/api/v1/bgp/withdraw` | Withdraw a prefix |\n| GET | `/api/v1/bgp/prefix-lists` | List prefix-lists |\n| POST | `/api/v1/bgp/prefix-lists` | Create a prefix-list |\n| GET | `/api/v1/bgp/route-maps` | List route-maps |\n| POST | `/api/v1/bgp/route-maps` | Create a route-map |\n| GET | `/api/v1/bgp/policy` | Show import/export policy bindings |\n| POST | `/api/v1/bgp/policy/{dir}` | Bind a route-map (`{dir}` = `import` or `export`) |\n\n**Status:**\n\n```bash\ncurl -k \"$NODE/api/v1/bgp/status\" -H \"Authorization: Bearer $TOKEN\"\n```\n\n```json\n{ \"status\": \"running\", \"neighbors\": 2, \"routes\": 17, \"bfd_sessions_up\": 2 }\n```\n\n**Add a peer.** `peer_addr`, `peer_as`, and `local_as` are **required**. Optional:\n`hold_time`, `keepalive_interval`, `md5_key` (TCP-MD5 auth; never returned in\nreads), and BFD (`bfd_enabled`, `bfd_interval_ms`, `bfd_multiplier` — the\nfailure-detection time is interval × multiplier).\n\n```bash\ncurl -k -X POST \"$NODE/api/v1/bgp/neighbors\" \\\n  -H \"Authorization: Bearer $TOKEN\" -H \"Content-Type: application/json\" \\\n  -d '{\n    \"peer_addr\": \"192.0.2.1\", \"peer_as\": 64512, \"local_as\": 64513,\n    \"hold_time\": 90, \"keepalive_interval\": 30,\n    \"bfd_enabled\": true, \"bfd_interval_ms\": 250, \"bfd_multiplier\": 3\n  }'\n```\n\n**List peers** (`state` is one of `Idle`, `Connect`, `Active`, `OpenSent`,\n`OpenConfirm`, `Established`):\n\n```json\n{\n  \"neighbors\": [\n    {\n      \"id\": \"192.0.2.1\", \"peer_as\": 64512, \"local_as\": 64513, \"peer_addr\": \"192.0.2.1\",\n      \"state\": \"Established\", \"hold_time\": 90, \"keepalive_interval\": 30,\n      \"bfd_enabled\": true, \"bfd_interval_ms\": 250, \"bfd_multiplier\": 3,\n      \"last_state_change\": \"2026-06-30T11:58:02Z\", \"messages_in\": 412, \"messages_out\": 410,\n      \"graceful_restart\": false, \"bfd_status\": \"Up\", \"bfd_detect_time_ms\": 750, \"peer_graceful_restart\": false\n    }\n  ]\n}\n```\n\nRemove a peer → `DELETE /api/v1/bgp/neighbors/192.0.2.1` returns\n`{ \"status\": \"deleted\", \"addr\": \"192.0.2.1\" }`.\n\n**Advertise a prefix** (`prefix` required, valid CIDR; `next_hop` and\n`communities` optional):\n\n```bash\ncurl -k -X POST \"$NODE/api/v1/bgp/announce\" \\\n  -H \"Authorization: Bearer $TOKEN\" -H \"Content-Type: application/json\" \\\n  -d '{\"prefix\":\"203.0.113.0/24\",\"next_hop\":\"192.0.2.254\",\"communities\":[\"64512:100\"]}'\n```\n\n```json\n{ \"status\": \"announced\", \"prefix\": \"203.0.113.0/24\" }\n```\n\n**Withdraw a prefix:** `POST /api/v1/bgp/withdraw` with `{\"prefix\":\"203.0.113.0/24\"}`\nreturns `{ \"status\": \"withdrawn\", \"prefix\": \"203.0.113.0/24\" }`.\n\n`GET /api/v1/bgp/routes` returns the current route table as\n`{ \"routes\": [ ... ], \"family\": \"ipv4\" }`. Each route carries its next hop, AS\npath, communities, local-preference, MED, and origin.\n\n### Prefix-lists\n\nA named, ordered list of CIDR matchers used by route-maps. `name` is **required**;\neach entry has `prefix` (CIDR), `action` (`allow` or `deny`), and optional\n`ge`/`le` prefix-length bounds.\n\n```bash\ncurl -k -X POST \"$NODE/api/v1/bgp/prefix-lists\" \\\n  -H \"Authorization: Bearer $TOKEN\" -H \"Content-Type: application/json\" \\\n  -d '{\n    \"name\": \"customer-routes\",\n    \"entries\": [\n      { \"prefix\": \"203.0.113.0/24\", \"action\": \"allow\", \"ge\": 24, \"le\": 32 },\n      { \"prefix\": \"0.0.0.0/0\",       \"action\": \"deny\" }\n    ]\n  }'\n```\n\n```json\n{ \"status\": \"created\", \"prefix_list\": \"customer-routes\" }\n```\n\n`GET /api/v1/bgp/prefix-lists` returns `{ \"prefix_lists\": [ ... ] }`, each with its\n`name` and `entries`.\n\n### Route-maps\n\nA named, sequenced policy that matches routes and sets attributes. `name` is\n**required**; each entry has `seq`, `action` (`allow`/`deny`), `match_prefix` (a\nprefix-list name), and optional `set_local_pref`, `set_community`, `set_med`.\n\n```bash\ncurl -k -X POST \"$NODE/api/v1/bgp/route-maps\" \\\n  -H \"Authorization: Bearer $TOKEN\" -H \"Content-Type: application/json\" \\\n  -d '{\n    \"name\": \"prefer-customer\",\n    \"entries\": [ { \"seq\": 10, \"action\": \"allow\", \"match_prefix\": \"customer-routes\", \"set_local_pref\": 200, \"set_community\": \"64512:100\" } ]\n  }'\n```\n\n```json\n{ \"status\": \"created\", \"route_map\": \"prefer-customer\" }\n```\n\n`GET /api/v1/bgp/route-maps`:\n\n```json\n{\n  \"route_maps\": [\n    { \"name\": \"prefer-customer\", \"entries\": [ { \"seq\": 10, \"action\": \"allow\", \"match_prefix\": \"customer-routes\", \"set_local_pref\": 200, \"set_community\": \"64512:100\", \"set_med\": 0 } ] }\n  ]\n}\n```\n\n### Apply import/export policy\n\nBind a route-map to a neighbor in a direction. `{dir}` in the path is `import` or\n`export`; the body needs `neighbor` and `route_map` (both **required**).\n\n```bash\ncurl -k -X POST \"$NODE/api/v1/bgp/policy/import\" \\\n  -H \"Authorization: Bearer $TOKEN\" -H \"Content-Type: application/json\" \\\n  -d '{\"neighbor\":\"192.0.2.1\",\"route_map\":\"prefer-customer\"}'\n```\n\n```json\n{ \"status\": \"applied\", \"direction\": \"import\", \"neighbor\": \"192.0.2.1\", \"route_map\": \"prefer-customer\" }\n```\n\n`GET /api/v1/bgp/policy` returns the current bindings as neighbor → route-map maps:\n\n```json\n{ \"import\": { \"192.0.2.1\": \"prefer-customer\" }, \"export\": { \"192.0.2.1\": \"announce-only\" } }\n```\n\n---\n\n## NIC bonds\n\nAggregate physical NICs into a bond for redundancy or throughput. Create a bond,\nenslave/release member interfaces, and set a consistent MTU across the bond and\nits members.\n\n| Method | Path | Purpose |\n|---|---|---|\n| GET | `/api/v1/bonds` | List bonds |\n| POST | `/api/v1/bonds` | Create a bond |\n| GET | `/api/v1/bonds/{id}` | Get one bond |\n| DELETE | `/api/v1/bonds/{id}` | Delete a bond |\n| POST | `/api/v1/bonds/{id}/members` | Enslave a member NIC |\n| DELETE | `/api/v1/bonds/{id}/members/{iface}` | Release a member NIC |\n| PUT | `/api/v1/bonds/{id}/mtu` | Set the bond+members MTU |\n\n**Create a bond.** `name` and `mode` are **required**; `mode` is `active-backup`\nor `802.3ad` (LACP). `id` is optional — one is generated if omitted.\n\n```bash\ncurl -k -X POST \"$NODE/api/v1/bonds\" \\\n  -H \"Authorization: Bearer $TOKEN\" -H \"Content-Type: application/json\" \\\n  -d '{\"name\":\"cnv-bond0\",\"mode\":\"active-backup\",\"mtu\":1500}'\n```\n\n```json\n{\n  \"status\": \"created\",\n  \"bond\": { \"id\": \"bond-9f2a1c44d0e7b3a8\", \"name\": \"cnv-bond0\", \"mode\": \"active-backup\", \"mtu\": 1500, \"active_slave\": \"\", \"members\": [] }\n}\n```\n\n**Enslave a member** (member `state` is `active`, `standby`, or `failed`):\n\n```bash\ncurl -k -X POST \"$NODE/api/v1/bonds/bond-9f2a1c44d0e7b3a8/members\" \\\n  -H \"Authorization: Bearer $TOKEN\" -H \"Content-Type: application/json\" \\\n  -d '{\"interface\":\"cnv-nic-0\"}'\n```\n\n```json\n{ \"status\": \"enslaved\", \"id\": \"bond-9f2a1c44d0e7b3a8\", \"interface\": \"cnv-nic-0\" }\n```\n\n**Get a bond:**\n\n```json\n{\n  \"bond\": {\n    \"id\": \"bond-9f2a1c44d0e7b3a8\", \"name\": \"cnv-bond0\", \"mode\": \"active-backup\", \"mtu\": 1500, \"active_slave\": \"cnv-nic-0\",\n    \"members\": [ { \"interface\": \"cnv-nic-0\", \"state\": \"active\" }, { \"interface\": \"cnv-nic-1\", \"state\": \"standby\" } ]\n  }\n}\n```\n\n**Set the MTU:**\n\n```bash\ncurl -k -X PUT \"$NODE/api/v1/bonds/bond-9f2a1c44d0e7b3a8/mtu\" \\\n  -H \"Authorization: Bearer $TOKEN\" -H \"Content-Type: application/json\" \\\n  -d '{\"mtu\":9000}'\n```\n\n```json\n{ \"status\": \"mtu-set\", \"id\": \"bond-9f2a1c44d0e7b3a8\", \"mtu\": 9000 }\n```\n\nRelease a member → `DELETE /api/v1/bonds/{id}/members/cnv-nic-1` returns\n`{ \"status\": \"released\", \"id\": \"...\", \"interface\": \"cnv-nic-1\" }`.\nDelete a bond → `{ \"status\": \"deleted\", \"id\": \"...\" }`.\n\n---\n\n## Gateway HA & floating IPs\n\nFor two nodes paired active/standby, inspect HA state and trigger\nfailover/failback. Floating IPs are virtual addresses bound to a primary endpoint\nwith an optional standby for takeover.\n\n### Gateway HA\n\n*(Plan feature: Gateway HA.)*\n\n| Method | Path | Purpose |\n|---|---|---|\n| GET | `/api/v1/gateway/status` | Current HA state |\n| POST | `/api/v1/gateway/failover` | Force this node to take over (become active) |\n| POST | `/api/v1/gateway/failback` | Trigger failback |\n\n### Source NAT (masquerade)\n\nHow a private tenant subnet reaches the internet through the gateway's public\naddress. Traffic leaving `source_cidr` via `interface` is rewritten to that\ninterface's address, and replies are translated back.\n\n| Method | Path | Purpose |\n|---|---|---|\n| GET | `/api/v1/gateway/snat` | List NAT rules |\n| POST | `/api/v1/gateway/snat` | Add a masquerade rule |\n| DELETE | `/api/v1/gateway/snat/{id}` | Remove a rule |\n\n```bash\ncurl -k -X POST \"$NODE/api/v1/gateway/snat\" \\\n  -H \"Authorization: Bearer $TOKEN\" -H \"Content-Type: application/json\" \\\n  -d '{\"source_cidr\":\"10.20.0.0/24\",\"interface\":\"cnv-nic-1\"}'\n```\n\n```json\n{ \"status\": \"created\", \"rule\": { \"id\": 3, \"type\": \"snat\", \"source_ip\": \"10.20.0.0/24\", \"interface\": \"cnv-nic-1\" } }\n```\n\n`source` and `wan` are accepted as aliases for `source_cidr` and `interface`,\nmatching the names the CLI uses. These are the same rules `cenvero-str-ctl\ngateway snat` manages — both go through the same manager, so the two views\ncannot drift.\n\n```bash\ncurl -k \"$NODE/api/v1/gateway/status\" -H \"Authorization: Bearer $TOKEN\"\n```\n\n`local_state`/`peer_state` are `active`, `standby`, or `solo`:\n\n```json\n{\n  \"local_state\": \"active\", \"peer_state\": \"standby\", \"vip\": \"203.0.113.1\",\n  \"last_heartbeat\": \"2026-06-30T12:00:01Z\", \"uptime\": \"72h3m12s\", \"active_conns\": 1024, \"peer_addr\": \"10.0.0.6\"\n}\n```\n\n```bash\ncurl -k -X POST \"$NODE/api/v1/gateway/failover\" -H \"Authorization: Bearer $TOKEN\"\n```\n\n```json\n{ \"status\": \"failover_triggered\" }\n```\n\n`POST /api/v1/gateway/failback` returns `{ \"status\": \"failback_triggered\" }`.\n\n### Floating IPs\n\n| Method | Path | Purpose |\n|---|---|---|\n| GET | `/api/v1/float` | List floating IPs |\n| POST | `/api/v1/float` | Assign a floating IP |\n| GET | `/api/v1/float/{id}` | Get one floating IP |\n| DELETE | `/api/v1/float/{id}` | Release a floating IP |\n\n**Assign a floating IP.** `ip` is **required**; `primary` and `standby` name the\nendpoints. `state` is `active`, `standby`, or `failover`.\n\n```bash\ncurl -k -X POST \"$NODE/api/v1/float\" \\\n  -H \"Authorization: Bearer $TOKEN\" -H \"Content-Type: application/json\" \\\n  -d '{\"ip\":\"203.0.113.7\",\"primary\":\"web-01\",\"standby\":\"web-02\"}'\n```\n\n```json\n{\n  \"status\": \"created\",\n  \"floating_ip\": { \"id\": \"fip-7c1e\", \"ip\": \"203.0.113.7\", \"primary\": \"web-01\", \"standby\": \"web-02\", \"state\": \"active\", \"created_at\": \"2026-06-30T12:00:00Z\" }\n}\n```\n\n**List:**\n\n```json\n{ \"floating_ips\": [ { \"id\": \"fip-7c1e\", \"ip\": \"203.0.113.7\", \"primary\": \"web-01\", \"standby\": \"web-02\", \"active_host\": \"node-a\", \"state\": \"active\", \"created_at\": \"2026-06-30T12:00:00Z\" } ] }\n```\n\nRelease → `DELETE /api/v1/float/fip-7c1e` returns `{ \"status\": \"deleted\", \"id\": \"fip-7c1e\" }`.\n\n---\n\n## Traffic visibility\n\nRead-only views of live connections, usage accounting, and the bandwidth\nlimits/quotas you have configured.\n\n### Flows\n\nThe live connection table the node tracks, plus aggregate stats and a downloadable\nexport. All reads. With no traffic tracked these return empty results rather than\nerroring.\n\n| Method | Path | Purpose |\n|---|---|---|\n| GET | `/api/v1/flows` | List flows (`?state=new/active/closed`) |\n| GET | `/api/v1/flows/stats` | Aggregate statistics |\n| GET | `/api/v1/flows/export` | Download flows (`?format=csv/json`, `?state=`) |\n\n```bash\ncurl -k \"$NODE/api/v1/flows?state=active\" -H \"Authorization: Bearer $TOKEN\"\n```\n\n```json\n{\n  \"flows\": [\n    {\n      \"id\": \"f-001\", \"src_ip\": \"10.0.0.11\", \"dst_ip\": \"203.0.113.5\", \"src_port\": 51000, \"dst_port\": 443, \"protocol\": \"tcp\",\n      \"bytes_in\": 12000, \"bytes_out\": 3400, \"packets_in\": 40, \"packets_out\": 28,\n      \"start_time\": \"2026-06-30T11:59:00Z\", \"last_seen\": \"2026-06-30T12:00:00Z\", \"state\": \"active\"\n    }\n  ],\n  \"count\": 1\n}\n```\n\n**Stats** (`per_protocol`/`per_state` are always present; `top_talkers` is ranked\nby packet volume):\n\n```json\n{\n  \"stats\": {\n    \"total_flows\": 128, \"active_flows\": 73, \"total_bytes\": 9120384, \"total_packets\": 21044,\n    \"per_protocol\": { \"tcp\": 110, \"udp\": 18 }, \"per_state\": { \"active\": 73, \"closed\": 55 },\n    \"top_talkers\": [ { \"ip\": \"10.0.0.11\", \"packets\": 8044, \"bytes\": 4011008, \"flows\": 12 } ]\n  }\n}\n```\n\n**Export** streams a file (`Content-Disposition: attachment`). `format` defaults to\n`json`; pass `format=csv` for a spreadsheet-friendly download:\n\n```bash\ncurl -k \"$NODE/api/v1/flows/export?format=csv&state=active\" \\\n  -H \"Authorization: Bearer $TOKEN\" -o flows.csv\n```\n\n### Accounting\n\nPer-period usage roll-ups derived from the node's traffic accounting. All three\naccept an optional window via `?start=` and `?end=` (RFC 3339); the default window\nis the current UTC calendar month to now.\n\n| Method | Path | Purpose |\n|---|---|---|\n| GET | `/api/v1/accounting/summary` | Totals for the period |\n| GET | `/api/v1/accounting/billing` | Per-source usage + cost (`?rate=` $/Mbps) |\n| GET | `/api/v1/accounting/95th` | 95th-percentile in/out Mbps |\n\n```bash\ncurl -k \"$NODE/api/v1/accounting/summary\" -H \"Authorization: Bearer $TOKEN\"\n```\n\n```json\n{\n  \"summary\": {\n    \"total_bandwidth_gb\": 12.5, \"total_bytes\": 12500000000, \"total_packets\": 9000000,\n    \"total_vms\": 4, \"active_ips\": 4, \"period_start\": \"2026-06-01T00:00:00Z\", \"period_end\": \"2026-06-30T12:00:00Z\"\n  }\n}\n```\n\n**Billing.** `rate` is an optional $/Mbps figure for 95th-percentile billing; omit\nit (or pass `0`) to get per-source usage and P95 with zero cost.\n\n```bash\ncurl -k \"$NODE/api/v1/accounting/billing?rate=1.50\" -H \"Authorization: Bearer $TOKEN\"\n```\n\n```json\n{\n  \"billing\": {\n    \"period\": \"2026-06-01/2026-06-30\", \"total_usd\": 42.00,\n    \"items\": [ { \"mac\": \"aa:bb:cc:dd:ee:ff\", \"bytes_total\": 8000000000, \"gb\": 8.0, \"p95_mbps\": 28.0, \"cost_usd\": 42.00 } ]\n  }\n}\n```\n\n**95th percentile:**\n\n```json\n{ \"percentile_95th\": { \"in_mbps\": 31.2, \"out_mbps\": 18.7, \"period_start\": \"2026-06-01T00:00:00Z\", \"period_end\": \"2026-06-30T00:00:00Z\" } }\n```\n\n### Bandwidth limits, pools & quotas\n\nPer-MAC rate limits, shared pools, and monthly usage quotas. *(Plan feature:\nBandwidth shaping.)*\n\n| Method | Path | Purpose |\n|---|---|---|\n| GET | `/api/v1/bandwidth` | List limits + pools (or one limit via `?mac=`) |\n| POST | `/api/v1/bandwidth` | Create/update a per-MAC limit |\n| POST | `/api/v1/bandwidth/pools` | Create a shared pool |\n| POST | `/api/v1/bandwidth/pools/members` | Add a MAC to a pool |\n| GET | `/api/v1/bandwidth/quotas` | List quotas (or one via `?mac=`) |\n| POST | `/api/v1/bandwidth/quotas` | Create/update a quota |\n| GET | `/api/v1/bandwidth/quotas/{mac}` | Get one MAC's quota + status |\n| DELETE | `/api/v1/bandwidth/{id}` | Remove a limit — that MAC becomes unshaped |\n| DELETE | `/api/v1/bandwidth/pools/{id}` | Remove a shared pool |\n| DELETE | `/api/v1/bandwidth/pools/{id}/members/{mac}` | Remove one MAC from a pool |\n\nRemoving a limit stops shaping that MAC — it is not throttled at all afterwards,\nso it draws whatever the node's licensed ceiling allows. Removing a pool leaves\nits former members unshaped in the same way. Each DELETE returns **404** when the\nlimit, pool or member does not exist.\n\n```bash\ncurl -k -X DELETE \"$NODE/api/v1/bandwidth/limit-1\" -H \"Authorization: Bearer $TOKEN\"\ncurl -k -X DELETE \"$NODE/api/v1/bandwidth/pools/1\" -H \"Authorization: Bearer $TOKEN\"\ncurl -k -X DELETE \"$NODE/api/v1/bandwidth/pools/1/members/aa:bb:cc:dd:ee:ff\" \\\n  -H \"Authorization: Bearer $TOKEN\"\n```\n\n**Limit fields** — `id` and `target_mac` are **required**. `rate_bps`,\n`burst_bytes`, and `guaranteed_bps` are in bits/bytes per second; `direction` is\n`up`, `down`, or `both` (default `both`). `pool_id` links the limit to a shared\npool.\n\n```bash\ncurl -k -X POST \"$NODE/api/v1/bandwidth\" \\\n  -H \"Authorization: Bearer $TOKEN\" -H \"Content-Type: application/json\" \\\n  -d '{\"id\":\"vm-11-cap\",\"target_mac\":\"aa:bb:cc:dd:ee:ff\",\"rate_bps\":1000000000,\"burst_bytes\":125000,\"direction\":\"both\"}'\n```\n\n```json\n{\n  \"status\": \"updated\",\n  \"limit\": { \"id\": \"vm-11-cap\", \"target_mac\": \"aa:bb:cc:dd:ee:ff\", \"interface_id\": 0, \"rate_bps\": 1000000000, \"burst_bytes\": 125000, \"guaranteed_bps\": 0, \"direction\": \"both\" }\n}\n```\n\n**List** (returns all limits and pools; pass `?mac=` to get a single limit, `404`\nif none). Note that pool objects use capitalized field names:\n\n```json\n{\n  \"limits\": [ { \"id\": \"vm-11-cap\", \"target_mac\": \"aa:bb:cc:dd:ee:ff\", \"interface_id\": 0, \"rate_bps\": 1000000000, \"burst_bytes\": 125000, \"guaranteed_bps\": 0, \"direction\": \"both\" } ],\n  \"pools\": [ { \"ID\": \"tenant-a-pool\", \"Name\": \"Tenant A shared\", \"TotalBps\": 10000000000, \"AllocatedBps\": 2000000000, \"Members\": [\"aa:bb:cc:dd:ee:ff\"] } ]\n}\n```\n\n**Create a shared pool** — body: `id` and `total_bps` (**both required**), `name`\noptional. Several MACs can then draw from the pool's aggregate cap.\n\n```bash\ncurl -k -X POST \"$NODE/api/v1/bandwidth/pools\" \\\n  -H \"Authorization: Bearer $TOKEN\" -H \"Content-Type: application/json\" \\\n  -d '{\"id\":\"tenant-a-pool\",\"name\":\"Tenant A shared\",\"total_bps\":10000000000}'\n```\n\n```json\n{ \"status\": \"created\", \"id\": \"tenant-a-pool\" }\n```\n\n**Add a MAC to a pool** — body: `pool_id` and `member_mac` (**both required**),\n`rate_bps` optional (the member's own ceiling within the pool).\n\n```bash\ncurl -k -X POST \"$NODE/api/v1/bandwidth/pools/members\" \\\n  -H \"Authorization: Bearer $TOKEN\" -H \"Content-Type: application/json\" \\\n  -d '{\"pool_id\":\"tenant-a-pool\",\"member_mac\":\"aa:bb:cc:dd:ee:ff\",\"rate_bps\":1000000000}'\n```\n\n```json\n{ \"status\": \"added\", \"pool_id\": \"tenant-a-pool\", \"member_mac\": \"aa:bb:cc:dd:ee:ff\" }\n```\n\n**Quotas.** A quota caps a MAC's monthly usage and resets at the start of each UTC\nmonth. `id`, `mac`, and `monthly_limit_bytes` (> 0) are **required**. `action` is\n`notify`, `throttle`, or `block`; `enforced: true` opts into a *hard* cap (default\nis advisory — alert and count only).\n\n```bash\ncurl -k -X POST \"$NODE/api/v1/bandwidth/quotas\" \\\n  -H \"Authorization: Bearer $TOKEN\" -H \"Content-Type: application/json\" \\\n  -d '{\"id\":\"q-vm11\",\"mac\":\"aa:bb:cc:dd:ee:ff\",\"monthly_limit_bytes\":1000000000000,\"action\":\"throttle\",\"enforced\":true}'\n```\n\nThe response includes the derived `status` (`ok`, `warning`, `exceeded`),\n`remaining_bytes`, and whether the MAC is currently `throttled`:\n\n```json\n{\n  \"status\": \"updated\",\n  \"quota\": {\n    \"id\": \"q-vm11\", \"mac\": \"aa:bb:cc:dd:ee:ff\", \"monthly_limit_bytes\": 1000000000000, \"used_bytes\": 250000000000, \"reset_day\": 0,\n    \"action\": \"throttle\", \"enforced\": true, \"current_period\": \"2026-06\", \"status\": \"ok\", \"remaining_bytes\": 750000000000, \"throttled\": false\n  }\n}\n```\n\n**Get one MAC's quota:** `GET /api/v1/bandwidth/quotas/aa:bb:cc:dd:ee:ff` returns\n`{ \"quota\": { ... } }` with the same shape, or `404` `{ \"error\": \"no quota for MAC ...\" }`.\n`GET /api/v1/bandwidth/quotas` (no `?mac=`) returns `{ \"quotas\": [ ... ] }`.\n\n---\n\n## Cluster\n\nInspect and manage this node's clustering state. Cluster membership is normally\nprovisioned through the panel-delivered configuration; these endpoints let you\nread status and, when clustering is enabled, manage membership. Every member is\nthe same kind of node, so there is no node type to select.\n*(Plan feature: Cluster. See also the [Clustering guide](/docs/clustering/overview).)*\n\n| Method | Path | Purpose |\n|---|---|---|\n| GET | `/api/v1/cluster` | Brief cluster summary |\n| GET | `/api/v1/cluster/status` | State, leader, and peers |\n| GET | `/api/v1/cluster/state` | Replicated-state counts |\n| POST | `/api/v1/cluster/join` | Join a peer (`node_id`, `address` required) |\n| POST | `/api/v1/cluster/leave` | Leave the cluster |\n\n```bash\ncurl -k \"$NODE/api/v1/cluster/status\" -H \"Authorization: Bearer $TOKEN\"\n```\n\n```json\n{ \"state\": \"leader\", \"leader\": \"10.0.0.5\", \"is_leader\": true, \"peers\": [\"10.0.0.6\"] }\n```\n\n`state` is this node's role in the cluster — `leader` or `follower` — not a kind\nof node.\n\n`GET /api/v1/cluster/state` returns replicated-resource counts:\n\n```json\n{ \"blocklist_count\": 0, \"ip_allocations_count\": 12, \"vxlan_peers_count\": 2, \"floating_ips_count\": 1, \"tenants_count\": 3 }\n```\n\nJoin/leave return `{ \"status\": \"joined\", ... }` / `{ \"status\": \"left\" }`, or\n**503** when clustering is not enabled on the node.\n\n---\n\n## Multi-tenancy\n\nA **tenant** is one of your downstream customers on this node. Tenants have a\nlifecycle (`active` → `suspended` → `deleted`), a resource **quota**, and their own\n**scoped API keys**.\n\n| Method | Path | Purpose |\n|---|---|---|\n| GET | `/api/v1/tenant` | List tenants |\n| POST | `/api/v1/tenant` | Create a tenant (`name` required) |\n| GET | `/api/v1/tenant/{id}` | Get a tenant |\n| PUT | `/api/v1/tenant/{id}` | Update name and/or status |\n| DELETE | `/api/v1/tenant/{id}` | Delete a tenant (cascades its keys, quota, networks & IPAM) |\n| GET / PUT | `/api/v1/tenant/{id}/quota` | Read / set a tenant's quota |\n| GET / POST | `/api/v1/tenant/{id}/keys` | List / mint scoped API keys |\n| DELETE | `/api/v1/tenant/{id}/keys/{kid}` | Revoke a scoped key |\n\n### Create / list tenants\n\n```bash\ncurl -k -X POST \"$NODE/api/v1/tenant\" \\\n  -H \"Authorization: Bearer $TOKEN\" -H \"Content-Type: application/json\" \\\n  -d '{\"name\":\"acme-corp\"}'\n```\n\n```json\n{\n  \"status\": \"created\",\n  \"tenant\": { \"id\": \"t-9f3a21\", \"name\": \"acme-corp\", \"status\": \"active\", \"created_at\": \"2026-06-30T14:05:00Z\", \"updated_at\": \"2026-06-30T14:05:00Z\" }\n}\n```\n\nReturns **201**. `GET /api/v1/tenant` returns `{ \"tenants\": [ ... ] }`; `status` is\n`active`, `suspended`, or `deleted`.\n\n### Update a tenant\n\n`PUT /api/v1/tenant/{id}` — body: `name` and/or `status` (`active`, `suspended`,\n`deleted`).\n\n```bash\ncurl -k -X PUT \"$NODE/api/v1/tenant/t-9f3a21\" \\\n  -H \"Authorization: Bearer $TOKEN\" -H \"Content-Type: application/json\" \\\n  -d '{\"name\":\"acme-corp\",\"status\":\"suspended\"}'\n```\n\n```json\n{ \"status\": \"updated\", \"tenant\": { \"id\": \"t-9f3a21\", \"name\": \"acme-corp\", \"status\": \"suspended\", \"updated_at\": \"2026-06-30T14:06:00Z\" } }\n```\n\n**Delete a tenant.** `DELETE /api/v1/tenant/t-9f3a21` returns\n`{ \"status\": \"deleted\", \"id\": \"t-9f3a21\" }`. Deleting a tenant cascades cleanup of\neverything scoped to it: its scoped API keys (which stop authenticating\nimmediately), its quota, and its private networks and IPAM pools/allocations.\n\n### Tenant quota\n\nA quota caps how much bandwidth a tenant may use. **`0` means unlimited.**\n\n| Field | Type | Meaning |\n|---|---|---|\n| `max_bandwidth_bps` | int64 | Bandwidth cap in **bits/sec** (`0` = unlimited) |\n\n```bash\ncurl -k -X PUT \"$NODE/api/v1/tenant/t-9f3a21/quota\" \\\n  -H \"Authorization: Bearer $TOKEN\" -H \"Content-Type: application/json\" \\\n  -d '{\"max_bandwidth_bps\":1000000000}'\n```\n\n```json\n{\n  \"status\": \"quota-set\",\n  \"quota\": { \"tenant_id\": \"t-9f3a21\", \"max_bandwidth_bps\": 1000000000 }\n}\n```\n\nThe cap is applied on the way out of the node and behaves as described in\n[Tenants & Bandwidth](/docs/tenants) — TCP settles at the rate, traffic over it is\ndropped rather than queued. Every plan is otherwise unlimited: there is no cap on\nhow many addresses, workloads or firewall rules a tenant may have.\n\n### Tenant-scoped API keys\n\nMint a key that authenticates as the tenant but is **confined to that tenant's\nresources** — ideal for handing to the tenant's own automation or your\nper-customer billing logic. **Create** body: `name` (label) and `ttl_hours`\n(`0`/omitted = no expiry).\n\n```bash\ncurl -k -X POST \"$NODE/api/v1/tenant/t-9f3a21/keys\" \\\n  -H \"Authorization: Bearer $TOKEN\" -H \"Content-Type: application/json\" \\\n  -d '{\"name\":\"acme-automation\",\"ttl_hours\":720}'\n```\n\n```json\n{\n  \"status\": \"created\",\n  \"key\": {\n    \"id\": \"k-7b1c44\", \"tenant_id\": \"t-9f3a21\", \"name\": \"acme-automation\",\n    \"created_at\": \"2026-06-30T14:07:00Z\", \"expires_at\": \"2026-07-30T14:07:00Z\",\n    \"secret\": \"tnk_3f8a...e91c\", \"note\": \"store this secret now; it is shown only once\"\n  }\n}\n```\n\n> The `secret` is returned **once** at creation and never again. Store it\n> immediately; list responses omit it.\n\nList → `GET /api/v1/tenant/t-9f3a21/keys` returns `{ \"keys\": [ ... ] }` (no\nsecrets). Revoke → `DELETE /api/v1/tenant/t-9f3a21/keys/k-7b1c44` returns\n`{ \"status\": \"revoked\", \"id\": \"k-7b1c44\" }`.\n\n---\n\n## Billing automation (operator hook)\n\nThese endpoints are your hook for gating downstream customers. Your billing system\ncalls them — typically with a **minted operator API key** (`apikeys mint`) — to\nsuspend a non-paying customer, resume them on payment, or apply/clear a bandwidth\nlimit. A tenant here is one of your customers. (A tenant-scoped key may also call\n`/billing/tenants/{id}/...` for its own tenant.) These are mutating calls and are\n**frozen** (`403`) while the node's license is inactive.\n\n| Method | Path | Purpose |\n|---|---|---|\n| POST | `/api/v1/billing/tenants/{id}/suspend` | Mark the tenant `suspended` |\n| POST | `/api/v1/billing/tenants/{id}/resume` | Mark the tenant `active` |\n| POST | `/api/v1/billing/tenants/{id}/limit` | Apply an aggregate rate cap |\n| POST | `/api/v1/billing/tenants/{id}/unlimit` | Remove the rate cap |\n| GET | `/api/v1/billing/tenants/{id}` | Read the customer's billing state |\n\n```bash\ncurl -k -X POST \"$NODE/api/v1/billing/tenants/t-9f3a21/suspend\" -H \"Authorization: Bearer $TOKEN\"\n```\n\n```json\n{ \"id\": \"t-9f3a21\", \"status\": \"suspended\" }\n```\n\n**Rate-limit** — body: `rate_mbps` (int64, **Mbps**). Negative is treated as `0`;\ncapped at `1000000` (1 Tbps). Stored as `rate_mbps × 1,000,000` bits/sec.\n\n```bash\ncurl -k -X POST \"$NODE/api/v1/billing/tenants/t-9f3a21/limit\" \\\n  -H \"Authorization: Bearer $TOKEN\" -H \"Content-Type: application/json\" \\\n  -d '{\"rate_mbps\":100}'\n```\n\n```json\n{ \"id\": \"t-9f3a21\", \"max_bandwidth_bps\": 100000000 }\n```\n\n`unlimit` returns `{ \"id\": \"t-9f3a21\", \"max_bandwidth_bps\": 0 }`; `GET` returns\n`{ \"id\": \"t-9f3a21\", \"name\": \"acme-corp\", \"status\": \"active\", \"max_bandwidth_bps\": 100000000 }`.\n\n> CLI equivalent: `cenvero-str-ctl billing suspend|resume|limit|unlimit|status <tenant-id>`.\n\n---\n\n## Container networking\n\nAttach a container's network namespace to one of your **managed private networks**.\nThe container claims a managed endpoint (IP + MAC) and is wired with a veth pair\ninto the network's bridge — the same attach path a VM uses, so containers share the\nnetwork's IP/MAC pool and firewall.\n\n| Method | Path | Purpose |\n|---|---|---|\n| GET | `/api/v1/containers` | List attached containers |\n| POST | `/api/v1/containers/attach` | Attach a container netns |\n| GET | `/api/v1/containers/{id}` | Get one container |\n| POST | `/api/v1/containers/{id}/detach` | Detach (frees the endpoint) |\n\n**Attach** body:\n\n| Field | Type | Required | Notes |\n|---|---|---|---|\n| `runtime` | string | yes | `lxc`, `docker`, or `podman` |\n| `network_id` | string | yes | A managed network you've already created |\n| `netns_pid` | int | yes | The container init PID whose network namespace is the target |\n| `container_id` | string | no | Your runtime's container id (for tracking) |\n| `ip` | string | no | Request a specific IP; omitted = next free in the network |\n| `firewall` | bool | no | Apply the managed per-container firewall rule |\n| `id` | string | no | Supply your own record id; omitted = generated |\n\n```bash\ncurl -k -X POST \"$NODE/api/v1/containers/attach\" \\\n  -H \"Authorization: Bearer $TOKEN\" -H \"Content-Type: application/json\" \\\n  -d '{\"runtime\":\"docker\",\"network_id\":\"net-7a\",\"netns_pid\":48213,\"container_id\":\"9c2f...\",\"firewall\":true}'\n```\n\n```json\n{\n  \"status\": \"attached\",\n  \"container\": {\n    \"id\": \"c-12ab\", \"runtime\": \"docker\", \"container_id\": \"9c2f...\", \"netns_pid\": 48213,\n    \"veth_host\": \"cnv-veth-12ab\", \"veth_container\": \"eth0\", \"bridge\": \"cnv-user-br0\",\n    \"mac\": \"02:42:0a:00:00:05\", \"ip\": \"10.0.0.5\", \"network_id\": \"net-7a\", \"endpoint_id\": \"ep-31\"\n  }\n}\n```\n\nReturns **201**. List → `GET /api/v1/containers` returns `{ \"containers\": [ ... ] }`.\nDetach → `POST /api/v1/containers/c-12ab/detach` tears down the veth, releases the\nendpoint, and returns `{ \"status\": \"detached\", \"id\": \"c-12ab\" }`.\n\n---\n\n## Operations\n\n### Alerting\n\nDefine **conditions** (a threshold on a metric), attach **actions** (notify on\nfire), and review/acknowledge fired alerts.\n\n| Method | Path | Purpose |\n|---|---|---|\n| GET | `/api/v1/alerts` | List fired alerts (`?state=firing/resolved/acknowledged`) |\n| GET | `/api/v1/alerts/conditions` | List conditions |\n| POST | `/api/v1/alerts/conditions` | Create a condition |\n| DELETE | `/api/v1/alerts/conditions/{id}` | Delete a condition |\n| POST | `/api/v1/alerts/conditions/{id}/actions` | Attach an action |\n| POST | `/api/v1/alerts/{id}/ack` | Acknowledge an alert |\n| GET | `/api/v1/alerts/history` | Full alert history |\n\n**Create a condition** — `metric_type` (`bandwidth`, `pps`, `connections`,\n`quota`, `threat`) and `operator` (`gt`, `lt`, `eq`) are required; `threshold`,\n`target`, `duration_secs`, `cooldown_secs` optional. Returns **201**.\n\n```bash\ncurl -k -X POST \"$NODE/api/v1/alerts/conditions\" \\\n  -H \"Authorization: Bearer $TOKEN\" -H \"Content-Type: application/json\" \\\n  -d '{\"metric_type\":\"bandwidth\",\"operator\":\"gt\",\"threshold\":900000000,\"target\":\"global\",\"duration_secs\":60,\"cooldown_secs\":300}'\n```\n\n```json\n{ \"condition\": { \"id\": \"cond-1\", \"metric_type\": \"bandwidth\", \"threshold\": 900000000, \"operator\": \"gt\", \"target\": \"global\", \"duration_secs\": 60, \"cooldown_secs\": 300 } }\n```\n\n**Attach an action** — `type` is `websocket`, `webhook`, or `log`; for `webhook`,\n`config` is the URL to POST to when the alert fires. Returns **201**.\n\n```bash\ncurl -k -X POST \"$NODE/api/v1/alerts/conditions/cond-1/actions\" \\\n  -H \"Authorization: Bearer $TOKEN\" -H \"Content-Type: application/json\" \\\n  -d '{\"type\":\"webhook\",\"config\":\"https://hooks.example.net/stratum\"}'\n```\n\n```json\n{ \"condition_id\": \"cond-1\", \"type\": \"webhook\" }\n```\n\n**Acknowledge an alert** — `by` defaults to `operator`:\n\n```bash\ncurl -k -X POST \"$NODE/api/v1/alerts/a-55/ack\" \\\n  -H \"Authorization: Bearer $TOKEN\" -H \"Content-Type: application/json\" \\\n  -d '{\"by\":\"oncall@example.net\"}'\n```\n\n```json\n{ \"acknowledged\": \"a-55\", \"by\": \"oncall@example.net\" }\n```\n\n`GET /api/v1/alerts` and `/alerts/history` return `{ \"alerts\": [ ... ] }` /\n`{ \"history\": [ ... ] }`; delete a condition returns `{ \"removed\": \"cond-1\" }`.\n\n> `POST /api/v1/alerts` is **not** a fire endpoint — alerts fire from configured\n> conditions. It returns `400` pointing you to the condition/ack subpaths above.\n\n### Self-healing\n\nThe node runs periodic health checks and repairs what it can.\n\n| Method | Path | Purpose |\n|---|---|---|\n| GET | `/api/v1/heal` | Latest check results |\n| POST | `/api/v1/heal/check` | Run the full sweep now (returns fresh results) |\n\n```bash\ncurl -k \"$NODE/api/v1/heal\" -H \"Authorization: Bearer $TOKEN\"\n```\n\n```json\n{\n  \"checks\": [\n    { \"name\": \"bridge-mgmt\", \"healthy\": true, \"last_checked\": \"2026-06-30T14:11:00Z\" },\n    { \"name\": \"bridge-user\", \"healthy\": false, \"error\": \"link down\", \"repaired\": true, \"last_checked\": \"2026-06-30T14:11:00Z\" }\n  ]\n}\n```\n\nEach check reports `name`, `healthy`, and `last_checked`; plus `error`,\n`repaired`, and `repair_error` when relevant.\n\n### Backups\n\nCreate config/full backups, list and restore them, and manage schedules.\n\n| Method | Path | Purpose |\n|---|---|---|\n| GET | `/api/v1/backups` | List backups |\n| POST | `/api/v1/backups` | Create a backup (`type`: `config` (default) or `full`) |\n| POST | `/api/v1/backups/restore` | Restore a backup (`ref`: id or path) |\n| GET | `/api/v1/backups/schedules` | List schedules |\n| POST | `/api/v1/backups/schedules` | Create a schedule |\n| DELETE | `/api/v1/backups/schedules/{id}` | Remove a schedule |\n\n```bash\ncurl -k -X POST \"$NODE/api/v1/backups\" \\\n  -H \"Authorization: Bearer $TOKEN\" -H \"Content-Type: application/json\" \\\n  -d '{\"type\":\"config\"}'\n```\n\n```json\n{ \"backup\": { \"id\": \"bk-02\", \"type\": \"config\", \"size\": 20480, \"created_at\": \"2026-06-30T14:12:00Z\", \"retention\": 7 } }\n```\n\nReturns **201**. **Restore** — a `config` restore applies in place\n(`{ \"restored\": \"bk-01\", \"staged\": false }`); a `full` restore unpacks to a staging\narea and reports `staged: true` with a note describing the manual completion step\n(stop the agent, restore the staged snapshot, restart).\n\n**Create a schedule** — `expression` (e.g. `daily@03:00`, required), `type`\n(`config` (default) or `full`), `retention` (default `7`).\n\n```bash\ncurl -k -X POST \"$NODE/api/v1/backups/schedules\" \\\n  -H \"Authorization: Bearer $TOKEN\" -H \"Content-Type: application/json\" \\\n  -d '{\"expression\":\"daily@03:00\",\"type\":\"config\",\"retention\":14}'\n```\n\n```json\n{ \"schedule\": { \"id\": \"sch-01\", \"expression\": \"daily@03:00\", \"type\": \"config\", \"retention_count\": 14, \"next_run\": \"2026-07-01T03:00:00Z\" } }\n```\n\nDelete → `{ \"removed\": \"sch-01\" }`.\n\n### Node dashboard\n\n`GET /api/v1/dashboard` — one aggregated snapshot of the node for a status\nscreen: counts and live figures pulled from the subsystems that are running,\nin a single request instead of polling a dozen endpoints.\n\n```bash\ncurl -k \"$NODE/api/v1/dashboard\" -H \"Authorization: Bearer $TOKEN\"\n```\n\n### Not implemented\n\nThese paths exist on the router and return **501 Not Implemented** with an\nexplanation. They are listed here so you do not spend time discovering by\nexperiment that they return nothing useful.\n\n| Method | Path | Use instead |\n|---|---|---|\n| GET | `/api/v1/audit` | The audit record lives in the management panel |\n| GET | `/api/v1/topology` | Build it from `/networks`, `/bridges`, `/nics`, `/cluster/status` |\n| POST | `/api/v1/config/batch` | Apply settings individually |\n| POST | `/api/v1/cluster/profile` | Nothing to select — every node runs the same services |\n| PUT | `/api/v1/dns/records/{id}` | Delete the record and create it again |\n| PUT | `/api/v1/lb/{id}` | Delete the VIP and create it again |\n\nEach answers 501 with a message naming the alternative. They previously returned\na success-shaped body — an empty audit log, an empty topology, `\"applied\"` with\nzero changes — which is indistinguishable from a real, empty answer. A 501 you\ncan branch on is more useful than a success you cannot trust.\n\n### Event log (Server-Sent Events)\n\nStream system events as they happen over a long-lived HTTP connection. For a\npush-style socket see the **Real-time WebSocket** section below.\n\n`GET /api/v1/events?categories=<CSV>` — `categories` is an optional\ncomma-separated filter (uppercase). Omit it for **all** categories. Valid\ncategories:\n\n```\nTRAFFIC  SECURITY  BANDWIDTH  DHCP  DNS  NETWORK  ALERT  SYSTEM  LB  CLUSTER\n```\n\n```bash\ncurl -k -N \"$NODE/api/v1/events?categories=SECURITY,ALERT\" -H \"Authorization: Bearer $TOKEN\"\n```\n\nThe response is `text/event-stream`. Each event arrives as an SSE frame;\nheartbeats (`: keepalive`) hold the connection open:\n\n```\n: connected\n\nevent: SECURITY\ndata: {\"id\":\"b1c2...\",\"type\":\"firewall.block\",\"category\":\"SECURITY\",\"timestamp\":\"2026-06-30T14:13:00Z\",\"payload\":{\"src\":\"203.0.113.7\",\"rule\":\"deny-inbound\"}}\n\n: keepalive\n```\n\n---\n\n## Real-time WebSocket\n\nFor a bidirectional, push-style stream, connect to the node's WebSocket endpoint:\n\n```\nwss://<node-ip>:7072/ws\n```\n\nEnable it once with `cenvero-str-ctl service websocket on`. The WebSocket\nauthenticates with the **node API token only** (not operator-minted or\ntenant-scoped keys).\n\n### 1. Connect & authenticate\n\nProvide the token either as a query parameter or an `Authorization` header:\n\n```\nwss://<node-ip>:7072/ws?token=<api-token>\n```\n\nor `Authorization: Bearer <api-token>`.\n\n> Browser clients can't set request headers on a WebSocket, so use the `?token=`\n> form there. The `Origin` is restricted to the node's own host by default;\n> configure additional allowed origins on the node (`api_allowed_origins`) if you\n> connect from a different web origin.\n\n### 2. Subscribe\n\nAfter the socket opens, send a **subscribe** message naming the categories you want\n(uppercase, the same set as the SSE stream). Until you subscribe, **no events are\ndelivered**. Subscriptions are additive; there is no acknowledgement message —\nmatching events simply begin to flow.\n\n```json\n{ \"action\": \"subscribe\", \"categories\": [\"SECURITY\", \"ALERT\", \"NETWORK\"] }\n```\n\n### 3. Receive events\n\nEach event is delivered as a JSON **text** frame:\n\n```json\n{\n  \"id\": \"b1c2d3...\",\n  \"type\": \"alert.fired\",\n  \"category\": \"ALERT\",\n  \"timestamp\": \"2026-06-30T14:13:00Z\",\n  \"payload\": { \"condition_id\": \"cond-1\", \"value\": 950000000 }\n}\n```\n\n| Field | Type | Notes |\n|---|---|---|\n| `id` | string | Unique event id |\n| `type` | string | Event type, e.g. `alert.fired`, `firewall.block` |\n| `category` | string | One of the categories above |\n| `timestamp` | string | UTC RFC 3339 |\n| `payload` | object | Event-specific detail (omitted when empty) |\n\n### Example — `wscat`\n\n```bash\nwscat --no-check -c \"wss://node.example.net:7072/ws?token=$TOKEN\"\n# once connected:\n> {\"action\":\"subscribe\",\"categories\":[\"SECURITY\",\"ALERT\"]}\n< {\"id\":\"b1c2...\",\"type\":\"alert.fired\",\"category\":\"ALERT\",\"timestamp\":\"2026-06-30T14:13:00Z\",\"payload\":{...}}\n```\n\n(`--no-check` skips TLS verification for a privately-managed node certificate\nduring testing; in production, trust the node's certificate instead.)\n\n### Example — browser\n\n```js\nconst ws = new WebSocket(\"wss://node.example.net:7072/ws?token=YOUR_API_TOKEN\");\n\nws.onopen = () => {\n  ws.send(JSON.stringify({ action: \"subscribe\", categories: [\"ALERT\", \"SECURITY\"] }));\n};\n\nws.onmessage = (e) => {\n  const evt = JSON.parse(e.data);\n  console.log(evt.category, evt.type, evt.payload);\n};\n```\n\n> A client that can't keep up with the event rate is dropped to protect the node;\n> reconnect and re-subscribe to resume.\n\n---\n\n## Event webhooks\n\nRegister HTTP endpoints that the node **POSTs a signed event to** when something\nhappens — an out-of-band, push-style alternative to holding open the SSE\n`/api/v1/events` stream or a WebSocket. A webhook is a URL, an optional\nevent-category filter, and a per-webhook secret. Subscriptions are stored on the\nnode and **survive a restart**, and every delivery carries an **HMAC-SHA256**\nsignature so your receiver can verify it. Managing webhooks uses the standard\n`api_token` bearer, like the rest of this API.\n\nDelivery is **fail-safe**: events are handed to a bounded, out-of-band worker\npool, so a slow, hanging, or broken receiver can never block the node's event\nprocessing or the other webhooks. Each delivery is attempted with a per-attempt\ntimeout and a few retries with exponential backoff; if the delivery queue is ever\nfull, the event is dropped (and counted) rather than blocking.\n\n| Method | Path | Purpose |\n|---|---|---|\n| GET | `/api/v1/webhooks` | List subscriptions (never includes secrets) |\n| POST | `/api/v1/webhooks` | Register a subscription (returns the secret **once**) |\n| GET | `/api/v1/webhooks/{id}` | Get one subscription |\n| DELETE | `/api/v1/webhooks/{id}` | Delete a subscription |\n| POST | `/api/v1/webhooks/{id}/test` | Send a one-shot test delivery |\n\n**Subscription fields**\n\n| Field | Type | Notes |\n|---|---|---|\n| `id` | string | Server-assigned subscription id |\n| `url` | string | The `http`/`https` endpoint events are POSTed to |\n| `categories` | array | Event-category filter (uppercase); **empty means every category** |\n| `created_at` | string | UTC timestamp |\n| `delivered` | integer | Successful deliveries so far |\n| `failed` | integer | Deliveries that failed after all retries |\n| `dropped` | integer | Events dropped because the delivery queue was full |\n| `last_status` | integer | HTTP status of the most recent attempt (omitted until the first attempt) |\n| `last_error` | string | Most recent error, if any (omitted when the last attempt succeeded) |\n\nThe category filter uses the same uppercase categories as the event stream; an\nunknown category is rejected. Omit the filter (or send an empty list) to receive\nevery category:\n\n```\nTRAFFIC  SECURITY  BANDWIDTH  DHCP  DNS  NETWORK  ALERT  SYSTEM  LB  CLUSTER\n```\n\n### Register a webhook\n\n`POST /api/v1/webhooks` — body: `url` (**required**, `http` or `https`);\n`categories` (optional filter); `secret` (optional). Omit `secret` and the node\ngenerates a strong one (prefixed `whsec_`). Returns **201**.\n\n```bash\ncurl -k -X POST \"$NODE/api/v1/webhooks\" \\\n  -H \"Authorization: Bearer $TOKEN\" -H \"Content-Type: application/json\" \\\n  -d '{\"url\":\"https://hooks.example.com/stratum\",\"categories\":[\"SECURITY\",\"ALERT\"]}'\n```\n\n```json\n{\n  \"webhook\": {\n    \"id\": \"9f1c2d3e4a5b6c7d\",\n    \"url\": \"https://hooks.example.com/stratum\",\n    \"categories\": [\"ALERT\", \"SECURITY\"],\n    \"created_at\": \"2026-06-30T14:20:00Z\",\n    \"delivered\": 0,\n    \"failed\": 0,\n    \"dropped\": 0,\n    \"secret\": \"whsec_EXAMPLE_shown_once_store_it_now\"\n  }\n}\n```\n\n> The `secret` is returned **only here**, at registration — it is the HMAC key\n> your receiver needs to verify deliveries. Store it now; list and `GET` responses\n> never include it. Lost it? Delete the webhook and register a new one.\n\nList → `GET /api/v1/webhooks` returns `{ \"webhooks\": [ ... ] }` (secret-free). Get\none → `GET /api/v1/webhooks/9f1c2d3e4a5b6c7d` returns `{ \"webhook\": { ... } }`.\nDelete → `DELETE /api/v1/webhooks/9f1c2d3e4a5b6c7d` returns\n`{ \"deleted\": \"9f1c2d3e4a5b6c7d\" }`.\n\n### Delivery format\n\nEach delivery is an HTTP `POST` with `Content-Type: application/json`. The body is\nthe event itself — the **same JSON shape** delivered over the SSE and WebSocket\nstreams (`id`, `type`, `category`, `timestamp`, `payload`). These headers\naccompany every delivery:\n\n| Header | Value |\n|---|---|\n| `X-Stratum-Signature` | `sha256=` followed by the hex HMAC-SHA256 of the **raw request body**, keyed by your webhook secret |\n| `X-Stratum-Event` | The event category (e.g. `SECURITY`) |\n| `X-Stratum-Delivery` | The event id (matches `id` in the body) |\n| `X-Stratum-Timestamp` | Send time, Unix seconds (UTC) |\n| `User-Agent` | `cenvero-stratum-webhook/1` |\n\nExample delivered body:\n\n```json\n{\n  \"id\": \"b1c2d3e4f5\",\n  \"type\": \"firewall.block\",\n  \"category\": \"SECURITY\",\n  \"timestamp\": \"2026-06-30T14:20:05Z\",\n  \"payload\": { \"src\": \"203.0.113.7\", \"rule\": \"deny-inbound\" }\n}\n```\n\n**Verify a delivery** by recomputing the signature over the exact bytes you\nreceived and comparing it (in constant time) to the `X-Stratum-Signature` header:\n\n```bash\n# body = the raw request body; SECRET = your webhook secret\nprintf '%s' \"$body\" | openssl dgst -sha256 -hmac \"$SECRET\"\n# prepend \"sha256=\" to the hex digest, then compare to X-Stratum-Signature\n```\n\n### Test a webhook\n\n`POST /api/v1/webhooks/{id}/test` sends one synchronous test delivery — a\n`webhook_test` event in the `SYSTEM` category — so you can confirm the receiver is\nreachable and verifies the signature.\n\n```bash\ncurl -k -X POST \"$NODE/api/v1/webhooks/9f1c2d3e4a5b6c7d/test\" \\\n  -H \"Authorization: Bearer $TOKEN\"\n```\n\n```json\n{ \"tested\": \"9f1c2d3e4a5b6c7d\", \"status\": \"delivered\" }\n```\n\nIf the receiver is unreachable or returns a non-2xx status, the call reports\n**502** with `{ \"error\": \"...\", \"webhook_id\": \"...\" }` — the node is fine, the\ndownstream endpoint is not. An unknown id returns **404**.\n\n---\n\n## Bulk create\n\nTwo convenience endpoints create **many items in one request**, each item using\nthe same shape as the corresponding single-create call. Every item is applied\nindependently and the response reports **per-item** success or failure, so one bad\nitem never aborts the rest.\n\n| Method | Path | Per-item shape |\n|---|---|---|\n| POST | `/api/v1/rules/batch` | A firewall rule (as in `POST /api/v1/rules`) |\n| POST | `/api/v1/dns/records/batch` | A DNS record (as in `POST /api/v1/dns/records`) |\n\nSend the items as a JSON array under `rules` (firewall) or `records` (DNS). A\nbatch may hold up to **1000** items; an empty array, or one over the cap, is\nrejected with **400**. Firewall `/rules/batch` is available on any active plan;\n`/dns/records/batch` requires the DNS plan feature. Both are mutating calls, so\nthey are frozen (**403**) while the node's license is inactive.\n\nThe response carries `created` and `failed` counts and a `results` array — one\nentry per submitted item, in order, each with its `index`, an `ok` flag, and\neither the created object (`rule` / `record`) or an `error` string. The HTTP\nstatus reflects the batch as a whole:\n\n| Outcome | Status |\n|---|---|\n| Every item created | `201 Created` |\n| Some created, some failed | `207 Multi-Status` |\n| No item created | `400 Bad Request` |\n\n**Create several firewall rules:**\n\n```bash\ncurl -k -X POST \"$NODE/api/v1/rules/batch\" \\\n  -H \"Authorization: Bearer $TOKEN\" -H \"Content-Type: application/json\" \\\n  -d '{\n    \"rules\": [\n      { \"chain\": \"forward\", \"action\": \"drop\", \"protocol\": \"tcp\", \"dest_port\": 23 },\n      { \"chain\": \"forward\", \"action\": \"drop\", \"protocol\": \"tcp\", \"dest_port\": 2323 }\n    ]\n  }'\n```\n\n```json\n{\n  \"created\": 2,\n  \"failed\": 0,\n  \"results\": [\n    { \"index\": 0, \"ok\": true, \"rule\": { \"id\": 11, \"chain\": \"forward\", \"priority\": 0, \"protocol\": \"tcp\", \"dest_port\": 23, \"action\": \"drop\", \"stateful\": false } },\n    { \"index\": 1, \"ok\": true, \"rule\": { \"id\": 12, \"chain\": \"forward\", \"priority\": 0, \"protocol\": \"tcp\", \"dest_port\": 2323, \"action\": \"drop\", \"stateful\": false } }\n  ]\n}\n```\n\n**A partial batch** (one item is missing a required field) returns **207**, with\nthe valid items created and the bad one reported in place:\n\n```bash\ncurl -k -X POST \"$NODE/api/v1/dns/records/batch\" \\\n  -H \"Authorization: Bearer $TOKEN\" -H \"Content-Type: application/json\" \\\n  -d '{\n    \"records\": [\n      { \"zone_id\": 1, \"name\": \"a\", \"type\": \"A\", \"value\": \"10.20.0.1\" },\n      { \"zone_id\": 1, \"name\": \"b\", \"type\": \"A\" }\n    ]\n  }'\n```\n\n```json\n{\n  \"created\": 1,\n  \"failed\": 1,\n  \"results\": [\n    { \"index\": 0, \"ok\": true, \"record\": { \"id\": 20, \"zone_id\": 1, \"name\": \"a\", \"type\": \"A\", \"value\": \"10.20.0.1\", \"ttl\": 300 } },\n    { \"index\": 1, \"error\": \"zone_id, name, type and value are required\" }\n  ]\n}\n```\n\n---\n\n## Host addressing\n\nThe step between *a network reserved its gateway address* and *something on the\nhost answers on it*. Creating a network does not configure that address on an\ninterface; this is how you do it.\n\n| Method | Path | Purpose |\n|---|---|---|\n| GET | `/api/v1/nics/{iface}/addresses` | Addresses currently on an interface |\n| POST | `/api/v1/nics/{iface}/addresses` | Put an address on it |\n| DELETE | `/api/v1/nics/{iface}/addresses/{cidr}` | Remove one |\n\n```bash\ncurl -k -X POST \"$NODE/api/v1/nics/cnv-user-br0/addresses\" \\\n  -H \"Authorization: Bearer $TOKEN\" -H \"Content-Type: application/json\" \\\n  -d '{\"address\":\"10.20.0.1/24\"}'\n```\n\n```json\n{ \"status\": \"configured\", \"interface\": \"cnv-user-br0\", \"address\": \"10.20.0.1/24\",\n  \"addresses\": [\"10.20.0.1/24\", \"fe80::.../64\"] }\n```\n\nYou usually do not need to name the interface. `POST /api/v1/addresses` defaults\nto the workload bridge, which is where a managed network's gateway address\nbelongs and the only sensible answer on a normal node:\n\n```bash\ncurl -k -X POST \"$NODE/api/v1/addresses\" \\\n  -H \"Authorization: Bearer $TOKEN\" -H \"Content-Type: application/json\" \\\n  -d '{\"address\":\"10.20.0.1/24\"}'\n```\n\nName one explicitly — in the path or as `\"interface\"` in the body — when the node\nhas a second bridge or a dedicated interface.\n\nThe prefix is **required** — a bare address is refused rather than guessed. Assuming\n`/32` where you meant `/24` produces an interface that looks correct and cannot\nreach its own subnet.\n\nAdding is **idempotent**, so you can reconcile toward a desired state instead of\ntracking what you have already done.\n\n**The management interface is refused.** It carries the address this node is\nreached on, and a call that strips it is not recoverable without console access.\nReads are allowed on every interface — seeing the node's own addressing is what a\npanel needs to render a network page, and reading cannot strand anything.\n\nTo delete, URL-escape the prefix:\n\n```bash\ncurl -k -X DELETE \"$NODE/api/v1/nics/cnv-user-br0/addresses/10.20.0.1%2F24\" \\\n  -H \"Authorization: Bearer $TOKEN\"\n```\n\n## DHCP scopes\n\nA network's address pool exists from the moment you create it. The DHCP server\nstill will not answer for that subnet until a **scope** binds serving to the\npool. Without one the server has nothing to offer and, per RFC 2131, stays\nsilent — the client retries with no reply, which looks exactly like a broken\nconnection.\n\n| Method | Path | Purpose |\n|---|---|---|\n| GET | `/api/v1/dhcp/scopes` | List scopes |\n| POST | `/api/v1/dhcp/scopes` | Bind serving for a subnet to a pool |\n| DELETE | `/api/v1/dhcp/scopes/{subnet}` | Remove a scope |\n\n```bash\ncurl -k -X POST \"$NODE/api/v1/dhcp/scopes\" \\\n  -H \"Authorization: Bearer $TOKEN\" -H \"Content-Type: application/json\" \\\n  -d '{\"subnet\":\"10.20.0.0/24\",\"pool_id\":16,\"gateway\":\"10.20.0.1\",\n       \"subnet_mask\":\"255.255.255.0\",\"dns\":[\"1.1.1.1\"],\"lease_seconds\":3600}'\n```\n\n`subnet` and `pool_id` are required. Find the pool with\n`GET /api/v1/ipam/pools` — a network's pool carries the network's name. A scope\nwith no pool is refused, because it would reproduce exactly the silent failure\nthis exists to prevent.\n\nDeleting takes the subnet URL-escaped: `.../dhcp/scopes/10.20.0.0%2F24`.\n\n## Reservations, node identity and services\n\nThe last of what used to need a shell.\n\n### Static DHCP reservations\n\nPin an address to a MAC so a workload always gets the same one.\n\n| Method | Path | Purpose |\n|---|---|---|\n| GET | `/api/v1/dhcp/reservations` | List reservations |\n| POST | `/api/v1/dhcp/reservations` | Pin an address to a MAC |\n| DELETE | `/api/v1/dhcp/reservations/{mac}` | Release one |\n\n```bash\ncurl -k -X POST \"$NODE/api/v1/dhcp/reservations\" \\\n  -H \"Authorization: Bearer $TOKEN\" -H \"Content-Type: application/json\" \\\n  -d '{\"mac\":\"02:ce:0a:4d:00:02\",\"ip\":\"10.20.0.50\",\"hostname\":\"db-1\"}'\n```\n\nThe address is matched to its pool from the address itself, so a reservation does\nnot name a network — and an address outside every configured range is refused\nrather than stored against nothing.\n\n### Node identity\n\n`GET /api/v1/node` returns the **hardware id** this node's licence and\ncertificates are bound to, plus its version.\n\n```bash\ncurl -k \"$NODE/api/v1/node\" -H \"Authorization: Bearer $TOKEN\"\n```\n\n```json\n{ \"hardware_id\": \"b57bd87...\", \"hardware_id_available\": true,\n  \"mode\": \"gateway\", \"version\": \"1.0.0-rc.74\" }\n```\n\nThis is the value you match a pending activation or certificate request against\nbefore approving it. `hardware_id_available` is false where the id could not be\nderived — on a virtual machine, where the binding inputs are not stable. Show\nthat rather than presenting the value as an identity.\n\n### Services\n\n`GET /api/v1/service` reports each network service: the operator's switch, the\naddress it binds, and whether it is actually listening.\n\n```bash\ncurl -k \"$NODE/api/v1/service\" -H \"Authorization: Bearer $TOKEN\"\n```\n\nRead-only, deliberately. A switch takes effect at the next agent restart, and an\nAPI that flips one without being able to restart the agent would leave you unable\nto tell whether anything happened. Toggling stays local until the agent can apply\nit live.\n\n## Subsystem reads\n\nThese subsystems are configured from the CLI and can now be **read** over the\nAPI, so a panel can show what a node actually has without shelling in. Each\nreturns the same view its CLI command shows — they call the same manager.\n\n| Method | Path | Purpose |\n|---|---|---|\n| GET | `/api/v1/l7lb/status` | Layer-7 balancer state |\n| GET | `/api/v1/l7lb/pools` | Backend pools |\n| GET | `/api/v1/l7lb/frontends` | Frontends (host/path routing, TLS) |\n| GET | `/api/v1/vrf` | Virtual routing & forwarding devices |\n| GET | `/api/v1/geneve` | Geneve overlay tunnels |\n| GET | `/api/v1/nat64/status` | NAT64 configuration + live bindings |\n| GET | `/api/v1/plugins` | Installed plugins |\n| GET | `/api/v1/apikeys` | Operator API keys — metadata only |\n\n```bash\ncurl -k \"$NODE/api/v1/l7lb/status\" -H \"Authorization: Bearer $TOKEN\"\ncurl -k \"$NODE/api/v1/vrf\"        -H \"Authorization: Bearer $TOKEN\"\n```\n\nA subsystem that is not running on this node answers **503** naming it, rather\nthan an empty list — \"not available here\" and \"none configured\" are different\nanswers and a panel should be able to tell them apart.\n\n`/apikeys` returns id, label, scope and timestamps. It never returns key\nmaterial: a minted key is shown once at creation and is not recoverable.\n\n**Creating and changing these is still CLI-only.** Their mutating commands take\npositional arguments rather than a JSON body, and exposing them as a passthrough\nwould have locked in an awkward shape; they will get designed request bodies\nrather than a quick wrapper.\n\n## Metrics scrape\n\n`GET /api/v1/metrics` returns the node's metrics in **Prometheus text exposition\nformat**, behind the same `api_token` bearer as the rest of the API — so a\nPrometheus scrape config authenticates like any other client. It is **read-only**\n(a `GET`, never blocked by a license freeze) and simply renders already-collected\ncounters and gauges; it never touches the data plane.\n\n> This is **distinct** from the optional standalone metrics listener (the loopback\n> `metrics_bind_addr`, default `127.0.0.1:9090`, described in\n> [Configuration](/docs/configuration)). Both expose the **same** `stratum_*`\n> metric set; this endpoint surfaces it on the authenticated REST API so you can\n> scrape it over the management port without opening a second listener.\n\n```bash\ncurl -k \"$NODE/api/v1/metrics\" -H \"Authorization: Bearer $TOKEN\"\n```\n\nUnlike the JSON endpoints, the response is\n`Content-Type: text/plain; version=0.0.4; charset=utf-8` — the standard Prometheus\nexposition body, one `# HELP`/`# TYPE` header per metric family followed by its\nsamples:\n\n```\n# HELP stratum_uptime_seconds Agent uptime in seconds\n# TYPE stratum_uptime_seconds gauge\nstratum_uptime_seconds 86400\n# HELP stratum_active_tasks Active internal tasks\n# TYPE stratum_active_tasks gauge\nstratum_active_tasks 42\n# HELP stratum_nic_rx_bytes Total received bytes per NIC\n# TYPE stratum_nic_rx_bytes counter\nstratum_nic_rx_bytes{interface=\"cnv-nic-0\"} 481920512\n# HELP stratum_firewall_drops_total Total firewall drops\n# TYPE stratum_firewall_drops_total counter\nstratum_firewall_drops_total{chain=\"forward\",reason=\"acl\"} 1204\n```\n\nThe exposition covers the families the node already collects: runtime gauges\n(`stratum_uptime_seconds`, `stratum_active_tasks`), per-VM byte/packet counters,\nper-bridge connection gauges, per-NIC rx/tx byte counters, per-reason\nfirewall-drop counters, load-balancer active-connection gauges, BGP session-state\nand cluster-state gauges, and an alerts-fired counter.\n\nA minimal Prometheus scrape configuration:\n\n```yaml\nscrape_configs:\n  - job_name: cenvero-stratum\n    scheme: https\n    metrics_path: /api/v1/metrics\n    authorization:\n      credentials: <your-api-token>\n    tls_config:\n      insecure_skip_verify: true   # node cert is privately managed; pin it in production\n    static_configs:\n      - targets: [\"node.example.com:7070\"]\n```\n\n---\n\n## gRPC endpoint\n\nThe node also exposes a gRPC endpoint:\n\n```\n<node-ip>:7071\n```\n\nEnable it with `cenvero-str-ctl service grpc on`. It uses the **same TLS\ncertificate** as REST/WebSocket and requires the **node API token** in the\n`authorization` metadata on every call (`authorization: Bearer <api-token>`). The\nnode may optionally require a **client certificate (mTLS)** when configured.\n\nThe gRPC port serves the **standard gRPC Health Checking protocol**\n(`grpc.health.v1.Health`), so load balancers and orchestration systems can probe\nnode liveness over gRPC. The full management surface is the **REST API** documented\nabove — gRPC is for health/liveness probing, **not** a REST mirror.\n\n```bash\ngrpcurl -H \"authorization: Bearer $TOKEN\" \\\n  node.example.net:7071 grpc.health.v1.Health/Check\n```\n\n```json\n{ \"status\": \"SERVING\" }\n```\n\n> Server reflection is intentionally disabled; supply the standard\n> `grpc.health.v1` descriptor (or use a purpose-built health-probe client). Use the\n> empty service name for overall node health, or `cenvero.stratum` for the agent's\n> service.\n\n---\n\n## Every endpoint, ready to paste\n\nOne line per endpoint, generated from the routes the agent actually serves — so\nit cannot drift from the code. Set these once and the rest copies straight out:\n\n```bash\nNODE=https://your-node:7070\nTOKEN=your-api-token\n```\n\nPlaceholders in braces are yours to fill. Request bodies for the mutating calls\nare in the sections above; this is the shape and the address, not a replacement\nfor them.\n\n### Accounting & billing data\n\n```bash\ncurl -k \"$NODE/api/v1/accounting/95th\" -H \"Authorization: Bearer $TOKEN\"\ncurl -k \"$NODE/api/v1/accounting/billing\" -H \"Authorization: Bearer $TOKEN\"\ncurl -k \"$NODE/api/v1/accounting/summary\" -H \"Authorization: Bearer $TOKEN\"\n```\n\n### Host addressing (default interface)\n\n```bash\ncurl -k -X POST \"$NODE/api/v1/addresses\" \\\n  -H \"Authorization: Bearer $TOKEN\" -H \"Content-Type: application/json\" -d '{}'\n```\n\n### Alerting\n\n```bash\ncurl -k \"$NODE/api/v1/alerts\" -H \"Authorization: Bearer $TOKEN\"\ncurl -k -X POST \"$NODE/api/v1/alerts\" \\\n  -H \"Authorization: Bearer $TOKEN\" -H \"Content-Type: application/json\" -d '{}'\ncurl -k \"$NODE/api/v1/alerts/conditions\" -H \"Authorization: Bearer $TOKEN\"\ncurl -k -X POST \"$NODE/api/v1/alerts/conditions\" \\\n  -H \"Authorization: Bearer $TOKEN\" -H \"Content-Type: application/json\" -d '{}'\ncurl -k -X DELETE \"$NODE/api/v1/alerts/conditions/{id}\" -H \"Authorization: Bearer $TOKEN\"\ncurl -k -X POST \"$NODE/api/v1/alerts/conditions/{id}/actions\" \\\n  -H \"Authorization: Bearer $TOKEN\" -H \"Content-Type: application/json\" -d '{}'\ncurl -k \"$NODE/api/v1/alerts/history\" -H \"Authorization: Bearer $TOKEN\"\ncurl -k -X POST \"$NODE/api/v1/alerts/{id}/ack\" \\\n  -H \"Authorization: Bearer $TOKEN\" -H \"Content-Type: application/json\" -d '{}'\n```\n\n### Operator API keys\n\n```bash\ncurl -k \"$NODE/api/v1/apikeys\" -H \"Authorization: Bearer $TOKEN\"\n```\n\n### Audit\n\n```bash\ncurl -k \"$NODE/api/v1/audit\" -H \"Authorization: Bearer $TOKEN\"   # 501 — see Not implemented\n```\n\n### Backups\n\n```bash\ncurl -k \"$NODE/api/v1/backups\" -H \"Authorization: Bearer $TOKEN\"\ncurl -k -X POST \"$NODE/api/v1/backups\" \\\n  -H \"Authorization: Bearer $TOKEN\" -H \"Content-Type: application/json\" -d '{}'\ncurl -k -X POST \"$NODE/api/v1/backups/restore\" \\\n  -H \"Authorization: Bearer $TOKEN\" -H \"Content-Type: application/json\" -d '{}'\ncurl -k \"$NODE/api/v1/backups/schedules\" -H \"Authorization: Bearer $TOKEN\"\ncurl -k -X POST \"$NODE/api/v1/backups/schedules\" \\\n  -H \"Authorization: Bearer $TOKEN\" -H \"Content-Type: application/json\" -d '{}'\ncurl -k -X DELETE \"$NODE/api/v1/backups/schedules/{id}\" -H \"Authorization: Bearer $TOKEN\"\n```\n\n### Bandwidth & quotas\n\n```bash\ncurl -k \"$NODE/api/v1/bandwidth\" -H \"Authorization: Bearer $TOKEN\"\ncurl -k -X POST \"$NODE/api/v1/bandwidth\" \\\n  -H \"Authorization: Bearer $TOKEN\" -H \"Content-Type: application/json\" -d '{}'\ncurl -k -X POST \"$NODE/api/v1/bandwidth/pools\" \\\n  -H \"Authorization: Bearer $TOKEN\" -H \"Content-Type: application/json\" -d '{}'\ncurl -k -X POST \"$NODE/api/v1/bandwidth/pools/members\" \\\n  -H \"Authorization: Bearer $TOKEN\" -H \"Content-Type: application/json\" -d '{}'\ncurl -k -X DELETE \"$NODE/api/v1/bandwidth/pools/{id}\" -H \"Authorization: Bearer $TOKEN\"\ncurl -k -X DELETE \"$NODE/api/v1/bandwidth/pools/{id}/members/{mac}\" -H \"Authorization: Bearer $TOKEN\"\ncurl -k \"$NODE/api/v1/bandwidth/quotas\" -H \"Authorization: Bearer $TOKEN\"\ncurl -k -X POST \"$NODE/api/v1/bandwidth/quotas\" \\\n  -H \"Authorization: Bearer $TOKEN\" -H \"Content-Type: application/json\" -d '{}'\ncurl -k \"$NODE/api/v1/bandwidth/quotas/{mac}\" -H \"Authorization: Bearer $TOKEN\"\ncurl -k -X DELETE \"$NODE/api/v1/bandwidth/{id}\" -H \"Authorization: Bearer $TOKEN\"\n```\n\n### BGP\n\n```bash\ncurl -k -X POST \"$NODE/api/v1/bgp/announce\" \\\n  -H \"Authorization: Bearer $TOKEN\" -H \"Content-Type: application/json\" -d '{}'\ncurl -k \"$NODE/api/v1/bgp/neighbors\" -H \"Authorization: Bearer $TOKEN\"\ncurl -k -X POST \"$NODE/api/v1/bgp/neighbors\" \\\n  -H \"Authorization: Bearer $TOKEN\" -H \"Content-Type: application/json\" -d '{}'\ncurl -k -X DELETE \"$NODE/api/v1/bgp/neighbors/{addr}\" -H \"Authorization: Bearer $TOKEN\"\ncurl -k \"$NODE/api/v1/bgp/policy\" -H \"Authorization: Bearer $TOKEN\"\ncurl -k -X POST \"$NODE/api/v1/bgp/policy/{dir}\" \\\n  -H \"Authorization: Bearer $TOKEN\" -H \"Content-Type: application/json\" -d '{}'\ncurl -k \"$NODE/api/v1/bgp/prefix-lists\" -H \"Authorization: Bearer $TOKEN\"\ncurl -k -X POST \"$NODE/api/v1/bgp/prefix-lists\" \\\n  -H \"Authorization: Bearer $TOKEN\" -H \"Content-Type: application/json\" -d '{}'\ncurl -k \"$NODE/api/v1/bgp/route-maps\" -H \"Authorization: Bearer $TOKEN\"\ncurl -k -X POST \"$NODE/api/v1/bgp/route-maps\" \\\n  -H \"Authorization: Bearer $TOKEN\" -H \"Content-Type: application/json\" -d '{}'\ncurl -k \"$NODE/api/v1/bgp/routes\" -H \"Authorization: Bearer $TOKEN\"\ncurl -k \"$NODE/api/v1/bgp/status\" -H \"Authorization: Bearer $TOKEN\"\ncurl -k -X POST \"$NODE/api/v1/bgp/withdraw\" \\\n  -H \"Authorization: Bearer $TOKEN\" -H \"Content-Type: application/json\" -d '{}'\n```\n\n### Billing automation\n\n```bash\ncurl -k \"$NODE/api/v1/billing/tenants/{id}\" -H \"Authorization: Bearer $TOKEN\"\ncurl -k -X POST \"$NODE/api/v1/billing/tenants/{id}/limit\" \\\n  -H \"Authorization: Bearer $TOKEN\" -H \"Content-Type: application/json\" -d '{}'\ncurl -k -X POST \"$NODE/api/v1/billing/tenants/{id}/resume\" \\\n  -H \"Authorization: Bearer $TOKEN\" -H \"Content-Type: application/json\" -d '{}'\ncurl -k -X POST \"$NODE/api/v1/billing/tenants/{id}/suspend\" \\\n  -H \"Authorization: Bearer $TOKEN\" -H \"Content-Type: application/json\" -d '{}'\ncurl -k -X POST \"$NODE/api/v1/billing/tenants/{id}/unlimit\" \\\n  -H \"Authorization: Bearer $TOKEN\" -H \"Content-Type: application/json\" -d '{}'\n```\n\n### NIC bonds\n\n```bash\ncurl -k \"$NODE/api/v1/bonds\" -H \"Authorization: Bearer $TOKEN\"\ncurl -k -X POST \"$NODE/api/v1/bonds\" \\\n  -H \"Authorization: Bearer $TOKEN\" -H \"Content-Type: application/json\" -d '{}'\ncurl -k -X DELETE \"$NODE/api/v1/bonds/{id}\" -H \"Authorization: Bearer $TOKEN\"\ncurl -k \"$NODE/api/v1/bonds/{id}\" -H \"Authorization: Bearer $TOKEN\"\ncurl -k -X POST \"$NODE/api/v1/bonds/{id}/members\" \\\n  -H \"Authorization: Bearer $TOKEN\" -H \"Content-Type: application/json\" -d '{}'\ncurl -k -X DELETE \"$NODE/api/v1/bonds/{id}/members/{iface}\" -H \"Authorization: Bearer $TOKEN\"\ncurl -k -X PUT \"$NODE/api/v1/bonds/{id}/mtu\" \\\n  -H \"Authorization: Bearer $TOKEN\" -H \"Content-Type: application/json\" -d '{}'\n```\n\n### Bridges\n\n```bash\ncurl -k \"$NODE/api/v1/bridges\" -H \"Authorization: Bearer $TOKEN\"\ncurl -k -X POST \"$NODE/api/v1/bridges\" \\\n  -H \"Authorization: Bearer $TOKEN\" -H \"Content-Type: application/json\" -d '{}'\ncurl -k -X DELETE \"$NODE/api/v1/bridges/{name}\" -H \"Authorization: Bearer $TOKEN\"\ncurl -k -X POST \"$NODE/api/v1/bridges/{name}/ports\" \\\n  -H \"Authorization: Bearer $TOKEN\" -H \"Content-Type: application/json\" -d '{}'\ncurl -k -X DELETE \"$NODE/api/v1/bridges/{name}/ports/{iface}\" -H \"Authorization: Bearer $TOKEN\"\n```\n\n### Cluster\n\n```bash\ncurl -k \"$NODE/api/v1/cluster\" -H \"Authorization: Bearer $TOKEN\"\ncurl -k -X POST \"$NODE/api/v1/cluster/join\" \\\n  -H \"Authorization: Bearer $TOKEN\" -H \"Content-Type: application/json\" -d '{}'\ncurl -k -X POST \"$NODE/api/v1/cluster/leave\" \\\n  -H \"Authorization: Bearer $TOKEN\" -H \"Content-Type: application/json\" -d '{}'\ncurl -k -X POST \"$NODE/api/v1/cluster/profile\" \\   # 501 — see Not implemented\n  -H \"Authorization: Bearer $TOKEN\" -H \"Content-Type: application/json\" -d '{}'\ncurl -k \"$NODE/api/v1/cluster/state\" -H \"Authorization: Bearer $TOKEN\"\ncurl -k \"$NODE/api/v1/cluster/status\" -H \"Authorization: Bearer $TOKEN\"\n```\n\n### Configuration\n\n```bash\ncurl -k -X POST \"$NODE/api/v1/config/batch\" \\   # 501 — see Not implemented\n  -H \"Authorization: Bearer $TOKEN\" -H \"Content-Type: application/json\" -d '{}'\n```\n\n### Container networking\n\n```bash\ncurl -k \"$NODE/api/v1/containers\" -H \"Authorization: Bearer $TOKEN\"\ncurl -k -X POST \"$NODE/api/v1/containers/attach\" \\\n  -H \"Authorization: Bearer $TOKEN\" -H \"Content-Type: application/json\" -d '{}'\ncurl -k \"$NODE/api/v1/containers/{id}\" -H \"Authorization: Bearer $TOKEN\"\ncurl -k -X POST \"$NODE/api/v1/containers/{id}/detach\" \\\n  -H \"Authorization: Bearer $TOKEN\" -H \"Content-Type: application/json\" -d '{}'\n```\n\n### Dashboard\n\n```bash\ncurl -k \"$NODE/api/v1/dashboard\" -H \"Authorization: Bearer $TOKEN\"\n```\n\n### DHCP\n\n```bash\ncurl -k \"$NODE/api/v1/dhcp/leases\" -H \"Authorization: Bearer $TOKEN\"\ncurl -k \"$NODE/api/v1/dhcp/reservations\" -H \"Authorization: Bearer $TOKEN\"\ncurl -k -X POST \"$NODE/api/v1/dhcp/reservations\" \\\n  -H \"Authorization: Bearer $TOKEN\" -H \"Content-Type: application/json\" -d '{}'\ncurl -k -X DELETE \"$NODE/api/v1/dhcp/reservations/{mac}\" -H \"Authorization: Bearer $TOKEN\"\ncurl -k \"$NODE/api/v1/dhcp/scopes\" -H \"Authorization: Bearer $TOKEN\"\ncurl -k -X POST \"$NODE/api/v1/dhcp/scopes\" \\\n  -H \"Authorization: Bearer $TOKEN\" -H \"Content-Type: application/json\" -d '{}'\ncurl -k -X DELETE \"$NODE/api/v1/dhcp/scopes/{subnet}\" -H \"Authorization: Bearer $TOKEN\"\n```\n\n### DNS\n\n```bash\ncurl -k \"$NODE/api/v1/dns/dnssec/{zone}\" -H \"Authorization: Bearer $TOKEN\"\ncurl -k \"$NODE/api/v1/dns/records\" -H \"Authorization: Bearer $TOKEN\"\ncurl -k -X POST \"$NODE/api/v1/dns/records\" \\\n  -H \"Authorization: Bearer $TOKEN\" -H \"Content-Type: application/json\" -d '{}'\ncurl -k -X POST \"$NODE/api/v1/dns/records/batch\" \\\n  -H \"Authorization: Bearer $TOKEN\" -H \"Content-Type: application/json\" -d '{}'\ncurl -k -X DELETE \"$NODE/api/v1/dns/records/{id}\" -H \"Authorization: Bearer $TOKEN\"\ncurl -k -X PUT \"$NODE/api/v1/dns/records/{id}\" \\\n  -H \"Authorization: Bearer $TOKEN\" -H \"Content-Type: application/json\" -d '{}'\ncurl -k \"$NODE/api/v1/dns/zones\" -H \"Authorization: Bearer $TOKEN\"\ncurl -k -X POST \"$NODE/api/v1/dns/zones\" \\\n  -H \"Authorization: Bearer $TOKEN\" -H \"Content-Type: application/json\" -d '{}'\ncurl -k -X DELETE \"$NODE/api/v1/dns/zones/{id}\" -H \"Authorization: Bearer $TOKEN\"\n```\n\n### Endpoint index\n\n```bash\ncurl -k \"$NODE/api/v1/docs\" -H \"Authorization: Bearer $TOKEN\"\n```\n\n### Event stream\n\n```bash\ncurl -k \"$NODE/api/v1/events\" -H \"Authorization: Bearer $TOKEN\"\n```\n\n### Floating IPs\n\n```bash\ncurl -k \"$NODE/api/v1/float\" -H \"Authorization: Bearer $TOKEN\"\ncurl -k -X POST \"$NODE/api/v1/float\" \\\n  -H \"Authorization: Bearer $TOKEN\" -H \"Content-Type: application/json\" -d '{}'\ncurl -k -X DELETE \"$NODE/api/v1/float/{id}\" -H \"Authorization: Bearer $TOKEN\"\ncurl -k \"$NODE/api/v1/float/{id}\" -H \"Authorization: Bearer $TOKEN\"\n```\n\n### Flow tracking\n\n```bash\ncurl -k \"$NODE/api/v1/flows\" -H \"Authorization: Bearer $TOKEN\"\ncurl -k \"$NODE/api/v1/flows/export\" -H \"Authorization: Bearer $TOKEN\"\ncurl -k \"$NODE/api/v1/flows/stats\" -H \"Authorization: Bearer $TOKEN\"\n```\n\n### Port forwarding\n\n```bash\ncurl -k \"$NODE/api/v1/forward\" -H \"Authorization: Bearer $TOKEN\"\ncurl -k -X POST \"$NODE/api/v1/forward\" \\\n  -H \"Authorization: Bearer $TOKEN\" -H \"Content-Type: application/json\" -d '{}'\ncurl -k -X DELETE \"$NODE/api/v1/forward/{id}\" -H \"Authorization: Bearer $TOKEN\"\ncurl -k \"$NODE/api/v1/forward/{id}\" -H \"Authorization: Bearer $TOKEN\"\n```\n\n### Gateway & NAT\n\n```bash\ncurl -k -X POST \"$NODE/api/v1/gateway/failback\" \\\n  -H \"Authorization: Bearer $TOKEN\" -H \"Content-Type: application/json\" -d '{}'\ncurl -k -X POST \"$NODE/api/v1/gateway/failover\" \\\n  -H \"Authorization: Bearer $TOKEN\" -H \"Content-Type: application/json\" -d '{}'\ncurl -k \"$NODE/api/v1/gateway/snat\" -H \"Authorization: Bearer $TOKEN\"\ncurl -k -X POST \"$NODE/api/v1/gateway/snat\" \\\n  -H \"Authorization: Bearer $TOKEN\" -H \"Content-Type: application/json\" -d '{}'\ncurl -k -X DELETE \"$NODE/api/v1/gateway/snat/{id}\" -H \"Authorization: Bearer $TOKEN\"\ncurl -k \"$NODE/api/v1/gateway/status\" -H \"Authorization: Bearer $TOKEN\"\n```\n\n### Geneve tunnels\n\n```bash\ncurl -k \"$NODE/api/v1/geneve\" -H \"Authorization: Bearer $TOKEN\"\n```\n\n### Self-healing\n\n```bash\ncurl -k \"$NODE/api/v1/heal\" -H \"Authorization: Bearer $TOKEN\"\ncurl -k -X POST \"$NODE/api/v1/heal/check\" \\\n  -H \"Authorization: Bearer $TOKEN\" -H \"Content-Type: application/json\" -d '{}'\n```\n\n### Health\n\n```bash\ncurl -k \"$NODE/api/v1/health\" -H \"Authorization: Bearer $TOKEN\"\n```\n\n### Address pools\n\n```bash\ncurl -k -X POST \"$NODE/api/v1/ipam/allocate\" \\\n  -H \"Authorization: Bearer $TOKEN\" -H \"Content-Type: application/json\" -d '{}'\ncurl -k \"$NODE/api/v1/ipam/allocations\" -H \"Authorization: Bearer $TOKEN\"\ncurl -k \"$NODE/api/v1/ipam/pools\" -H \"Authorization: Bearer $TOKEN\"\ncurl -k -X POST \"$NODE/api/v1/ipam/pools\" \\\n  -H \"Authorization: Bearer $TOKEN\" -H \"Content-Type: application/json\" -d '{}'\ncurl -k -X DELETE \"$NODE/api/v1/ipam/pools/{id}\" -H \"Authorization: Bearer $TOKEN\"\ncurl -k -X POST \"$NODE/api/v1/ipam/release\" \\\n  -H \"Authorization: Bearer $TOKEN\" -H \"Content-Type: application/json\" -d '{}'\n```\n\n### Layer-7 load balancer\n\n```bash\ncurl -k \"$NODE/api/v1/l7lb/frontends\" -H \"Authorization: Bearer $TOKEN\"\ncurl -k \"$NODE/api/v1/l7lb/pools\" -H \"Authorization: Bearer $TOKEN\"\ncurl -k \"$NODE/api/v1/l7lb/status\" -H \"Authorization: Bearer $TOKEN\"\n```\n\n### Layer-4 load balancer\n\n```bash\ncurl -k \"$NODE/api/v1/lb\" -H \"Authorization: Bearer $TOKEN\"\ncurl -k -X POST \"$NODE/api/v1/lb\" \\\n  -H \"Authorization: Bearer $TOKEN\" -H \"Content-Type: application/json\" -d '{}'\ncurl -k -X DELETE \"$NODE/api/v1/lb/{id}\" -H \"Authorization: Bearer $TOKEN\"\ncurl -k \"$NODE/api/v1/lb/{id}\" -H \"Authorization: Bearer $TOKEN\"\ncurl -k -X PUT \"$NODE/api/v1/lb/{id}\" \\\n  -H \"Authorization: Bearer $TOKEN\" -H \"Content-Type: application/json\" -d '{}'\ncurl -k -X POST \"$NODE/api/v1/lb/{id}/backends\" \\\n  -H \"Authorization: Bearer $TOKEN\" -H \"Content-Type: application/json\" -d '{}'\ncurl -k -X DELETE \"$NODE/api/v1/lb/{id}/backends/{bid}\" -H \"Authorization: Bearer $TOKEN\"\ncurl -k -X POST \"$NODE/api/v1/lb/{id}/backends/{bid}/health\" \\\n  -H \"Authorization: Bearer $TOKEN\" -H \"Content-Type: application/json\" -d '{}'\n```\n\n### Licence\n\n```bash\ncurl -k \"$NODE/api/v1/license\" -H \"Authorization: Bearer $TOKEN\"\n```\n\n### MAC bindings\n\n```bash\ncurl -k \"$NODE/api/v1/macbind\" -H \"Authorization: Bearer $TOKEN\"\ncurl -k -X POST \"$NODE/api/v1/macbind\" \\\n  -H \"Authorization: Bearer $TOKEN\" -H \"Content-Type: application/json\" -d '{}'\ncurl -k -X DELETE \"$NODE/api/v1/macbind/{mac}\" -H \"Authorization: Bearer $TOKEN\"\n```\n\n### Metrics\n\n```bash\ncurl -k \"$NODE/api/v1/metrics\" -H \"Authorization: Bearer $TOKEN\"\n```\n\n### NAT64\n\n```bash\ncurl -k \"$NODE/api/v1/nat64/status\" -H \"Authorization: Bearer $TOKEN\"\n```\n\n### Networks & endpoints\n\n```bash\ncurl -k \"$NODE/api/v1/networks\" -H \"Authorization: Bearer $TOKEN\"\ncurl -k -X POST \"$NODE/api/v1/networks\" \\\n  -H \"Authorization: Bearer $TOKEN\" -H \"Content-Type: application/json\" -d '{}'\ncurl -k -X DELETE \"$NODE/api/v1/networks/{id}\" -H \"Authorization: Bearer $TOKEN\"\ncurl -k \"$NODE/api/v1/networks/{id}\" -H \"Authorization: Bearer $TOKEN\"\ncurl -k -X POST \"$NODE/api/v1/networks/{id}/attach\" \\\n  -H \"Authorization: Bearer $TOKEN\" -H \"Content-Type: application/json\" -d '{}'\ncurl -k \"$NODE/api/v1/networks/{id}/endpoints\" -H \"Authorization: Bearer $TOKEN\"\ncurl -k -X POST \"$NODE/api/v1/networks/{id}/endpoints/{eid}/detach\" \\\n  -H \"Authorization: Bearer $TOKEN\" -H \"Content-Type: application/json\" -d '{}'\ncurl -k -X PUT \"$NODE/api/v1/networks/{id}/endpoints/{eid}/mac\" \\\n  -H \"Authorization: Bearer $TOKEN\" -H \"Content-Type: application/json\" -d '{}'\n```\n\n### Interfaces & addressing\n\n```bash\ncurl -k \"$NODE/api/v1/nics\" -H \"Authorization: Bearer $TOKEN\"\ncurl -k \"$NODE/api/v1/nics/{iface}/addresses\" -H \"Authorization: Bearer $TOKEN\"\ncurl -k -X POST \"$NODE/api/v1/nics/{iface}/addresses\" \\\n  -H \"Authorization: Bearer $TOKEN\" -H \"Content-Type: application/json\" -d '{}'\ncurl -k -X DELETE \"$NODE/api/v1/nics/{iface}/addresses/{cidr}\" -H \"Authorization: Bearer $TOKEN\"\n```\n\n### Node identity\n\n```bash\ncurl -k \"$NODE/api/v1/node\" -H \"Authorization: Bearer $TOKEN\"\n```\n\n### Plugins\n\n```bash\ncurl -k \"$NODE/api/v1/plugins\" -H \"Authorization: Bearer $TOKEN\"\n```\n\n### Routing\n\n```bash\ncurl -k \"$NODE/api/v1/routes\" -H \"Authorization: Bearer $TOKEN\"\ncurl -k -X POST \"$NODE/api/v1/routes\" \\\n  -H \"Authorization: Bearer $TOKEN\" -H \"Content-Type: application/json\" -d '{}'\ncurl -k \"$NODE/api/v1/routes/rules\" -H \"Authorization: Bearer $TOKEN\"\ncurl -k -X POST \"$NODE/api/v1/routes/rules\" \\\n  -H \"Authorization: Bearer $TOKEN\" -H \"Content-Type: application/json\" -d '{}'\ncurl -k -X DELETE \"$NODE/api/v1/routes/rules/{id}\" -H \"Authorization: Bearer $TOKEN\"\ncurl -k -X DELETE \"$NODE/api/v1/routes/{id}\" -H \"Authorization: Bearer $TOKEN\"\n```\n\n### Firewall rules\n\n```bash\ncurl -k \"$NODE/api/v1/rules\" -H \"Authorization: Bearer $TOKEN\"\ncurl -k -X POST \"$NODE/api/v1/rules\" \\\n  -H \"Authorization: Bearer $TOKEN\" -H \"Content-Type: application/json\" -d '{}'\ncurl -k -X POST \"$NODE/api/v1/rules/batch\" \\\n  -H \"Authorization: Bearer $TOKEN\" -H \"Content-Type: application/json\" -d '{}'\ncurl -k -X DELETE \"$NODE/api/v1/rules/{id}\" -H \"Authorization: Bearer $TOKEN\"\ncurl -k \"$NODE/api/v1/rules/{id}\" -H \"Authorization: Bearer $TOKEN\"\ncurl -k -X PUT \"$NODE/api/v1/rules/{id}\" \\\n  -H \"Authorization: Bearer $TOKEN\" -H \"Content-Type: application/json\" -d '{}'\n```\n\n### Services\n\n```bash\ncurl -k \"$NODE/api/v1/service\" -H \"Authorization: Bearer $TOKEN\"\n```\n\n### Status\n\n```bash\ncurl -k \"$NODE/api/v1/status\" -H \"Authorization: Bearer $TOKEN\"\n```\n\n### Tenants\n\n```bash\ncurl -k \"$NODE/api/v1/tenant\" -H \"Authorization: Bearer $TOKEN\"\ncurl -k -X POST \"$NODE/api/v1/tenant\" \\\n  -H \"Authorization: Bearer $TOKEN\" -H \"Content-Type: application/json\" -d '{}'\ncurl -k -X DELETE \"$NODE/api/v1/tenant/{id}\" -H \"Authorization: Bearer $TOKEN\"\ncurl -k \"$NODE/api/v1/tenant/{id}\" -H \"Authorization: Bearer $TOKEN\"\ncurl -k -X PUT \"$NODE/api/v1/tenant/{id}\" \\\n  -H \"Authorization: Bearer $TOKEN\" -H \"Content-Type: application/json\" -d '{}'\ncurl -k \"$NODE/api/v1/tenant/{id}/keys\" -H \"Authorization: Bearer $TOKEN\"\ncurl -k -X POST \"$NODE/api/v1/tenant/{id}/keys\" \\\n  -H \"Authorization: Bearer $TOKEN\" -H \"Content-Type: application/json\" -d '{}'\ncurl -k -X DELETE \"$NODE/api/v1/tenant/{id}/keys/{kid}\" -H \"Authorization: Bearer $TOKEN\"\ncurl -k \"$NODE/api/v1/tenant/{id}/quota\" -H \"Authorization: Bearer $TOKEN\"\ncurl -k -X PUT \"$NODE/api/v1/tenant/{id}/quota\" \\\n  -H \"Authorization: Bearer $TOKEN\" -H \"Content-Type: application/json\" -d '{}'\n```\n\n### TLS\n\n```bash\ncurl -k \"$NODE/api/v1/tls/pubkey\" -H \"Authorization: Bearer $TOKEN\"\n```\n\n### Topology\n\n```bash\ncurl -k \"$NODE/api/v1/topology\" -H \"Authorization: Bearer $TOKEN\"   # 501 — see Not implemented\n```\n\n### VLAN lockdown\n\n```bash\ncurl -k \"$NODE/api/v1/vlan\" -H \"Authorization: Bearer $TOKEN\"\ncurl -k -X POST \"$NODE/api/v1/vlan\" \\\n  -H \"Authorization: Bearer $TOKEN\" -H \"Content-Type: application/json\" -d '{}'\ncurl -k -X DELETE \"$NODE/api/v1/vlan/{id}\" -H \"Authorization: Bearer $TOKEN\"\n```\n\n### VRF devices\n\n```bash\ncurl -k \"$NODE/api/v1/vrf\" -H \"Authorization: Bearer $TOKEN\"\n```\n\n### VXLAN overlays\n\n```bash\ncurl -k \"$NODE/api/v1/vxlan/fdb\" -H \"Authorization: Bearer $TOKEN\"\ncurl -k \"$NODE/api/v1/vxlan/networks\" -H \"Authorization: Bearer $TOKEN\"\ncurl -k -X POST \"$NODE/api/v1/vxlan/networks\" \\\n  -H \"Authorization: Bearer $TOKEN\" -H \"Content-Type: application/json\" -d '{}'\ncurl -k -X DELETE \"$NODE/api/v1/vxlan/networks/{vni}\" -H \"Authorization: Bearer $TOKEN\"\ncurl -k \"$NODE/api/v1/vxlan/networks/{vni}/peers\" -H \"Authorization: Bearer $TOKEN\"\ncurl -k -X POST \"$NODE/api/v1/vxlan/networks/{vni}/peers\" \\\n  -H \"Authorization: Bearer $TOKEN\" -H \"Content-Type: application/json\" -d '{}'\ncurl -k -X DELETE \"$NODE/api/v1/vxlan/networks/{vni}/peers/{host}\" -H \"Authorization: Bearer $TOKEN\"\n```\n\n### Webhooks\n\n```bash\ncurl -k \"$NODE/api/v1/webhooks\" -H \"Authorization: Bearer $TOKEN\"\ncurl -k -X POST \"$NODE/api/v1/webhooks\" \\\n  -H \"Authorization: Bearer $TOKEN\" -H \"Content-Type: application/json\" -d '{}'\ncurl -k -X DELETE \"$NODE/api/v1/webhooks/{id}\" -H \"Authorization: Bearer $TOKEN\"\ncurl -k \"$NODE/api/v1/webhooks/{id}\" -H \"Authorization: Bearer $TOKEN\"\ncurl -k -X POST \"$NODE/api/v1/webhooks/{id}/test\" \\\n  -H \"Authorization: Bearer $TOKEN\" -H \"Content-Type: application/json\" -d '{}'\n```\n## Feedback\n\nFound a gap, an inaccuracy, or something you wish this API did? We want to hear it.\nOpen a request from your account dashboard, or reach the team through the support\nchannel listed on your panel. Please include the agent version\n(`cenvero-str-ctl version`) and the endpoint in question.\n\n## See also\n\n- **[CLI Reference](/docs/cli)** — the same managers from the local `cenvero-str-ctl` command line.\n- **[Configuration](/docs/configuration)** — node config, ports, and the API settings.\n- **[Clustering](/docs/clustering/overview)** — joining nodes into a cluster.\n- **[Licensing](/docs/licensing)** — activation, renewal, and the enforcement states.\n"
        }
    ]
}