API Reference
Base URL: https://api.computeportal.io
The ComputePortal REST API gives programmatic control over every aspect of your GPU compute environment. All requests are authenticated with a Bearer token.
Authentication
Every request must include your CP API token:
http
Authorization: Bearer cp_live_<hex>bash
curl https://api.computeportal.io/v1/services \
-H "Authorization: Bearer cp_live_4a7f3b9c2e1d8a06"Get your token from the CP Dashboard under Settings → API Tokens, or via cpctl whoami.
Unauthenticated endpoints
A small number of endpoints are public (no token required). These are called out explicitly in each section.
Rate Limits
| Scope | Limit |
|---|---|
| Authenticated requests | 120 per 60 seconds per token |
| Public provision/redeem endpoints | 5 per 60 seconds per IP |
Responses include standard rate-limit headers:
http
RateLimit-Limit: 120
RateLimit-Remaining: 119
RateLimit-Reset: 1722345678On exhaustion: 429 Too Many Requests with Retry-After header.
Errors
All error responses use a consistent JSON shape:
json
{
"error": "human-readable description",
"code": "MACHINE_READABLE_CODE"
}| Status | Code | Description |
|---|---|---|
400 | BAD_REQUEST | Invalid request (e.g. CNAME not configured, default domain removal) |
401 | UNAUTHORIZED | Missing, expired, or invalid token |
402 | QUOTA_EXCEEDED | CPU/memory/GPU quota exceeded for your plan |
403 | FORBIDDEN | Token lacks permission for this resource |
404 | NOT_FOUND | Resource does not exist or is not owned by this account |
409 | CONFLICT | Resource already exists or bucket not empty |
410 | GONE | Token expired or already used |
422 | VALIDATION_ERROR | Request body failed schema validation |
429 | RATE_LIMITED | Rate limit exceeded |
500 | INTERNAL_ERROR | Unexpected server error |
Services
Services are Kubernetes Deployments running on CP compute infrastructure.
GET /v1/services
List all services in your account.
bash
curl https://api.computeportal.io/v1/services \
-H "Authorization: Bearer cp_live_..."Response 200
json
[
{
"name": "ml-inference",
"image": "myorg/inference:v2",
"status": "running",
"region": "eu-west",
"node": "node-eu1-001",
"cpu": "2",
"memory": "8Gi",
"gpu": "rtx4090",
"url": "https://ml-inference.cpctl.app",
"port": 8080,
"replicas": 2,
"desired_replicas": 2,
"autoscaling": { "enabled": false },
"created_at": "2026-07-01T08:00:00Z"
}
]POST /v1/services
Deploy a new service from a Docker image or GitHub repo.
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
image | string | Yes (or repo) | Docker image to deploy |
repo | string | Yes (or image) | GitHub repo URL to build and deploy |
name | string | No | Service name (derived from image/repo if omitted) |
branch | string | No | Git branch (repo deploys only) |
region | string | No | eu-west, us-east, or ap-south |
gpu | string | No | GPU type: rtx4090, rtx5090 |
node_id | string | No | Pin to a specific node ID |
replicas | integer | No | Initial replica count (default: 1) |
port | integer | No | Container port (auto-detected from image EXPOSE if omitted) |
env_vars | object | No | Environment variables to set at deploy time |
sub_path | string | No | Subdirectory within the repo to build |
no_probe | boolean | No | Disable readiness probe (for workers/batch jobs) |
health_check | object | No | Readiness probe config (see below) |
init_image | string | No | Init container image — runs to completion before the main container starts |
init_command | string | No | Shell command for the init container |
init_env | string[] | No | Env vars for the init container, each "KEY=VALUE" |
mount_configmap | string[] | No | ConfigMaps to mount, each "alias:mountPath" (e.g. "app-config:/data/config") |
health_check object
| Field | Type | Values | Description |
|---|---|---|---|
type | string | none, tcp, exec | Probe type. none = no probe; tcp = TCP socket (default); exec = run a command |
command | string[] | — | Required when type is exec |
bash
curl -X POST https://api.computeportal.io/v1/services \
-H "Authorization: Bearer cp_live_..." \
-H "Content-Type: application/json" \
-d '{
"image": "myorg/inference:v2",
"name": "ml-inference",
"gpu": "rtx4090",
"replicas": 2,
"env_vars": { "LOG_LEVEL": "info" }
}'Response 200
json
{
"service_id": "ml-inference",
"name": "ml-inference",
"url": "https://ml-inference.cpctl.app",
"status": "starting",
"port": 8080,
"port_source": "image"
}port_source values:
| Value | Meaning |
|---|---|
explicit | Set via --port / port request field |
image | Detected from image EXPOSE |
injected | Assigned by CP; app must read process.env.PORT |
Error Responses
| Status | Condition |
|---|---|
402 | Quota exceeded — insufficient CPU/memory/GPU |
422 | Neither image nor repo provided |
GET /v1/services/:name
Get details for a specific service.
bash
curl https://api.computeportal.io/v1/services/ml-inference \
-H "Authorization: Bearer cp_live_..."Response 200 — same shape as a single item from GET /v1/services.
PATCH /v1/services/:name
Update a running service's image, env vars, or replica count. Triggers a rolling restart.
Request Body
| Field | Type | Description |
|---|---|---|
image | string | New container image |
env_vars | object | Env vars to merge (does not replace existing keys) |
replicas | integer | New replica count |
no_probe | boolean | Enable/disable readiness probe |
health_check | object | New probe config |
bash
curl -X PATCH https://api.computeportal.io/v1/services/ml-inference \
-H "Authorization: Bearer cp_live_..." \
-H "Content-Type: application/json" \
-d '{ "image": "myorg/inference:v3", "replicas": 3 }'Response 200
json
{ "ok": true, "name": "ml-inference" }DELETE /v1/services/:name
Delete a service and all associated Kubernetes resources (Deployment, Service, Ingress).
bash
curl -X DELETE https://api.computeportal.io/v1/services/ml-inference \
-H "Authorization: Bearer cp_live_..."Response 204 No Content
Dispatches a service.destroyed webhook event.
POST /v1/services/:name/start
Start a stopped service (sets replicas to 1).
Response 200 { "ok": true }
POST /v1/services/:name/stop
Stop a running service (sets replicas to 0, preserves config).
Response 200 { "ok": true }
POST /v1/services/:name/restart
Rolling restart — all pods replaced one by one. Current replica count is preserved.
Response 200 { "ok": true }
POST /v1/services/:name/redeploy
Re-trigger the current deployment without changing image or configuration.
Response 200 { "ok": true }
POST /v1/services/:name/rollback
Roll back to the previous successful deployment.
Response 200
json
{
"ok": true,
"rolledBackTo": "myorg/inference:v1",
"deployedAt": "2026-06-28T09:15:00Z"
}| Status | Condition |
|---|---|
400 | Fewer than 2 successful deployments in history |
POST /v1/services/:name/scale
Scale replicas or adjust CPU/memory resource limits.
Request Body
| Field | Type | Description |
|---|---|---|
replicas | integer | Target replica count |
cpu | string | CPU limit (e.g. "2", "500m") |
memory | string | Memory limit (e.g. "4Gi", "512Mi") |
Response 200
json
{ "name": "ml-inference", "replicas": 3, "cpu": "2", "memory": "8Gi" }| Status | Condition |
|---|---|
402 | Quota exceeded |
GET /v1/services/:name/autoscale
Get the current HPA (Horizontal Pod Autoscaler) configuration.
Response 200
json
{
"autoscaling": {
"enabled": true,
"min_replicas": 2,
"max_replicas": 10,
"cpu_threshold_pct": 70,
"memory_threshold_pct": null,
"current_replicas": 4
}
}POST /v1/services/:name/autoscale
Configure CPU/memory-triggered autoscaling (Kubernetes HPA).
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
max_replicas | integer | Yes | Upper bound on replica count |
min_replicas | integer | No | Lower bound (default: 1) |
cpu_threshold_pct | integer | No | CPU utilisation % to trigger scale-up (default: 80) |
memory_threshold_pct | integer | No | Memory utilisation % to trigger scale-up |
bash
curl -X POST https://api.computeportal.io/v1/services/ml-inference/autoscale \
-H "Authorization: Bearer cp_live_..." \
-H "Content-Type: application/json" \
-d '{ "min_replicas": 2, "max_replicas": 10, "cpu_threshold_pct": 70 }'Response 200
json
{
"ok": true,
"autoscaling": {
"enabled": true,
"min_replicas": 2,
"max_replicas": 10,
"cpu_threshold_pct": 70
}
}| Status | Condition |
|---|---|
422 | max_replicas missing or less than 1 |
DELETE /v1/services/:name/autoscale
Disable autoscaling and remove the HPA.
Response 200 { "ok": true }
GET /v1/services/:name/logs
Fetch recent log output from a service.
Query Parameters
| Parameter | Default | Description |
|---|---|---|
tail | 100 | Number of recent lines |
follow | false | Stream in real time (SSE) |
bash
curl "https://api.computeportal.io/v1/services/ml-inference/logs?tail=50&follow=true" \
-H "Authorization: Bearer cp_live_..."Response 200 — plain text log lines. When follow=true, streamed as server-sent events until the connection is closed.
GET /v1/services/:name/build-logs
Stream Kaniko build logs for a repo-based deployment. Returns an SSE stream; terminates with a JSON trailer line:
[build-result] {"buildStatus":"success","exitCode":0}buildStatus | Meaning |
|---|---|
success | Image built and pushed |
failed | Build failed — see preceding log lines |
GET /v1/services/:name/metrics
Get CPU, memory, and network metrics for a service.
Response 200
json
{
"cpu_usage": "34%",
"memory_usage": "4.2 GiB / 16 GiB",
"network_rx": "0.4 MB/s",
"network_tx": "1.2 MB/s",
"replicas": 2
}GET /v1/services/:name/gpu
Get GPU utilization metrics for a service.
Response 200
json
{
"service": "ml-inference",
"gpu_type": "rtx4090",
"util_pct": 72,
"vram_used_gb": 14.0,
"vram_total_gb": 24.0,
"temp_c": 67,
"power_w": 280,
"node": "node-eu1-001"
}GET /v1/services/:name/deployments
List deployment history for a service.
Response 200
json
[
{
"id": "deploy_abc123",
"image": "myorg/inference:v2",
"deployedAt": "2026-07-08T08:00:00Z",
"status": "success"
},
{
"id": "deploy_xyz789",
"image": "myorg/inference:v1",
"deployedAt": "2026-06-28T09:15:00Z",
"status": "success"
}
]GET /v1/services/:name/deployments/:id
Get details for a specific deployment.
Response 200 — single deployment entry from the list above.
GET /v1/services/:name/events
List recent Kubernetes events for a service — useful for diagnosing startup failures.
Response 200
json
[
{
"reason": "Failed",
"message": "Error: ImagePullBackOff",
"type": "Warning",
"timestamp": "2026-08-10T08:45:34Z",
"count": 12
}
]Capped at 20 events, sorted by timestamp descending.
GET /v1/services/:name/env
List all environment variables on a service.
Response 200
json
{
"MODEL_PATH": "/models/v2",
"LOG_LEVEL": "info"
}POST /v1/services/:name/env
Set environment variables. Merges with existing vars (does not replace). Triggers a rolling restart.
Values may reference another service's live URL using the syntax ${{SERVICE_NAME.URL}} — resolved at set-time to the target service's public URL.
Request Body — flat key-value object:
bash
curl -X POST https://api.computeportal.io/v1/services/ml-inference/env \
-H "Authorization: Bearer cp_live_..." \
-H "Content-Type: application/json" \
-d '{ "MODEL_PATH": "/models/v3", "LOG_LEVEL": "debug" }'Response 200 { "ok": true }
POST /v1/services/:name/env/unset
Remove environment variables. Triggers a rolling restart.
Request Body
| Field | Type | Description |
|---|---|---|
keys | string[] | Variable names to remove |
Response 200 { "ok": true }
POST /v1/services/:name/link
Wire two services together by injecting each other's public URLs as environment variables.
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
target | string | Yes | Target service name |
bidirectional | boolean | No | Also inject source URL into target (default: true) |
Response 200
json
{
"ok": true,
"linked": ["tfx-frontend", "tfx-backend"],
"injected": {
"TFX_BACKEND_URL": "https://tfx-backend.cpctl.app",
"BACKEND_URL": "https://tfx-backend.cpctl.app"
},
"note": "Redeploy both services for the new env vars to take effect"
}POST /v1/services/:name/clone
Clone a service under a new name. Copies image, resource configuration, and (optionally) env vars.
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
target_name | string | Yes | New service name |
include_env | boolean | No | Copy environment variables (default: true) |
Response 200
json
{ "ok": true, "name": "ml-inference-staging", "url": "https://ml-inference-staging.cpctl.app", "cloned_from": "ml-inference" }POST /v1/services/:name/env/copy
Copy all environment variables from this service to another. Target's existing values win on conflict.
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
target_service | string | Yes | Destination service name |
Response 200
json
{ "ok": true, "copied": 5, "target": "ml-inference-staging" }POST /v1/services/:name/exec
Get a WebSocket URL for running a command inside a container.
Request Body
| Field | Type | Description |
|---|---|---|
command | string[] | Command to run (default: ["sh"]) |
Response 200
json
{
"exec_url": "wss://api.computeportal.io/v1/services/ml-inference/exec/ws",
"pod_name": "ml-inference-abc123",
"namespace": "compute-portal"
}Connect to exec_url with your token in the Authorization header. Frames are multiplexed: 0x00 = stdin, 0x01 = stdout, 0x02 = stderr.
GET /v1/services/:name/volumes
List persistent volumes attached to a service.
Response 200
json
[
{ "name": "pvc-my-svc-1234567890", "claim": "pvc-my-svc-1234567890", "mount": "/data", "size": "10Gi", "created_at": "2026-07-01T08:00:00Z" }
]POST /v1/services/:name/volumes
Attach a new persistent volume to a service. Creates a PersistentVolumeClaim and mounts it into the running Deployment.
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
mount | string | No | Mount path inside the container (default: /data) |
size_gi | integer | No | Volume size in GiB (default: 10) |
Response 200
json
{ "name": "pvc-my-svc-1234567890", "mount": "/data", "service": "my-svc", "size_gi": 10 }DELETE /v1/services/:name/volumes/:volName
Detach a persistent volume from a service. Removes the mount from the Deployment but retains the PVC — data is preserved and can be reattached later.
Response 200
json
{ "deleted": "pvc-my-svc-1234567890" }| Status | Condition |
|---|---|
404 | Volume not found |
POST /v1/services/:name/tcp-proxy
Expose a non-HTTP container port externally via a NodePort.
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
port | integer | Yes | Container port to expose |
Response 200
json
{
"host": "tcp.cpctl.app",
"port": 32541,
"target_port": 5432,
"service": "ml-inference"
}GET /v1/services/:name/tcp-proxy
List all TCP proxies for a service.
Response 200
json
[
{ "name": "ml-inference-tcp-5432", "host": "tcp.cpctl.app", "port": 32541, "target_port": 5432 }
]DELETE /v1/services/:name/tcp-proxy/:port
Remove a TCP proxy by container port.
Response 204 No Content
TCP Expose
The TCP Expose system routes raw TCP traffic from a public port on tcp.cpctl.app to a container port on a service. Unlike the tcp-proxy endpoints (which create only a Kubernetes NodePort), expose also writes nginx stream configuration across all bare-metal nodes so traffic is reachable at the public tcp.cpctl.app:<port> address.
Use this for protocols that require a stable public hostname and port — Cardano P2P, PostgreSQL, custom binary protocols. Up to 5 exposed ports per account.
POST /v1/services/:name/expose
Expose a container port publicly over raw TCP.
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
port | integer | Yes | Container port to expose (1024–49151) |
bash
curl -X POST https://api.computeportal.io/v1/services/cardano-relay/expose \
-H "Authorization: Bearer cp_live_..." \
-H "Content-Type: application/json" \
-d '{ "port": 3001 }'Response 201
json
{
"host": "tcp.cpctl.app",
"port": 3001,
"node_port": 30283,
"created_at": "2026-09-05T08:00:00Z"
}| Status | Condition |
|---|---|
409 | Port already exposed by another service |
422 | Port out of range (must be 1024–49151) |
429 | 5-port per-account limit reached |
GET /v1/services/:name/expose
List all exposed TCP ports for a service.
Response 200
json
[
{ "host": "tcp.cpctl.app", "port": 3001, "node_port": 30283, "created_at": "2026-09-05T08:00:00Z" }
]DELETE /v1/services/:name/expose/:port
Remove a public TCP expose by container port. Removes the nginx stream config from all nodes and deletes the Kubernetes NodePort Service.
Response 204 No Content
| Status | Condition |
|---|---|
404 | Port not exposed for this service |
POST /v1/services/:name/static-ip
Request a static outbound IP for a service.
Response 200
json
{ "ok": true, "ip": null, "status": "provisioning" }DELETE /v1/services/:name/static-ip
Release the static outbound IP.
Response 200 { "ok": true }
GET /v1/status
Platform health check — does not require authentication.
Response 200
json
{
"status": "healthy",
"version": "v0.3.0",
"services": 42,
"nodes": 6,
"orchestrator": "kubernetes",
"gpu": { "available": 12, "total": 16 }
}Domains
Custom domains serve your service under your own hostname with automatic TLS (Let's Encrypt via HTTP-01). The default <service>.cpctl.app domain is always available and cannot be removed.
Before attaching a domain, create a CNAME record at your DNS provider:
| Field | Value |
|---|---|
| Name | Your subdomain (e.g. api) |
| Type | CNAME |
| Target | cpctl.app |
| Proxy | DNS only — disable Cloudflare orange-cloud |
POST /v1/services/:name/domains
Attach a custom hostname. CP validates the CNAME, adds the hostname to the Traefik Ingress, and provisions TLS asynchronously.
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
hostname | string | Yes | Fully-qualified domain (e.g. api.example.com) |
skip_dns | boolean | No | Skip CNAME validation (use when DNS hasn't propagated yet) |
bash
curl -X POST https://api.computeportal.io/v1/services/my-api/domains \
-H "Authorization: Bearer cp_live_..." \
-H "Content-Type: application/json" \
-d '{ "hostname": "api.example.com" }'Response 200
json
{
"domain": "api.example.com",
"service": "my-api",
"status": "provisioning",
"tls": false,
"created_at": "2026-07-04T10:52:36Z"
}TLS is provisioned asynchronously — poll GET /v1/services/:name/domains/:hostname until tls: true.
| Status | Condition |
|---|---|
400 | CNAME not pointing to cpctl.app |
422 | Invalid hostname format |
GET /v1/services/:name/domains
List all hostnames attached to a service.
Response 200
json
[
{
"name": "my-api.cpctl.app",
"service": "my-api",
"status": "active",
"tls": "active",
"dns_configured": true,
"cname_target": "cpctl.app",
"created_at": ""
},
{
"name": "api.example.com",
"service": "my-api",
"status": "active",
"tls": "active",
"dns_configured": true,
"cname_target": "cpctl.app",
"created_at": "2026-07-04T10:52:36.000Z"
}
]tls is the string "active" when TLS is provisioned.
GET /v1/services/:name/domains/:hostname
Get TLS and DNS status for a specific hostname.
Response 200
json
{
"domain": "api.example.com",
"service": "my-api",
"status": "active",
"tls": true,
"cert_verified": true,
"dns_configured": true,
"cname_target": "cpctl.app",
"created_at": "2026-07-04T10:52:36Z"
}DELETE /v1/services/:name/domains/:hostname
Remove a custom hostname. TLS certificate is deprovisioned asynchronously.
Response 204 No Content
| Status | Condition |
|---|---|
400 | Attempt to remove the default <service>.cpctl.app domain |
404 | Hostname not attached |
Deploy Hooks
Deploy hooks are inbound HTTP endpoints that trigger a redeployment when called — useful for CI/CD systems that don't have a CP token.
POST /v1/services/:name/hooks
Create a new deploy hook for a service.
Response 200
json
{
"id": "hook_abc123",
"url": "https://api.computeportal.io/v1/hooks/hook_abc123",
"secret": "whsec_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
}Trigger the hook by POSTing to the url with the secret in the X-CP-Hook-Secret header.
GET /v1/services/:name/hooks
List all deploy hooks for a service.
Response 200
json
[
{ "id": "hook_abc123", "url": "https://api.computeportal.io/v1/hooks/hook_abc123", "created_at": "2026-07-01T10:00:00Z" }
]DELETE /v1/services/:name/hooks/:id
Delete a deploy hook.
Response 200 { "ok": true }
POST /v1/hooks/:id (public, secret-authenticated)
Trigger a redeployment via a deploy hook. No Bearer token required.
Auth: Pass the hook secret in the X-CP-Hook-Secret header.
Rate limit: 5 per 60 seconds per IP.
bash
curl -X POST https://api.computeportal.io/v1/hooks/hook_abc123 \
-H "X-CP-Hook-Secret: whsec_xxxxxxxxxxxxxxxx"Response 200
json
{ "ok": true, "service": "my-api" }Also dispatches a deploy.started webhook event.
Outbound Webhooks
Subscribe to CP events and receive HTTP POST notifications at your own URL. Payloads are HMAC-SHA256 signed via X-CP-Signature.
Available events: deploy.succeeded, deploy.failed, service.crashed, service.stopped, service.destroyed, deploy.started
POST /v1/webhooks
Register an outbound webhook.
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
url | string | Yes | HTTPS endpoint to receive events |
events | string[] | Yes | Event types to subscribe to |
bash
curl -X POST https://api.computeportal.io/v1/webhooks \
-H "Authorization: Bearer cp_live_..." \
-H "Content-Type: application/json" \
-d '{ "url": "https://hooks.example.com/cp", "events": ["deploy.succeeded", "deploy.failed"] }'Response 200
json
{
"id": "wh_abc123",
"url": "https://hooks.example.com/cp",
"events": ["deploy.succeeded", "deploy.failed"],
"secret": "whsec_xxxxxxxxxxxxxxxxxxxxxxxx"
}Verify incoming payloads by comparing X-CP-Signature with HMAC-SHA256(secret, body).
GET /v1/webhooks
List all outbound webhooks.
Response 200
json
[
{ "id": "wh_abc123", "url": "https://hooks.example.com/cp", "events": ["deploy.succeeded"], "created_at": "2026-07-01T10:00:00Z" }
]DELETE /v1/webhooks/:id
Delete an outbound webhook.
Response 200 { "ok": true }
Firewall
IP-based firewall rules applied at the Traefik Ingress layer — traffic is blocked before reaching the container.
POST /v1/services/:name/firewall/rules
Add a firewall rule. Each service supports up to 50 rules.
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
mode | string | Yes | deny — block a CIDR; allow — restrict access to a CIDR only |
cidr | string | Yes | IP or CIDR range (e.g. 1.2.3.4/32, 10.0.0.0/8) |
bash
curl -X POST https://api.computeportal.io/v1/services/my-api/firewall/rules \
-H "Authorization: Bearer cp_live_..." \
-H "Content-Type: application/json" \
-d '{ "mode": "deny", "cidr": "198.51.100.0/24" }'Response 200
json
{
"id": "deny:198.51.100.0/24",
"mode": "deny",
"cidr": "198.51.100.0/24",
"service": "my-api",
"created_at": "2026-07-08T10:00:00Z"
}| Status | Condition |
|---|---|
422 | Invalid mode or cidr |
429 | 50-rule limit reached |
GET /v1/services/:name/firewall/rules
List all firewall rules for a service.
Response 200
json
[
{ "id": "deny:198.51.100.0/24", "mode": "deny", "cidr": "198.51.100.0/24" }
]DELETE /v1/services/:name/firewall/rules/:rule
Remove a single rule. The :rule path segment is the rule id (e.g. deny:198.51.100.0%2F24).
Response 204 No Content
DELETE /v1/services/:name/firewall
Remove all firewall rules from a service (disables firewall entirely).
Response 204 No Content
Preview Environments
PR preview environments automatically deploy a temporary copy of a service for each GitHub pull request and destroy it when the PR closes.
POST /v1/services/:name/previews/enable
Enable PR preview environments for a service.
Response 200
json
{ "ok": true, "previews_enabled": true }POST /v1/services/:name/previews/disable
Disable PR preview environments.
Response 200 { "ok": true }
GET /v1/services/:name/previews
List active preview deployments.
Response 200
json
[
{
"name": "my-api-pr-42",
"pr_number": 42,
"branch": "feature/new-endpoint",
"status": "running",
"replicas": 1,
"created_at": "2026-07-08T10:00:00Z"
}
]GPU & Nodes
GET /v1/gpu/nodes
List GPU nodes and current availability.
Query Parameters
| Parameter | Description |
|---|---|
region | Filter by region ID |
type | Filter by GPU type (rtx4090, rtx5090) |
Response 200
json
[
{
"node_name": "node-eu1-001",
"gpu_type": "rtx4090",
"gpu_count": 8,
"available": 6,
"region": "eu-west",
"util_pct": 18,
"vram_used_gb": 12.4,
"vram_total_gb": 64.0,
"temp_c": 67,
"power_w": 280
}
]POST /v1/gpu/reserve
Reserve GPU capacity at a fixed rate.
Request Body
| Field | Type | Description |
|---|---|---|
type | string | GPU type: rtx4090, rtx5090 |
count | integer | Number of GPUs (default: 1) |
region | string | Region |
duration | string | Duration string (e.g. 1h, 24h, 7d) |
Response 200
json
{
"reservation_id": "res_abc123",
"gpu_type": "rtx4090",
"count": 2,
"region": "eu-west",
"reserved_until": "2026-07-09T08:00:00Z",
"cost_per_hour": 2.40
}POST /v1/gpu/jobs
Run a one-shot GPU job — billed per second, auto-cleaned up when the container exits.
Response 200
json
{ "job_id": "job_abc123", "status": "queued", "started_at": "2026-07-08T09:00:00Z" }GET /v1/gpu/jobs/:id/logs
Get log output for a GPU job.
Response 200 — plain text log lines.
GET /v1/nodes
List all compute nodes.
Query Parameters — region, gpu (filter by GPU type)
Response 200
json
[
{
"id": "abc123-uid",
"name": "cp-k8s-worker-001",
"hostname": "cp-k8s-worker-001",
"status": "ready",
"availability": "active",
"role": "worker",
"cpu": "64",
"memory": "256Gi",
"memory_gb": "256",
"region": "eu-west",
"gpu": "rtx4090",
"gpu_count": 8,
"reserved": false
}
]status is "ready" or "not-ready". availability is "active" or "drain". memory_gb is a string.
GET /v1/nodes/:id
Get hardware specs and status for a specific node.
Response 200 — single node object from the list above.
POST /v1/nodes/:id/reserve
Reserve an entire node for dedicated use.
Request Body
| Field | Type | Description |
|---|---|---|
duration | string | Reservation duration (e.g. 1h, 24h, 7d) |
Response 200
json
{ "node": "node-eu1-001", "reserved_until": "2026-07-09T08:00:00Z", "cost_per_hour": 8.00 }POST /v1/nodes/:id/release
Release a reserved node back to the shared pool.
Response 200 { "ok": true }
GET /v1/regions
List available CP regions.
Response 200
json
[
{ "id": "eu-west", "name": "EU West", "location": "Amsterdam", "status": "available" },
{ "id": "us-east", "name": "US East", "location": "New York", "status": "available" },
{ "id": "ap-south", "name": "AP South", "location": "Singapore", "status": "available" }
]Databases
Managed databases run as StatefulSets with persistent storage. Connection strings use cluster-internal DNS — traffic stays within the cluster.
GET /v1/databases
List all databases in your account.
Response 200
json
[
{
"name": "app-db",
"engine": "postgres",
"version": "16",
"status": "running",
"region": "eu-west",
"size_gb": 50,
"connection_string": "postgres://admin:secret@db-app-db.compute-portal.svc.cluster.local:5432/app-db",
"created_at": "2026-06-15T10:00:00Z"
}
]POST /v1/databases
Create a managed database.
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
name | string | Yes | Database name |
engine | string | No | postgres, mysql, redis, mongo (default: postgres) |
region | string | No | Region (default: account default) |
size_gb | integer | No | Storage in GB (default: 10) |
bash
curl -X POST https://api.computeportal.io/v1/databases \
-H "Authorization: Bearer cp_live_..." \
-H "Content-Type: application/json" \
-d '{ "name": "app-db", "engine": "postgres", "size_gb": 50 }'Response 200
json
{
"name": "app-db",
"engine": "postgres",
"region": "eu-west",
"size_gb": 50,
"status": "starting",
"connection_string": "postgres://admin:secret@db-app-db.compute-portal.svc.cluster.local:5432/app-db",
"password": "secret"
}GET /v1/databases/:name
Get details and connection info for a specific database.
Response 200 — single database object from the list.
POST /v1/databases/:name/stop
Scale the database StatefulSet to zero replicas (pause without deleting data).
Response 200 { "ok": true }
POST /v1/databases/:name/start
Restart a stopped database (scale replicas back to 1).
Response 200 { "ok": true }
POST /v1/databases/:name/attach
Inject connection environment variables into a service using the cluster-internal DNS name.
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
service | string | Yes | Target service name |
Response 200
json
{
"ok": true,
"database": "app-db",
"service": "my-app",
"injected": {
"DATABASE_URL": "postgres://admin:secret@db-app-db.compute-portal.svc.cluster.local:5432/app-db",
"POSTGRES_URL": "postgres://admin:secret@db-app-db.compute-portal.svc.cluster.local:5432/app-db",
"APP_DB_URL": "postgres://admin:secret@db-app-db.compute-portal.svc.cluster.local:5432/app-db"
}
}| Key | Description |
|---|---|
DATABASE_URL | Generic — works with most ORMs |
<ENGINE>_URL | Engine-specific (POSTGRES_URL, REDIS_URL, MYSQL_URL, MONGO_URL) |
<DB_NAME>_URL | Named key derived from the database name |
DELETE /v1/databases/:name
Destroy a database and all its data. Irreversible.
Response 204 No Content
Object Storage
S3-compatible storage backed by MinIO. Endpoint: https://s3-cli.computeportal.io. Buckets are scoped per user.
POST /v1/storage/buckets
Create a new bucket.
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
name | string | Yes | Bucket name (3–42 chars, lowercase alphanumeric and hyphens) |
bash
curl -X POST https://api.computeportal.io/v1/storage/buckets \
-H "Authorization: Bearer cp_live_..." \
-H "Content-Type: application/json" \
-d '{ "name": "my-uploads" }'Response 200
json
{
"bucket": "usr123-my-uploads",
"endpoint": "https://s3-cli.computeportal.io",
"access_key": "AKIAIOSFODNN7EXAMPLE",
"secret_key": "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY",
"region": "us-east-1"
}Use these credentials with any S3-compatible client (AWS SDK, rclone, s3cmd).
| Status | Condition |
|---|---|
422 | Name format invalid |
GET /v1/storage/buckets
List all buckets in your account.
Response 200
json
[
{
"id": "bkt_abc123",
"bucket": "usr123-my-uploads",
"endpoint": "https://s3-cli.computeportal.io",
"access_key": "AKIAIOSFODNN7EXAMPLE",
"region": "us-east-1",
"created_at": "2026-07-01T10:00:00Z"
}
]GET /v1/storage/buckets/:name
Get credentials for a specific bucket.
Response 200 — same as the creation response, including secret_key.
DELETE /v1/storage/buckets/:name
Delete a bucket. By default, fails if the bucket contains objects.
Query Parameters
| Parameter | Description |
|---|---|
force | true to delete all objects first |
Response 204 No Content
| Status | Condition |
|---|---|
409 | Bucket not empty (and force not set) |
Config Maps
ConfigMaps store files that are mounted into containers at a specified path. The upload flow is two-step: get presigned S3 URLs, upload files directly from the client, then commit to create the Kubernetes ConfigMap.
POST /v1/configmaps/upload-token
Get presigned S3 PUT URLs for uploading ConfigMap files. Files must be uploaded directly to S3 before calling POST /v1/configmaps.
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
name | string | Yes | ConfigMap alias name |
files | string[] | Yes | Filenames to upload (max 20; no path separators) |
bash
curl -X POST https://api.computeportal.io/v1/configmaps/upload-token \
-H "Authorization: Bearer cp_live_..." \
-H "Content-Type: application/json" \
-d '{ "name": "app-config", "files": ["config.json", "topology.json"] }'Response 200
json
{
"presigned_urls": {
"config.json": "https://s3-cli.computeportal.io/...",
"topology.json": "https://s3-cli.computeportal.io/..."
},
"s3_prefix": "configmaps/<user-id>/app-config/"
}Upload each file with an HTTP PUT to its presigned URL, then call POST /v1/configmaps with the same name, s3_prefix, and files list.
| Status | Condition |
|---|---|
422 | name missing, files empty, more than 20 files, or invalid filename |
POST /v1/configmaps
Commit uploaded files as a Kubernetes ConfigMap. Fetches files from S3 (uploaded via presigned URLs) and creates or replaces the ConfigMap.
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
name | string | Yes | ConfigMap alias name (same as used in upload-token) |
s3_prefix | string | Yes | From the upload-token response |
files | string[] | Yes | Filenames (same list as upload-token) |
Response 201
json
{
"name": "app-config-usr12345",
"alias": "app-config",
"keys": ["config.json", "topology.json"]
}name is the actual Kubernetes ConfigMap name (alias with a user-scoped suffix). Use alias when mounting with --mount-configmap.
GET /v1/configmaps
List all ConfigMaps in your account.
Response 200
json
[
{ "name": "app-config-usr12345", "alias": "app-config", "keys": ["config.json", "topology.json"] }
]DELETE /v1/configmaps/:name
Delete a ConfigMap. Use the full Kubernetes name (from the name field in POST /v1/configmaps).
Response 204 No Content
| Status | Condition |
|---|---|
404 | ConfigMap not found |
Environments
GET /v1/environments
List all environments.
Response 200
json
[
{ "name": "production", "created_at": "2026-06-01T00:00:00Z" },
{ "name": "staging", "created_at": "2026-06-15T00:00:00Z" }
]POST /v1/environments
Create a new environment.
Request Body { "name": "staging" }
Response 200 { "name": "staging", "created_at": "..." }
DELETE /v1/environments/:name
Delete an environment.
Response 204 No Content
Sandboxes
Ephemeral GPU environments for short-lived experiments, fine-tuning runs, and interactive sessions.
GET /v1/sandboxes
List all sandboxes.
Response 200
json
[
{ "id": "sbx_abc123", "status": "running", "created_at": "2026-07-08T08:00:00Z", "expires_at": "2026-07-08T10:00:00Z" }
]POST /v1/sandboxes
Create a new sandbox.
Response 200
json
{ "id": "sbx_abc123", "status": "starting" }POST /v1/sandboxes/:id/exec
Run a command inside a sandbox.
Response 200 — exec session details (same shape as /v1/services/:name/exec).
DELETE /v1/sandboxes/:id
Destroy a sandbox immediately.
Response 204 No Content
Agent Account Invitations
Issue single-use tokens that provision new CP accounts — designed for AI agents and automated clients.
POST /v1/invitations
Issue an invitation token.
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
label | string | Yes | Human-readable label (e.g. "my-agent-prod") |
ttl_hours | integer | No | Token lifetime in hours (default: 24) |
bash
curl -X POST https://api.computeportal.io/v1/invitations \
-H "Authorization: Bearer cp_live_..." \
-H "Content-Type: application/json" \
-d '{ "label": "my-agent-prod", "ttl_hours": 48 }'Response 200
json
{
"id": "inv_abc123",
"invitation_token": "cpinv_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
"label": "my-agent-prod",
"expires_at": "2026-08-30T12:00:00Z"
}| Status | Condition |
|---|---|
429 | 10 active invitation limit reached |
GET /v1/invitations
List all active invitation tokens issued by this account.
Response 200
json
[
{ "id": "inv_abc123", "label": "my-agent-prod", "expires_at": "2026-08-30T12:00:00Z", "used": false }
]DELETE /v1/invitations/:id
Revoke a pending invitation.
Response 204 No Content
POST /v1/users/redeem (public)
Redeem an invitation token to create a new CP account. No Bearer token required.
Rate limit: 5 per 60 seconds per IP.
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
invitation_token | string | Yes | Token from POST /v1/invitations |
bash
curl -X POST https://api.computeportal.io/v1/users/redeem \
-H "Content-Type: application/json" \
-d '{ "invitation_token": "cpinv_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" }'Response 200
json
{
"token": "cp_live_...",
"user_id": "usr_abc123",
"email": "agent-abc123@agents.computeportal.io"
}The returned token is a full CP API token for the new account. Tokens are single-use — redeemed tokens cannot be used again.
| Status | Condition |
|---|---|
410 | Token not found, already used, or expired |
Quota
GET /v1/billing/quota
Show compute quota usage for your active plan.
Response 200
json
{
"entitlement": {
"vcpu": 10.0,
"memory_gib": 20.0,
"storage_gib": 100.0,
"gpu_count": 2
},
"used": {
"vcpu_millicores": 3500,
"memory_mib": 7168,
"gpu_count": 0,
"vcpu_pct": 35,
"memory_pct": 35
},
"available": {
"vcpu_millicores": 6500,
"memory_mib": 13312
}
}Billing
GET /v1/billing/balance
Get current account balance and burn rate.
Response 200
json
{
"balance_usd": 42.50,
"credit_usd": 10.00,
"status": "active",
"alert_at_usd": 5.00,
"burn_rate_usd_per_hour": 0.0125
}POST /v1/billing/topup
Initiate a crypto top-up and get a deposit address, or start a card top-up.
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
amount | number | Yes | Amount in USD |
network | string | For crypto | Base, Ethereum, Solana, Tron, Cardano |
token | string | For crypto | USDC or USDT |
Response (crypto) 200
json
{
"payment_id": "topup_a1b2c3d4",
"network": "Base",
"token": "USDC",
"amount_usd": 50.00,
"deposit_address": "0x1a2b3c4d5e6f...",
"expires_at": "2026-07-27T17:00:00Z"
}The deposit address is valid for 30 minutes. After sending, confirm with POST /v1/billing/topup/verify.
POST /v1/billing/topup/verify
Submit a transaction ID to confirm and credit a crypto top-up.
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
payment_id | string | Yes | From the topup response |
tx_id | string | Yes | On-chain transaction ID |
Response 200
json
{
"verified": true,
"received": 50.00,
"tx_id": "5xSol1ExampleTxHash123...",
"new_balance": 92.50
}| Status | Condition |
|---|---|
404 | payment_id not found |
409 | Already settled |
410 | Deposit address expired |
GET /v1/billing/usage
Get usage totals for the current billing cycle.
Query Parameters
| Parameter | Description |
|---|---|
service | Filter by service name |
period | today, month, or YYYY-MM |
group_by | service for per-service breakdown |
Response 200
json
{
"period": "2026-07",
"total_usd": 128.40,
"line_items": [
{ "service": "ml-inference", "resource": "cpu", "units": "720 vCPU-hours", "cost_usd": 72.00 },
{ "service": "ml-inference", "resource": "gpu", "units": "24 GPU-hours", "cost_usd": 48.00 },
{ "service": "app-db", "resource": "storage", "units": "50 GiB-months", "cost_usd": 8.40 }
]
}POST /v1/billing/alert
Set a low-balance alert threshold.
Request Body { "threshold": 5.00 }
Response 200 { "ok": true, "alert_at_usd": 5.00 }
GET /v1/billing/history
List past usage snapshots (hourly billing history).
Query Parameters
| Parameter | Default | Description |
|---|---|---|
days | 30 | Lookback window (1–90 days) |
Response 200
json
[
{
"id": "snap_abc123",
"cpu_cores": 3.5,
"memory_gib": 7.0,
"storage_gib": 50.0,
"services": 4,
"cost_usd": 0.42,
"snapped_at": "2026-07-08T09:00:00Z"
}
]Machine Payments
Per-call, agent-native payments via a linked Stripe card. No pre-loaded balance required.
GET /v1/pay/status
Get the currently linked payment method.
Response 200
json
{
"linked": true,
"type": "card",
"spend_limit": 1.00,
"currency": "USD",
"active": true,
"last4": "6880",
"exp": "05/2029",
"cardholder_name": "CHOI YOON IL"
}POST /v1/pay/setup
Start a card setup flow. Returns a Stripe-hosted URL.
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
method | string | Yes | card |
limit | number | No | Per-call spend limit in USD (default: 1.00) |
Response 200
json
{
"method": "card",
"spend_limit": 1.00,
"currency": "USD",
"setup_url": "https://checkout.stripe.com/pay/cs_live_...",
"session_id": "cps_abc123"
}Poll GET /v1/pay/setup/poll?session_id=cps_abc123 to detect completion.
GET /v1/pay/setup/poll
Poll for completion of a card setup flow.
Query Parameters — session_id (required, from POST /v1/pay/setup)
Response 200
json
{ "status": "complete", "last4": "6880", "exp": "05/2029", "cardholder_name": "CHOI YOON IL", "spend_limit": 1.00 }status values: pending, complete, expired
POST /v1/pay/authorize
Pre-authorize a per-call charge. The card is charged immediately.
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
service | string | Yes | Service name |
amount | number | Yes | Charge amount in USD |
action | string | No | Action type: deploy, start, domain-add (default: deploy) |
Response 200
json
{
"token": "mpp_tok_xxxxxxxxxxxxxxxxxxxx",
"service": "my-api",
"amount_usd": 0.10,
"action": "deploy",
"expires_at": "2026-07-27T12:05:00Z",
"charged": true,
"stripe_payment_intent_id": "pi_3Rxxxxxxxxxxxxxxxxxxxxxxxx"
}Pass the token to POST /v1/services via the X-Payment-Token header (CLI: --pay-token). Token expires in 5 minutes.
| Status | Condition |
|---|---|
402 | No payment method linked, or amount exceeds spend limit |
GET /v1/pay/history
List per-call payment charges.
Query Parameters — limit (1–100, default: 20)
Response 200
json
[
{
"id": "chg_abc123",
"stripe_payment_intent_id": "pi_3Rxxxxxxxxxxxxxxxxxxxxxxxx",
"service": "my-api",
"action": "deploy",
"amount_usd": 0.10,
"status": "settled",
"charged_at": "2026-07-27T07:01:10Z"
}
]GitHub Integration
GET /v1/github/status
Check whether a GitHub account is connected.
Response 200
json
{ "connected": true, "appName": "ComputePortal", "connectedAt": "2026-06-01T10:00:00Z" }GET /v1/github/repos
List GitHub repos accessible via the connected account.
Response 200
json
[
{ "id": 123456, "fullName": "myorg/my-app", "private": false, "defaultBranch": "main" }
]GET /v1/github/branches
List branches for a specific repo.
Query Parameters — repo=owner/name (required)
Response 200 — array of branch name strings.
POST /v1/github/pat
Store a GitHub Personal Access Token for pulling private ghcr.io images.
Request Body { "token": "ghp_xxxxxxxxxxxx" }
Response 200 { "ok": true }
Authentication (Token Lifecycle)
GET /v1/auth/me
Get the currently authenticated user.
Response 200
json
{
"email": "alice@example.com",
"user_id": "usr_abc123",
"org": "default",
"plan": "pro",
"token_expires_at": 0
}DELETE /v1/auth/token
Revoke the current token (logout). Evicts the token from the server-side cache.
Response 204 No Content
WebSocket Endpoints
WS /v1/services/:name/logs/ws
Stream live pod logs over WebSocket.
Query Parameters — tail (default: 100)
Auth: Pass token in the Authorization header during handshake.
Each frame is a UTF-8 text message containing one log line. Connection closes with 1000 on success, 1008 if unauthorized.
WS /v1/services/:name/exec/ws
Interactive shell via WebSocket.
Query Parameters — cmd (JSON-encoded string array, e.g. ["sh"])
Frame format:
- Outbound (send):
0x00prefix + stdin bytes - Inbound (receive):
0x01prefix + stdout bytes,0x02prefix + stderr bytes
Infrastructure Reference
| Component | Value |
|---|---|
| API base URL | https://api.computeportal.io |
| CNAME target | cpctl.app |
| Default service domain | <service-name>-<user-slug>.cpctl.app |
| TLS | Let's Encrypt (HTTP-01, provisioned asynchronously) |
| Ingress | Traefik on Kubernetes |
| Orchestrator | Kubernetes (K8s-native) |
| Object storage endpoint | https://s3-cli.computeportal.io |
| MCP endpoint | https://mcp.computeportal.io/mcp |
| API version | v1 |
Breaking changes will be introduced under a new version prefix (/v2/...). Non-breaking additions may be added to v1 at any time.
SDK and Tools
- cpctl CLI —
curl -fsSL https://install.computeportal.io | sh - MCP Server — AI assistant integration at
https://mcp.computeportal.io/mcp(see MCP Server Reference)
