API Overview

The Circloud® NEST API allows you to programmatically manage your VPCs, subnets, pods, and tunnels.

Base URL: https://api.YOUR_DOMAIN (public, via Cloudflare tunnel) or https://localhost:8080 (local, from the machine you installed Circloud from).
Tip — switch base URL once: the examples below use $API_URL/.... Export API_URL once at the top of your shell session and every example just works against either endpoint:
# Public (requires DNS + Cloudflare configured)
export API_URL=https://api.YOUR_DOMAIN

# OR local (on the install host)
export API_URL=https://localhost:8080
Authentication: Tenant endpoints use X-Auth-Token; admin endpoints use X-Admin-Token.

Authentication

Include your tenant API token in every request:

curl -k -X GET $API_URL/vpcs \
  -H "X-Auth-Token: YOUR_API_TOKEN"

Admin endpoints require an admin token:

curl -k -X GET $API_URL/tenants \
  -H "X-Admin-Token: YOUR_ADMIN_TOKEN"

Many create endpoints also accept a token field in the JSON body. The header is still required and is the primary authentication check.

POST /admin/login Admin login

Request Body

ParameterTypeRequiredDefaultDescription
username*stringYes-Admin username
password*stringYes-Admin password

Response

200 Admin user with api_token

Login attempts (success and failure) are written to the ops log.

POST /admin/logout Admin logout (audit-log entry)

Stateless. Records a logout entry in the ops log for the admin identified by X-Admin-Token. The GUI calls this before clearing the session cookie.

Response

204 No content

POST /admin/rotate-token Rotate admin API token (self-service)

Generates a fresh API token for the calling admin. The old token stops working immediately on success — single-token rotation, no overlap window. The response body contains the new plaintext token, and the GUI auth panel updates in place; copy it from there any time.

Authentication: requires X-Admin-Token of the admin being rotated.

GUI behavior: the cloud-manager GUI resolves the admin token from the database on every request, so rotation is transparent — no pod restart, no dropped session. Update state/.credentials.json on the install host afterward so installer/update scripts keep working.

Response

200 {"username": "...", "api_token": "..."}

Tenant Endpoints (Admin)

Manage tenant lifecycle and retrieve full tenant topology. Requires X-Admin-Token.

POST /tenants/login Tenant login (returns api_token)

Request Body

ParameterTypeRequiredDefaultDescription
name*stringYes-Tenant name
password*stringYes-Tenant password

Login attempts (success and failure) are written to the ops log.

Response

200 {"id": "...", "name": "...", "api_token": "..."}

POST /tenants/logout Tenant logout (audit-log entry)

Stateless. Records a logout entry in the ops log for the tenant identified by X-API-Token. Called by the cloud-user GUI before clearing the session cookie.

Response

204 No content

POST /tenants/{tenant_id}/rotate-token Rotate tenant API token

Generates a fresh API token for the tenant. The old token stops working immediately on success — single-token rotation, no overlap window. The response body contains the new plaintext token, and the GUI auth panel updates in place; copy it from there any time.

Authentication: either X-Admin-Token (operator override) or X-API-Token of the tenant being rotated (self-service). Tenants can only rotate their own token.

Response

200 {"id": "...", "name": "...", "api_token": "..."}

POST /tenants Create tenant

Request Body

ParameterTypeRequiredDefaultDescription
name*stringYes-Tenant name (DNS-compliant)
descriptionstringNonullOptional description
password*stringYes-Login password for tenant GUI
GET /tenants List tenants

Response

200 Array of tenants

GET /tenants/{tenant_id} Get tenant with topology and status

Response

200 Tenant object with VPCs, pods, tunnels, and pod status

DELETE /tenants/{tenant_id} Delete tenant

Response

204 Tenant deleted (cascades to all resources)

Domain Endpoints (Admin)

Assign custom domains to tenants for tunnel usage.

POST /tenants/{tenant_id}/domains Add custom domain

Request Body

ParameterTypeRequiredDefaultDescription
domain*stringYes-Domain name (e.g., example.com)
GET /tenants/{tenant_id}/domains List tenant domains

Response

200 Array of domains

Provider Config (Admin)

Configure external subnet, DNS, and Cloudflare credentials.

POST /provider-config Create or update provider config

Request Body

ParameterTypeRequiredDefaultDescription
external_subnet_cidr*stringYes-External subnet CIDR
external_subnet_gateway*stringYes-Gateway for external subnet
peering_subnet_cidr*stringYes-Peering subnet for core services
pod_dns_nameserversarrayNo[]External DNS resolvers
pod_dns_ndotsintegerNonullDNS ndots setting
cloudflare_account_idstringNonullCloudflare account ID
cloudflare_api_tokenstringNonullCloudflare API token
cloudflare_domainstringNonullDefault tunnel domain
GET /provider-config Get provider config

Response

200 Provider configuration

VPC Endpoints

Create and manage Virtual Private Clouds.

POST /vpcs Create a VPC

Request Body

ParameterTypeRequiredDefaultDescription
tenant_id*stringYes-Your tenant UUID
token*stringYes-Your API token
name*stringYes-VPC name (DNS-compliant)
enable_internet*booleanYes-Enable internet access

Example

curl -k -X POST $API_URL/vpcs \
  -H "X-Auth-Token: YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "tenant_id": "YOUR_TENANT_ID",
    "token": "YOUR_TOKEN",
    "name": "production",
    "enable_internet": true
  }'
GET /vpcs List all VPCs

Response

200 Array of VPCs

DELETE /vpcs/{vpc_id} Delete a VPC

Response

204 VPC deleted (cascades to subnets and pods)

Subnet Endpoints

Create and manage subnets within VPCs.

POST /subnets Create a subnet

Request Body

ParameterTypeRequiredDefaultDescription
tenant_id*stringYes-Your tenant UUID
token*stringYes-Your API token
vpc_id*stringYes-Parent VPC UUID
name*stringYes-Subnet name
cidr*stringYes-CIDR block (e.g., "10.100.1.0/24")
gateway_ip*stringYes-Gateway IP within CIDR

Example

curl -k -X POST $API_URL/subnets \
  -H "X-Auth-Token: YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "tenant_id": "YOUR_TENANT_ID",
    "token": "YOUR_TOKEN",
    "vpc_id": "VPC_ID",
    "name": "web-tier",
    "cidr": "10.100.1.0/24",
    "gateway_ip": "10.100.1.1"
  }'
GET /subnets List all subnets

Response

200 Array of subnets

Security Rule Endpoints

Per-VPC firewall for tenant workloads. A rule is a direction (ingress or egress), an action (allow or deny), a remote (an IPv4 CIDR — 0.0.0.0/0 is the internet, i.e. every address — or another of your VPCs), a protocol with optional ports, a priority, a target inside the VPC (the whole VPC, one subnet, or specific pods), and an enabled flag. All endpoints use X-Auth-Token and operate only on your own resources — naming a VPC or rule that is not yours returns 404. All enum values are lowercase.

Evaluation: rules run in priority order (1–2000, lowest number first) and the first match wins; on a priority tie, deny is evaluated before allow. Three fixed implied rules frame your rules — the VPC's own core network (the platform's DNS, tunnels and load balancers) is always allowed in both directions and no rule can block it; beneath your rules: allow all outbound, deny all other inbound. They cannot be edited and are reported in the implied field of list responses. Every new VPC starts with one ordinary, editable rule, Allow internal traffic. Every mutation response carries applied and apply_error: the rule is always saved; applied: false means the change is saved but not yet in force, apply_error says why, and the platform retries automatically every few minutes.

GET /security-rules?vpc_id={vpc_id} List rules for a VPC

Query Parameters

ParameterTypeRequiredDefaultDescription
vpc_id*stringYes-VPC to list rules for (must be yours)

Response

200 Rules in evaluation order, plus the three implied rules

{
  "vpc_id": "vpc-uuid",
  "rules": [
    {
      "id": "rule-uuid",
      "name": "Allow internal traffic",
      "direction": "ingress",
      "action": "allow",
      "priority": 2000,
      "protocol": "all",
      "ports": null,
      "remote_type": "vpc",
      "remote_vpc_id": "vpc-uuid",
      "target_type": "vpc",
      "enabled": true
    }
  ],
  "implied": [
    {"key": "implied-allow-infrastructure", "direction": "ingress", "action": "allow"},
    {"key": "implied-allow-egress", "direction": "egress", "action": "allow"},
    {"key": "implied-deny-ingress", "direction": "ingress", "action": "deny"}
  ],
  "applied": true,
  "apply_error": null
}

Notes: every new VPC is seeded with one ordinary "Allow internal traffic" rule (priority 2000), editable and deletable like any other rule. A rule with enabled: false is kept but not enforced. A rule whose remote VPC, subnet, or pods have since been deleted reports remote_vpc_missing/target_missing and is skipped by enforcement.

GET /security-rules/{rule_id} Get a single rule

Response

200 One rule object (same shape as the entries in rules above)

404 Rule does not exist or belongs to another tenant

POST /security-rules Create a rule

Request Body

ParameterTypeRequiredDefaultDescription
vpc_id*stringYes-VPC this rule belongs to (must be yours)
name*stringYes-Rule name (1-128 characters)
descriptionstringNonullFree-text description (max 1024 characters)
direction*stringYes-ingress or egress
action*stringYes-allow or deny
priority*integerYes-1-2000; the lowest number is evaluated first
protocolstringNoalltcp, udp, icmp, or all
portsstringNoall ports"443", "8000-9000", or a comma list (max 32 entries); only valid with tcp or udp
remote_type*stringYes-cidr or vpc
remote_cidrstringIf cidr-IPv4 CIDR; 0.0.0.0/0 is the internet; host bits are cleared
remote_vpc_idstringIf vpc-One of your own VPCs
target_typestringNovpcvpc, subnet, or pods
target_subnet_idstringIf subnet-A subnet of vpc_id
target_pod_idsarrayIf pods-Max 64 pods, all in vpc_id
enabledbooleanNotrueA disabled rule is kept but not enforced

Example

curl -k -X POST $API_URL/security-rules \
  -H "X-Auth-Token: YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "vpc_id": "VPC_ID",
    "name": "allow-https-from-internet",
    "direction": "ingress",
    "action": "allow",
    "priority": 100,
    "protocol": "tcp",
    "ports": "443",
    "remote_type": "cidr",
    "remote_cidr": "0.0.0.0/0",
    "target_type": "vpc"
  }'

Response

201 Rule created; rule carries the full rule object

{
  "rule": {
    "id": "rule-uuid",
    "name": "allow-https-from-internet",
    "priority": 100,
    "enabled": true
  },
  "applied": true,
  "apply_error": null
}

404 vpc_id does not exist or belongs to another tenant

409 The VPC already has the maximum of 200 rules

422 Validation failure; detail names the field: {"field": ..., "message": ...}. Includes IPv6 CIDRs (rules are IPv4-only), ports on a portless protocol, or referencing a subnet/pod/VPC that is not yours or not in this VPC.

PATCH /security-rules/{rule_id} Update a rule

Partial update — only the fields present in the body change. Accepts any subset of the create parameters except vpc_id (a rule cannot move between VPCs). Every rule is editable, the seeded internal rule included.

Example

curl -k -X PATCH $API_URL/security-rules/RULE_ID \
  -H "X-Auth-Token: YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"priority": 50, "enabled": false}'

Notes: when changing remote_type or target_type, send the matching companion field(s) in the same request (e.g. remote_type: "cidr" with remote_cidr); stale companions from the previous shape are cleared automatically. Ports are re-validated whenever ports or protocol changes.

Response

200 Same shape as the create response (rule, applied, apply_error)

404 Rule does not exist or belongs to another tenant

422 Validation failure; detail is {"field": ..., "message": ...}

DELETE /security-rules/{rule_id} Delete a rule

Response

200 With a body (not 204), so it can report whether the change reached the network

{"rule": null, "applied": true, "apply_error": null}

404 Rule does not exist or belongs to another tenant

Note: no rule is protected, and a deleted rule stays deleted — nothing re-seeds it.

POST /security-rules/reorder Reorder a VPC's rules

Rewrites the priorities of every rule in one VPC in a single call. rule_ids must list that VPC's rules exactly once each, in the desired evaluation order — a partial list is rejected. Priorities are re-issued as 10, 20, 30, ..., leaving room to slot a rule in between later.

Request Body

ParameterTypeRequiredDefaultDescription
vpc_id*stringYes-VPC whose rules are being reordered (must be yours)
rule_ids*arrayYes-Every rule ID in the VPC, exactly once, in evaluation order

Example

curl -k -X POST $API_URL/security-rules/reorder \
  -H "X-Auth-Token: YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "vpc_id": "VPC_ID",
    "rule_ids": ["RULE_ID_2", "RULE_ID_1", "RULE_ID_3"]
  }'

Response

200 Same shape as the list response, with the new priorities

404 vpc_id is not yours, or a listed rule is not in this VPC

422 A rule is listed more than once, or the list is missing some of the VPC's rules

Pod Endpoints

Deploy and manage containerized workloads.

POST /pods Create a pod

Request Body

ParameterTypeRequiredDefaultDescription
tenant_id*stringYes-Your tenant UUID
token*stringYes-Your API token
vpc_id*stringYes-Target VPC UUID
subnet_id*stringYes-Target subnet UUID
name*stringYes-Pod name (DNS-compliant)
container_image*stringYes-Container image (e.g., "nginx:latest")
replica_countintegerNo1Replica count (1-10). If auto_scaling is true, this is the initial replica count (min 2)
service_portintegerNo80VIP service port (default 80)
use_registry_credentialsbooleanNotrueUse tenant registry secret for image pulls
external_accessbooleanNofalseEnable outbound internet (SNAT)
external_inboundbooleanNofalseEnable inbound with external IP (requires external_access)
auto_healingbooleanNofalseAuto-restart on failure
auto_scalingbooleanNofalseEnable automatic scaling (CPU-based)
autoscale_max_replicasintegerNo10Maximum replicas when autoscaling (default 10)
autoscale_cpu_target_millicoresintegerNo200Target average CPU usage per pod in millicores (default 200)
tunnelsarrayNo[]Inline tunnel definitions for this pod

Notes: For replicas, Circloud creates one VIP service that load-balances across all replicas. Auto-scaling implies auto_healing, and the minimum replica count is always 2. Live status data is available in the admin /tenants/{tenant_id} response.

Example

curl -k -X POST $API_URL/pods \
  -H "X-Auth-Token: YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "tenant_id": "YOUR_TENANT_ID",
    "token": "YOUR_TOKEN",
    "vpc_id": "VPC_ID",
    "subnet_id": "SUBNET_ID",
    "name": "web-server",
    "container_image": "nginx:latest",
    "replica_count": 2,
    "service_port": 80,
    "external_access": true,
    "external_inbound": true,
    "auto_scaling": true,
    "autoscale_max_replicas": 6,
    "autoscale_cpu_target_millicores": 250,
    "env_vars": {
      "DATABASE_URL": "postgres://app:pw@db:5432/app",
      "LOG_LEVEL": "info"
    }
  }'

env_vars

Optional. Container environment variables as a name → value map (strings only). Stored encrypted at rest (Fernet), safe for DB passwords and API keys. Update later via PATCH /pods/{pod_id}/config.

POST /pods/{pod_id}/replicas Add one replica

Response

200 Pod object with updated replica_count

Note: Adds a single replica and updates the logical pod's replica_count. Not available when auto_scaling is enabled.

DELETE /pods/{pod_id}/replicas Remove one replica

Response

200 Pod object with updated replica_count

Note: Removes a single replica (minimum 1). Not available when auto_scaling is enabled.

POST /pods/{pod_id}/restart Restart a pod

Response

200 Pod restart initiated

GET /pods List all pods

Response

200 Array of pods with status

DELETE /pods/{pod_id} Delete a pod

Response

204 Pod deleted

PATCH /pods/{pod_id}/config Update pod configuration

Update pod runtime configuration (auto-healing, persistent storage, autoscaling). Stops the workload, applies changes, and recreates with new settings. Network resources (VIP, DNS, EIP) are preserved.

Request Body (all fields optional)

ParameterTypeRequiredDefaultDescription
auto_healingbooleanNo-Toggle auto-healing (restart on failure)
persistentbooleanNo-Toggle persistent host-path storage
persistent_mount_pathstringNo/storageMount path for persistent storage
auto_scalingbooleanNo-Toggle CPU-based automatic scaling
autoscale_min_replicasintegerNo2Minimum replicas for autoscaling (2-50)
autoscale_max_replicasintegerNo10Maximum replicas for autoscaling (2-50)
autoscale_cpu_target_millicoresintegerNo200Target CPU per pod in millicores (10-10000)

Edge cases: Enabling auto_scaling forces auto_healing on. Disabling auto_healing also disables auto_scaling. GPU access cannot be changed after creation.

Example — Enable autoscaling

curl -k -X PATCH $API_URL/pods/POD_ID/config \
  -H "X-Auth-Token: YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "auto_scaling": true,
    "autoscale_max_replicas": 6,
    "autoscale_cpu_target_millicores": 250
  }'

Example — Enable persistent storage

curl -k -X PATCH $API_URL/pods/POD_ID/config \
  -H "X-Auth-Token: YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "persistent": true,
    "persistent_mount_path": "/data"
  }'

Example — Set / rotate env vars

Full replace: fields omitted from the dict are removed from the pod. Pass {} to clear. Triggers a workload recreate so the new values take effect on the running pod.

curl -k -X PATCH $API_URL/pods/POD_ID/config \
  -H "X-Auth-Token: YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "env_vars": {
      "ADMIN_EMAILS": "[email protected]",
      "FLASK_SECRET_KEY": "<64-char hex>",
      "LOG_LEVEL": "info"
    }
  }'

Response

200 Updated pod object

409 Pod is stopped or being deleted

Tunnel Endpoints

Create Cloudflare tunnels for secure external access via Cloudflare's global network.

Supported Protocols

ProtocolUse CasesHow to Connect
httpWeb apps, REST APIs, webhooksBrowser: https://subdomain.domain.com
httpsEnd-to-end encryption, mTLSBrowser: https://subdomain.domain.com
sshRemote terminal, SFTP, Gitcloudflared access ssh --hostname host
tcpDatabases (PostgreSQL, MySQL, Redis)cloudflared access tcp --hostname host
udpGame servers, VoIP, DNSDirect: hostname:port
POST /pods/{pod_id}/tunnel Create tunnel for pod

Request Body

ParameterTypeRequiredDefaultDescription
tunnel_name*stringYes-Unique tunnel name
subdomain*stringYes-Subdomain for access
protocol_type*stringYes-http, https, ssh, tcp, udp
service_portintegerNo80Target port (default: 80)
domainstringNonullCustom domain (must be approved)

HTTP Tunnel (Web Apps)

curl -k -X POST $API_URL/pods/POD_ID/tunnel \
  -H "X-Auth-Token: YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "tunnel_name": "web-app",
    "subdomain": "app",
    "protocol_type": "http",
    "service_port": 80
  }'
# Access: https://app.yourdomain.com

SSH Tunnel (Remote Access)

curl -k -X POST $API_URL/pods/POD_ID/tunnel \
  -H "X-Auth-Token: YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "tunnel_name": "ssh-access",
    "subdomain": "ssh",
    "protocol_type": "ssh",
    "service_port": 22
  }'
# Connect: cloudflared access ssh --hostname ssh.yourdomain.com

TCP Tunnel (Database)

curl -k -X POST $API_URL/pods/POD_ID/tunnel \
  -H "X-Auth-Token: YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "tunnel_name": "database",
    "subdomain": "db",
    "protocol_type": "tcp",
    "service_port": 5432
  }'
# Connect: cloudflared access tcp --hostname db.yourdomain.com --url localhost:5432
# Then: psql -h localhost -p 5432 -U myuser mydb

UDP Tunnel (Game Server)

curl -k -X POST $API_URL/pods/POD_ID/tunnel \
  -H "X-Auth-Token: YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "tunnel_name": "game-server",
    "subdomain": "game",
    "protocol_type": "udp",
    "service_port": 27015
  }'
# Connect directly: game.yourdomain.com:27015

Response

201 Tunnel created

{
  "id": "tunnel-uuid",
  "tunnel_name": "web-tunnel",
  "hostname": "app.yourdomain.com",
  "protocol_type": "http",
  "status": "active"
}
POST /tunnels?pod_id={pod_id} Create tunnel (auto-named)

Notes

Uses the pod ID query parameter and auto-derives the tunnel name from the pod.

GET /pods/{pod_id}/tunnels List tunnels for pod

Response

200 Array of tunnels for the pod

GET /tunnels List all tunnels

Response

200 Array of tunnels

DELETE /tunnels/{tunnel_id} Delete tunnel

Response

204 Tunnel deleted

Admin: POST /service-tunnels creates Cloudflare tunnels for platform services (API, GUIs) using X-Admin-Token.

Registry Credentials

Manage private container registry credentials.

POST /tenants/{tenant_id}/registry-credentials Add registry credentials

Request Body

ParameterTypeRequiredDefaultDescription
name*stringYes-Registry name (e.g., "dockerhub")
server*stringYes-Registry server URL
username*stringYes-Registry username
password*stringYes-Registry password/token

Example

curl -k -X POST $API_URL/tenants/YOUR_TENANT_ID/registry-credentials \
  -H "X-Auth-Token: YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "dockerhub",
    "server": "https://index.docker.io/v1/",
    "username": "myusername",
    "password": "mytoken"
  }'
GET /tenants/{tenant_id}/registry-credentials List registry credentials

Response

200 Array of credentials (passwords hidden)

DELETE /tenants/{tenant_id}/registry-credentials/{name} Delete registry credentials

Response

200 Credential deleted

Topology API

Deploy complete infrastructure in a single API call.

POST /tenants/{tenant_id}/topology Deploy topology

Request Body

ParameterTypeRequiredDefaultDescription
token*stringYes-Your API token
vpcs*arrayYes-Array of VPC definitions
subnets*arrayYes-Array of subnet definitions
podsarrayNo[]Array of pod definitions

Example

curl -k -X POST $API_URL/tenants/TENANT_ID/topology \
  -H "Content-Type: application/json" \
  -d '{
    "token": "YOUR_TOKEN",
    "vpcs": [
      {"name": "production", "enable_internet": true}
    ],
    "subnets": [
      {
        "name": "web",
        "vpc_name": "production",
        "cidr": "10.100.1.0/24",
        "gateway_ip": "10.100.1.1"
      }
    ],
    "pods": [
      {
        "name": "nginx",
        "vpc_name": "production",
        "subnet_name": "web",
        "container_image": "nginx:latest",
        "replica_count": 2,
        "service_port": 80,
        "auto_scaling": true,
        "autoscale_max_replicas": 6,
        "autoscale_cpu_target_millicores": 250
      }
    ]
  }'
GET /tenants/{tenant_id}/topology Get current topology

Response

200 Current topology as JSON

Health & Version

Check API health and version information.

curl -k $API_URL/healthz
curl -k $API_URL/version

Error Handling

All errors return a JSON response with a detail field.

Status CodeDescription
200Request succeeded
201Resource created
204Resource deleted
400Invalid parameters
401Invalid token
404Resource not found
409Resource conflict
{"detail": "Error message describing what went wrong"}