API Overview
The Circloud® NEST API allows you to programmatically manage your VPCs, subnets, pods, and tunnels.
$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
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.
Request Body
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
| username* | string | Yes | - | Admin username |
| password* | string | Yes | - | Admin password |
Response
200 Admin user with api_token
Login attempts (success and failure) are written to the ops log.
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
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.
Request Body
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
| name* | string | Yes | - | Tenant name |
| password* | string | Yes | - | Tenant password |
Login attempts (success and failure) are written to the ops log.
Response
200 {"id": "...", "name": "...", "api_token": "..."}
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
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": "..."}
Request Body
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
| name* | string | Yes | - | Tenant name (DNS-compliant) |
| description | string | No | null | Optional description |
| password* | string | Yes | - | Login password for tenant GUI |
Response
200 Array of tenants
Response
200 Tenant object with VPCs, pods, tunnels, and pod status
Response
204 Tenant deleted (cascades to all resources)
Domain Endpoints (Admin)
Assign custom domains to tenants for tunnel usage.
Request Body
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
| domain* | string | Yes | - | Domain name (e.g., example.com) |
Response
200 Array of domains
Provider Config (Admin)
Configure external subnet, DNS, and Cloudflare credentials.
Request Body
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
| external_subnet_cidr* | string | Yes | - | External subnet CIDR |
| external_subnet_gateway* | string | Yes | - | Gateway for external subnet |
| peering_subnet_cidr* | string | Yes | - | Peering subnet for core services |
| pod_dns_nameservers | array | No | [] | External DNS resolvers |
| pod_dns_ndots | integer | No | null | DNS ndots setting |
| cloudflare_account_id | string | No | null | Cloudflare account ID |
| cloudflare_api_token | string | No | null | Cloudflare API token |
| cloudflare_domain | string | No | null | Default tunnel domain |
Response
200 Provider configuration
VPC Endpoints
Create and manage Virtual Private Clouds.
Request Body
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
| tenant_id* | string | Yes | - | Your tenant UUID |
| token* | string | Yes | - | Your API token |
| name* | string | Yes | - | VPC name (DNS-compliant) |
| enable_internet* | boolean | Yes | - | 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
}'
Response
200 Array of VPCs
Response
204 VPC deleted (cascades to subnets and pods)
Subnet Endpoints
Create and manage subnets within VPCs.
Request Body
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
| tenant_id* | string | Yes | - | Your tenant UUID |
| token* | string | Yes | - | Your API token |
| vpc_id* | string | Yes | - | Parent VPC UUID |
| name* | string | Yes | - | Subnet name |
| cidr* | string | Yes | - | CIDR block (e.g., "10.100.1.0/24") |
| gateway_ip* | string | Yes | - | 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"
}'
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.
Query Parameters
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
| vpc_id* | string | Yes | - | 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.
Response
200 One rule object (same shape as the entries in rules above)
404 Rule does not exist or belongs to another tenant
Request Body
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
| vpc_id* | string | Yes | - | VPC this rule belongs to (must be yours) |
| name* | string | Yes | - | Rule name (1-128 characters) |
| description | string | No | null | Free-text description (max 1024 characters) |
| direction* | string | Yes | - | ingress or egress |
| action* | string | Yes | - | allow or deny |
| priority* | integer | Yes | - | 1-2000; the lowest number is evaluated first |
| protocol | string | No | all | tcp, udp, icmp, or all |
| ports | string | No | all ports | "443", "8000-9000", or a comma list (max 32 entries); only valid with tcp or udp |
| remote_type* | string | Yes | - | cidr or vpc |
| remote_cidr | string | If cidr | - | IPv4 CIDR; 0.0.0.0/0 is the internet; host bits are cleared |
| remote_vpc_id | string | If vpc | - | One of your own VPCs |
| target_type | string | No | vpc | vpc, subnet, or pods |
| target_subnet_id | string | If subnet | - | A subnet of vpc_id |
| target_pod_ids | array | If pods | - | Max 64 pods, all in vpc_id |
| enabled | boolean | No | true | A 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.
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": ...}
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.
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
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
| vpc_id* | string | Yes | - | VPC whose rules are being reordered (must be yours) |
| rule_ids* | array | Yes | - | 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.
Request Body
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
| tenant_id* | string | Yes | - | Your tenant UUID |
| token* | string | Yes | - | Your API token |
| vpc_id* | string | Yes | - | Target VPC UUID |
| subnet_id* | string | Yes | - | Target subnet UUID |
| name* | string | Yes | - | Pod name (DNS-compliant) |
| container_image* | string | Yes | - | Container image (e.g., "nginx:latest") |
| replica_count | integer | No | 1 | Replica count (1-10). If auto_scaling is true, this is the initial replica count (min 2) |
| service_port | integer | No | 80 | VIP service port (default 80) |
| use_registry_credentials | boolean | No | true | Use tenant registry secret for image pulls |
| external_access | boolean | No | false | Enable outbound internet (SNAT) |
| external_inbound | boolean | No | false | Enable inbound with external IP (requires external_access) |
| auto_healing | boolean | No | false | Auto-restart on failure |
| auto_scaling | boolean | No | false | Enable automatic scaling (CPU-based) |
| autoscale_max_replicas | integer | No | 10 | Maximum replicas when autoscaling (default 10) |
| autoscale_cpu_target_millicores | integer | No | 200 | Target average CPU usage per pod in millicores (default 200) |
| tunnels | array | No | [] | 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.
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.
Response
200 Pod object with updated replica_count
Note: Removes a single replica (minimum 1). Not available when auto_scaling is enabled.
Response
200 Pod restart initiated
Response
200 Array of pods with status
Response
204 Pod deleted
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)
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
| auto_healing | boolean | No | - | Toggle auto-healing (restart on failure) |
| persistent | boolean | No | - | Toggle persistent host-path storage |
| persistent_mount_path | string | No | /storage | Mount path for persistent storage |
| auto_scaling | boolean | No | - | Toggle CPU-based automatic scaling |
| autoscale_min_replicas | integer | No | 2 | Minimum replicas for autoscaling (2-50) |
| autoscale_max_replicas | integer | No | 10 | Maximum replicas for autoscaling (2-50) |
| autoscale_cpu_target_millicores | integer | No | 200 | Target 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
| Protocol | Use Cases | How to Connect |
|---|---|---|
| http | Web apps, REST APIs, webhooks | Browser: https://subdomain.domain.com |
| https | End-to-end encryption, mTLS | Browser: https://subdomain.domain.com |
| ssh | Remote terminal, SFTP, Git | cloudflared access ssh --hostname host |
| tcp | Databases (PostgreSQL, MySQL, Redis) | cloudflared access tcp --hostname host |
| udp | Game servers, VoIP, DNS | Direct: hostname:port |
Request Body
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
| tunnel_name* | string | Yes | - | Unique tunnel name |
| subdomain* | string | Yes | - | Subdomain for access |
| protocol_type* | string | Yes | - | http, https, ssh, tcp, udp |
| service_port | integer | No | 80 | Target port (default: 80) |
| domain | string | No | null | Custom 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"
}
Notes
Uses the pod ID query parameter and auto-derives the tunnel name from the pod.
Response
200 Array of tunnels for the pod
Response
200 Array of tunnels
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.
Request Body
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
| name* | string | Yes | - | Registry name (e.g., "dockerhub") |
| server* | string | Yes | - | Registry server URL |
| username* | string | Yes | - | Registry username |
| password* | string | Yes | - | 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"
}'
Response
200 Array of credentials (passwords hidden)
Response
200 Credential deleted
Topology API
Deploy complete infrastructure in a single API call.
Request Body
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
| token* | string | Yes | - | Your API token |
| vpcs* | array | Yes | - | Array of VPC definitions |
| subnets* | array | Yes | - | Array of subnet definitions |
| pods | array | No | [] | 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
}
]
}'
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 Code | Description |
|---|---|
| 200 | Request succeeded |
| 201 | Resource created |
| 204 | Resource deleted |
| 400 | Invalid parameters |
| 401 | Invalid token |
| 404 | Resource not found |
| 409 | Resource conflict |
{"detail": "Error message describing what went wrong"}