Skip to content

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

ScopeLimit
Authenticated requests120 per 60 seconds per token
Public provision/redeem endpoints5 per 60 seconds per IP

Responses include standard rate-limit headers:

http
RateLimit-Limit: 120
RateLimit-Remaining: 119
RateLimit-Reset: 1722345678

On 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"
}
StatusCodeDescription
400BAD_REQUESTInvalid request (e.g. CNAME not configured, default domain removal)
401UNAUTHORIZEDMissing, expired, or invalid token
402QUOTA_EXCEEDEDCPU/memory/GPU quota exceeded for your plan
403FORBIDDENToken lacks permission for this resource
404NOT_FOUNDResource does not exist or is not owned by this account
409CONFLICTResource already exists or bucket not empty
410GONEToken expired or already used
422VALIDATION_ERRORRequest body failed schema validation
429RATE_LIMITEDRate limit exceeded
500INTERNAL_ERRORUnexpected 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

FieldTypeRequiredDescription
imagestringYes (or repo)Docker image to deploy
repostringYes (or image)GitHub repo URL to build and deploy
namestringNoService name (derived from image/repo if omitted)
branchstringNoGit branch (repo deploys only)
regionstringNoeu-west, us-east, or ap-south
gpustringNoGPU type: rtx4090, rtx5090
node_idstringNoPin to a specific node ID
replicasintegerNoInitial replica count (default: 1)
portintegerNoContainer port (auto-detected from image EXPOSE if omitted)
env_varsobjectNoEnvironment variables to set at deploy time
sub_pathstringNoSubdirectory within the repo to build
no_probebooleanNoDisable readiness probe (for workers/batch jobs)
health_checkobjectNoReadiness probe config (see below)
init_imagestringNoInit container image — runs to completion before the main container starts
init_commandstringNoShell command for the init container
init_envstring[]NoEnv vars for the init container, each "KEY=VALUE"
mount_configmapstring[]NoConfigMaps to mount, each "alias:mountPath" (e.g. "app-config:/data/config")

health_check object

FieldTypeValuesDescription
typestringnone, tcp, execProbe type. none = no probe; tcp = TCP socket (default); exec = run a command
commandstring[]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:

ValueMeaning
explicitSet via --port / port request field
imageDetected from image EXPOSE
injectedAssigned by CP; app must read process.env.PORT

Error Responses

StatusCondition
402Quota exceeded — insufficient CPU/memory/GPU
422Neither 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

FieldTypeDescription
imagestringNew container image
env_varsobjectEnv vars to merge (does not replace existing keys)
replicasintegerNew replica count
no_probebooleanEnable/disable readiness probe
health_checkobjectNew 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"
}
StatusCondition
400Fewer than 2 successful deployments in history

POST /v1/services/:name/scale

Scale replicas or adjust CPU/memory resource limits.

Request Body

FieldTypeDescription
replicasintegerTarget replica count
cpustringCPU limit (e.g. "2", "500m")
memorystringMemory limit (e.g. "4Gi", "512Mi")

Response 200

json
{ "name": "ml-inference", "replicas": 3, "cpu": "2", "memory": "8Gi" }
StatusCondition
402Quota 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

FieldTypeRequiredDescription
max_replicasintegerYesUpper bound on replica count
min_replicasintegerNoLower bound (default: 1)
cpu_threshold_pctintegerNoCPU utilisation % to trigger scale-up (default: 80)
memory_threshold_pctintegerNoMemory 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
  }
}
StatusCondition
422max_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

ParameterDefaultDescription
tail100Number of recent lines
followfalseStream 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}
buildStatusMeaning
successImage built and pushed
failedBuild 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

FieldTypeDescription
keysstring[]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

FieldTypeRequiredDescription
targetstringYesTarget service name
bidirectionalbooleanNoAlso 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

FieldTypeRequiredDescription
target_namestringYesNew service name
include_envbooleanNoCopy 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

FieldTypeRequiredDescription
target_servicestringYesDestination 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

FieldTypeDescription
commandstring[]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

FieldTypeRequiredDescription
mountstringNoMount path inside the container (default: /data)
size_giintegerNoVolume 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" }
StatusCondition
404Volume not found

POST /v1/services/:name/tcp-proxy

Expose a non-HTTP container port externally via a NodePort.

Request Body

FieldTypeRequiredDescription
portintegerYesContainer 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

FieldTypeRequiredDescription
portintegerYesContainer 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"
}
StatusCondition
409Port already exposed by another service
422Port out of range (must be 1024–49151)
4295-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

StatusCondition
404Port 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:

FieldValue
NameYour subdomain (e.g. api)
TypeCNAME
Targetcpctl.app
ProxyDNS 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

FieldTypeRequiredDescription
hostnamestringYesFully-qualified domain (e.g. api.example.com)
skip_dnsbooleanNoSkip 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.

StatusCondition
400CNAME not pointing to cpctl.app
422Invalid 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

StatusCondition
400Attempt to remove the default <service>.cpctl.app domain
404Hostname 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

FieldTypeRequiredDescription
urlstringYesHTTPS endpoint to receive events
eventsstring[]YesEvent 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

FieldTypeRequiredDescription
modestringYesdeny — block a CIDR; allow — restrict access to a CIDR only
cidrstringYesIP 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"
}
StatusCondition
422Invalid mode or cidr
42950-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

ParameterDescription
regionFilter by region ID
typeFilter 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

FieldTypeDescription
typestringGPU type: rtx4090, rtx5090
countintegerNumber of GPUs (default: 1)
regionstringRegion
durationstringDuration 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 Parametersregion, 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

FieldTypeDescription
durationstringReservation 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

FieldTypeRequiredDescription
namestringYesDatabase name
enginestringNopostgres, mysql, redis, mongo (default: postgres)
regionstringNoRegion (default: account default)
size_gbintegerNoStorage 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

FieldTypeRequiredDescription
servicestringYesTarget 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"
  }
}
KeyDescription
DATABASE_URLGeneric — works with most ORMs
<ENGINE>_URLEngine-specific (POSTGRES_URL, REDIS_URL, MYSQL_URL, MONGO_URL)
<DB_NAME>_URLNamed 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

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

StatusCondition
422Name 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

ParameterDescription
forcetrue to delete all objects first

Response 204 No Content

StatusCondition
409Bucket 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

FieldTypeRequiredDescription
namestringYesConfigMap alias name
filesstring[]YesFilenames 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.

StatusCondition
422name 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

FieldTypeRequiredDescription
namestringYesConfigMap alias name (same as used in upload-token)
s3_prefixstringYesFrom the upload-token response
filesstring[]YesFilenames (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

StatusCondition
404ConfigMap 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

FieldTypeRequiredDescription
labelstringYesHuman-readable label (e.g. "my-agent-prod")
ttl_hoursintegerNoToken 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"
}
StatusCondition
42910 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

FieldTypeRequiredDescription
invitation_tokenstringYesToken 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.

StatusCondition
410Token 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

FieldTypeRequiredDescription
amountnumberYesAmount in USD
networkstringFor cryptoBase, Ethereum, Solana, Tron, Cardano
tokenstringFor cryptoUSDC 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

FieldTypeRequiredDescription
payment_idstringYesFrom the topup response
tx_idstringYesOn-chain transaction ID

Response 200

json
{
  "verified": true,
  "received": 50.00,
  "tx_id": "5xSol1ExampleTxHash123...",
  "new_balance": 92.50
}
StatusCondition
404payment_id not found
409Already settled
410Deposit address expired

GET /v1/billing/usage

Get usage totals for the current billing cycle.

Query Parameters

ParameterDescription
serviceFilter by service name
periodtoday, month, or YYYY-MM
group_byservice 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

ParameterDefaultDescription
days30Lookback 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

FieldTypeRequiredDescription
methodstringYescard
limitnumberNoPer-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 Parameterssession_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

FieldTypeRequiredDescription
servicestringYesService name
amountnumberYesCharge amount in USD
actionstringNoAction 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.

StatusCondition
402No payment method linked, or amount exceeds spend limit

GET /v1/pay/history

List per-call payment charges.

Query Parameterslimit (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 Parametersrepo=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 Parameterstail (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 Parameterscmd (JSON-encoded string array, e.g. ["sh"])

Frame format:

  • Outbound (send): 0x00 prefix + stdin bytes
  • Inbound (receive): 0x01 prefix + stdout bytes, 0x02 prefix + stderr bytes

Infrastructure Reference

ComponentValue
API base URLhttps://api.computeportal.io
CNAME targetcpctl.app
Default service domain<service-name>-<user-slug>.cpctl.app
TLSLet's Encrypt (HTTP-01, provisioned asynchronously)
IngressTraefik on Kubernetes
OrchestratorKubernetes (K8s-native)
Object storage endpointhttps://s3-cli.computeportal.io
MCP endpointhttps://mcp.computeportal.io/mcp
API versionv1

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 CLIcurl -fsSL https://install.computeportal.io | sh
  • MCP Server — AI assistant integration at https://mcp.computeportal.io/mcp (see MCP Server Reference)

GPU Compute Platform