API v1

Krova API

Create, manage, and destroy Cubes programmatically — the same hardware-isolated microVMs you get in the dashboard, each with its own kernel, a per-cube sandbox, and no public IP. Scoped API keys, idempotency, and a machine-readable OpenAPI spec.

Authentication

All API requests (except /regions, /images, and /pricing) require an API key. Generate keys from your Space Settings in the dashboard. Each key is scoped to a single Space and inherits the permissions of the membership that created it.

Include your key in every request
Pass your API key as the X-API-KEY header.
curl -H "X-API-KEY: kro_your_key_here" https://krova.cloud/api/v1/regions

Idempotency

Mutating POST endpoints accept an optional Idempotency-Key header. Replays return the original response without re-running the operation. Keys expire after 24 hours and are scoped per Space.

Supported on: create cube, add domain, create TCP mapping, create snapshot.

Idempotency-Key: <any unique string, max 255 chars>

Replayed responses include the header Idempotency-Replayed: true.

Endpoints

Base URL: https://krova.cloud/api/v1

Public

No authentication required.

GET/regionsNo auth
#

List regions with available capacity.

Example request

curl https://krova.cloud/api/v1/regions

Example response

{
  "regions": [
    {
      "id": "abc123...",
      "name": "Germany (Nuremberg)",
      "slug": "eu-nuremberg"
    }
  ]
}
GET/imagesNo auth
#

List the OS images you can pass as image when creating a Cube.

Example request

curl https://krova.cloud/api/v1/images

Example response

{
  "images": [
    {
      "id": "ubuntu-24.04",
      "name": "Ubuntu 24.04 LTS",
      "version": "24.04",
      "description": "Ubuntu 24.04 LTS (Debian-based)"
    }
  ]
}
GET/pricingNo auth
#

Per-resource hourly rates and volume tiers. Every allocated GB of RAM and disk is billed — Krova sells 1:1 with host resources and does not oversell. Tier multiplier is applied to all rates.

Rates are quoted per hour but billed by the minute — run a Cube for 5 minutes and you pay for 5 minutes.

Example request

curl https://krova.cloud/api/v1/pricing

Example response

{
  "currency": "USD",
  "rates": {
    "vcpuPerHour": 0.001,
    "ramGbPerHour": 0.0025,
    "diskGbPerHour": 0.00005
  },
  "tiers": [
    { "minVcpus": 1, "maxVcpus": 2, "multiplier": 1.00, "label": "Standard" },
    { "minVcpus": 3, "maxVcpus": 4, "multiplier": 0.95, "label": "Plus" },
    { "minVcpus": 5, "maxVcpus": 8, "multiplier": 0.85, "label": "Pro" },
    { "minVcpus": 9, "maxVcpus": null, "multiplier": 0.80, "label": "Enterprise" }
  ],
  "note": "Running Cubes pay vCPU + RAM + disk per hour; sleeping Cubes pay only diskGbPerHour × diskLimitGb × tierMultiplier per hour. RAM and disk are sold 1:1 with host resources — no overselling."
}

Cubes

publicIpv4is the shared host-gateway address you use to reach a Cube's mapped ports (SSH and any TCP mappings) — not a dedicated public IP for the Cube. Each Cube has no public IP of its own; only the ports you explicitly map are reachable, and each can be locked to an IP allowlist.

GET/spaces/{spaceId}/cubescube.view
#

List all Cubes in a space. Returns the normalized cube shape including state, resources, and costPerHour.

Example request

curl -H "X-API-KEY: kro_your_key" \
  https://krova.cloud/api/v1/spaces/{spaceId}/cubes

Example response

{
  "cubes": [
    {
      "id": "cube_abc123",
      "name": "my-api-server",
      "state": "running",
      "publicIpv4": "1.2.3.4",
      "resources": {
        "vcpu": 2,
        "ramGb": 4,
        "diskGb": 30
      },
      "image": "ubuntu-24.04",
      "costPerHour": 0.0135,
      "createdAt": "2026-05-01T12:00:00.000Z",
      "updatedAt": "2026-05-01T12:05:00.000Z"
    }
  ],
  "pagination": {
    "page": 1,
    "limit": 10,
    "total": 1,
    "totalPages": 1
  }
}
POST/spaces/{spaceId}/cubescube.create
#

Create a new Cube. Provisioning runs asynchronously — poll the get-cube endpoint to track state.

Request Body (JSON)

FieldTypeRequiredDescription
namestringYesCube name, 1–64 chars. Also becomes the hostname inside the Cube.
resources.vcpunumberYesInteger CPU cores.
resources.ramGbnumberYesRAM in GB.
resources.diskGbnumberYesDisk in GB.
imagestringYesImage id from /images.
sshPublicKeystringYesSSH public key written to the Cube's /root/.ssh/authorized_keys at boot. Must start with ssh-ed25519, ssh-rsa, ecdsa-sha2-*, ssh-dss, or sk-*@openssh.com.
regionstringNoRegion slug from /regions.
userDatastringNocloud-init script. Max 16 KB.

Example request

curl -X POST \
  -H "X-API-KEY: kro_your_key" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "my-api-server",
    "resources": { "vcpu": 2, "ramGb": 4, "diskGb": 30 },
    "image": "ubuntu-24.04",
    "sshPublicKey": "ssh-ed25519 AAAA... user@host",
    "region": "eu-nuremberg",
    "userData": "#cloud-config\npackages:\n  - nginx\n"
  }' \
  https://krova.cloud/api/v1/spaces/{spaceId}/cubes

Example response

{
  "cube": {
    "id": "cube_abc123",
    "name": "my-api-server",
    "state": "pending",
    "publicIpv4": null,
    "resources": { "vcpu": 2, "ramGb": 4, "diskGb": 30 },
    "image": "ubuntu-24.04",
    "costPerHour": 0.0135,
    "createdAt": "2026-05-04T08:00:00.000Z",
    "updatedAt": "2026-05-04T08:00:00.000Z"
  }
}
GET/spaces/{spaceId}/cubes/{cubeId}cube.view
#

Get a single Cube. Returns the same shape as list, plus serverDomain for Caddy routing.

Example request

curl -H "X-API-KEY: kro_your_key" \
  https://krova.cloud/api/v1/spaces/{spaceId}/cubes/{cubeId}

Example response

{
  "cube": {
    "id": "cube_abc123",
    "name": "my-api-server",
    "state": "running",
    "publicIpv4": "1.2.3.4",
    "resources": { "vcpu": 2, "ramGb": 4, "diskGb": 30 },
    "image": "ubuntu-24.04",
    "costPerHour": 0.0135,
    "serverDomain": "sv1.eu-nuremberg.krova.cloud",
    "createdAt": "2026-05-01T12:00:00.000Z",
    "updatedAt": "2026-05-01T12:05:00.000Z"
  }
}
DELETE/spaces/{spaceId}/cubes/{cubeId}cube.manage
#

Delete a Cube. Processed asynchronously — the worker stops the VM, frees ports, and cleans up snapshots.

Example request

curl -X DELETE \
  -H "X-API-KEY: kro_your_key" \
  https://krova.cloud/api/v1/spaces/{spaceId}/cubes/{cubeId}

Example response

{
  "success": true
}
POST/spaces/{spaceId}/cubes/{cubeId}/sleepcube.manage
#

Pause a running Cube. Compute (vCPU + RAM) billing stops immediately; state and disk are preserved. The disk component of the Cube's price continues — billed hourly at the same per-GB rate it pays while running, for as long as the rootfs sits on host disk. Rates are quoted per hour but billed by the minute.

Example request

curl -X POST \
  -H "X-API-KEY: kro_your_key" \
  https://krova.cloud/api/v1/spaces/{spaceId}/cubes/{cubeId}/sleep

Example response

{
  "success": true
}
POST/spaces/{spaceId}/cubes/{cubeId}/wakecube.manage
#

Resume a sleeping Cube. Requires sufficient credits for at least one hour of runtime.

Example request

curl -X POST \
  -H "X-API-KEY: kro_your_key" \
  https://krova.cloud/api/v1/spaces/{spaceId}/cubes/{cubeId}/wake

Example response

{
  "success": true
}

Domains

Map a custom domain to a port on a Cube via Caddy reverse-proxy.

GET/spaces/{spaceId}/cubes/{cubeId}/domainscube.view
#

Example response

{
  "domains": [
    {
      "id": "dm_abc",
      "cubeId": "cube_abc",
      "domain": "api.example.com",
      "port": 8080,
      "status": "active",
      "createdAt": "2026-05-04T08:00:00.000Z",
      "updatedAt": "2026-05-04T08:00:00.000Z"
    }
  ]
}
POST/spaces/{spaceId}/cubes/{cubeId}/domainscube.manage
#

Add a CNAME record pointing your domain at dns.krova.cloud, then call this. Cloudflare provisions and manages the TLS certificate automatically. If your domain's DNS is on Cloudflare, set the CNAME to DNS-only (grey cloud).

Example request

curl -X POST \
  -H "X-API-KEY: kro_your_key" \
  -H "Content-Type: application/json" \
  -d '{ "domain": "api.example.com", "port": 8080 }' \
  https://krova.cloud/api/v1/spaces/{spaceId}/cubes/{cubeId}/domains
DELETE/spaces/{spaceId}/cubes/{cubeId}/domains/{mappingId}cube.manage
#

Removes the Caddy route. Update DNS afterwards if reusing the domain elsewhere.

TCP Port Mappings

Forward a public host port to a port on the Cube. Optional IP whitelist supports CIDR ranges.

GET/spaces/{spaceId}/cubes/{cubeId}/tcp-mappingscube.view
#

Example response

{
  "tcpMappings": [
    {
      "id": "tcp_abc",
      "cubeId": "cube_abc",
      "cubePort": 5432,
      "hostPort": 30001,
      "label": "postgres",
      "status": "active",
      "isSsh": false,
      "createdAt": "2026-05-04T08:00:00.000Z",
      "updatedAt": "2026-05-04T08:00:00.000Z",
      "whitelistedIps": [{ "id": "wl_abc", "cidr": "1.2.3.4/32" }]
    }
  ]
}
POST/spaces/{spaceId}/cubes/{cubeId}/tcp-mappingscube.manage
#

Host port is auto-allocated from the server pool. Up to 500 whitelist entries.

Example request

curl -X POST \
  -H "X-API-KEY: kro_your_key" \
  -H "Content-Type: application/json" \
  -d '{
    "cubePort": 5432,
    "label": "postgres",
    "whitelistedIps": ["1.2.3.4/32"]
  }' \
  https://krova.cloud/api/v1/spaces/{spaceId}/cubes/{cubeId}/tcp-mappings
DELETE/spaces/{spaceId}/cubes/{cubeId}/tcp-mappings/{mappingId}cube.manage
#

Your Cube's SSH mapping (the row with isSsh: true) cannot be deleted — every Cube needs SSH access. To change which port sshd listens on inside your Cube, use PUT /ssh-port below.

SSH Port

Every Cube has exactly one SSH mapping, created automatically at boot with sshd on port 22. When you change the sshd port inside your Cube, call this endpoint with the new port so the platform's port-forward keeps working. There's no mapping id in the URL because each Cube has exactly one SSH mapping.

PUT/spaces/{spaceId}/cubes/{cubeId}/ssh-portcube.manage
#

Updates the iptables forward in place — the public host port stays the same and the IP whitelist is preserved. The mapping's status briefly flips to pending while the change is applied, then returns to active. The new value shows up as cubePort on the SSH row in the TCP mappings list.

Example request

curl -X PUT \
  -H "X-API-KEY: kro_your_key" \
  -H "Content-Type: application/json" \
  -d '{ "cubePort": 2222 }' \
  https://krova.cloud/api/v1/spaces/{spaceId}/cubes/{cubeId}/ssh-port

Example response

{
  "success": true,
  "cubePort": 2222
}

409 Conflict if another SSH port change is already in progress on the same Cube, or if the requested port is already used by another TCP mapping on the same Cube.
400 Bad Request if cubePort is missing or outside 1..65535.

Snapshots

Point-in-time disk snapshots stored on S3-compatible object storage.

GET/spaces/{spaceId}/cubes/{cubeId}/snapshotscube.view
#

Example response

{
  "snapshots": [
    {
      "id": "snap_abc",
      "cubeId": "cube_abc",
      "spaceId": "sp_xyz",
      "name": "before-upgrade",
      "status": "complete",
      "sizeBytes": 1234567890,
      "kind": "manual",
      "completedAt": "2026-05-04T08:01:30.000Z",
      "createdAt": "2026-05-04T08:00:00.000Z"
    }
  ]
}
POST/spaces/{spaceId}/cubes/{cubeId}/snapshotscube.manage
#

Triggers a snapshot job. name is optional — auto-generated if omitted. Only one snapshot may be in progress per Cube.

Example request

curl -X POST \
  -H "X-API-KEY: kro_your_key" \
  -H "Content-Type: application/json" \
  -d '{ "name": "before-upgrade" }' \
  https://krova.cloud/api/v1/spaces/{spaceId}/cubes/{cubeId}/snapshots
DELETE/spaces/{spaceId}/cubes/{cubeId}/snapshots/{snapshotId}cube.manage
#

Deletes the snapshot object from the storage backend and the DB record.

POST/spaces/{spaceId}/cubes/{cubeId}/restorecube.manage
#

Restore a Cube's disk from a snapshot. The Cube is paused, the snapshot is downloaded and applied, then the Cube boots from the restored disk.

Example request

curl -X POST \
  -H "X-API-KEY: kro_your_key" \
  -H "Content-Type: application/json" \
  -d '{ "snapshotId": "snap_abc" }' \
  https://krova.cloud/api/v1/spaces/{spaceId}/cubes/{cubeId}/restore

Webhooks

Receive a signed POST whenever a Cube changes state. See the Webhooks section below for event types, payload shape, and signature verification.

GET/spaces/{spaceId}/webhooks
#

Example response

{
  "webhooks": [
    {
      "id": "wh_abc",
      "url": "https://example.com/hooks/krova",
      "events": ["cube.running", "cube.deleted"],
      "enabled": true,
      "createdAt": "2026-05-04T08:00:00.000Z",
      "updatedAt": "2026-05-04T08:00:00.000Z"
    }
  ]
}
POST/spaces/{spaceId}/webhooks
#

The signing secret is returned only once at creation. Store it securely — to rotate, delete and re-create the endpoint.

Example request

curl -X POST \
  -H "X-API-KEY: kro_your_key" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://example.com/hooks/krova",
    "events": ["cube.running", "cube.sleeping", "cube.error", "cube.deleted"]
  }' \
  https://krova.cloud/api/v1/spaces/{spaceId}/webhooks

Example response

{
  "webhook": {
    "id": "wh_abc",
    "url": "https://example.com/hooks/krova",
    "events": ["cube.running", "cube.sleeping", "cube.error", "cube.deleted"],
    "enabled": true,
    "secret": "a3f2...deadbeef",
    "createdAt": "2026-05-04T08:00:00.000Z",
    "updatedAt": "2026-05-04T08:00:00.000Z"
  }
}
GET/spaces/{spaceId}/webhooks/{endpointId}
#

Returns the endpoint without the secret.

DELETE/spaces/{spaceId}/webhooks/{endpointId}
#

Cascade-deletes all delivery records for this endpoint.

GET/spaces/{spaceId}/webhooks/{endpointId}/deliveries
#

Last delivery attempts. Default 50, max 100 via ?limit=. Retained 30 days.

Example response

{
  "deliveries": [
    {
      "id": "dlv_abc",
      "event": "cube.running",
      "status": "delivered",
      "attempts": 1,
      "lastAttemptAt": "2026-05-04T08:05:00.000Z",
      "responseStatus": 200,
      "createdAt": "2026-05-04T08:05:00.000Z"
    }
  ]
}

Outbound Webhooks

When a webhook endpoint is enabled, Krova POSTs a signed JSON payload to your URL on every subscribed event. Each request carries an HMAC-SHA256 signature you can verify with the secret returned at creation.

Event types

EventFired when
cube.runningCube transitions to running (boot complete, wake, or state-sync detection)
cube.sleepingCube transitions to sleeping (user sleep, zero-balance auto-sleep, or unexpected pause)
cube.errorCube transitions to error (boot failure detected within 5 min of start)
cube.deletedCube is fully deleted

Payload shape

{
  "id": "evt_abc123",
  "event": "cube.running",
  "createdAt": "2026-05-04T12:05:00.000Z",
  "spaceId": "sp_xyz",
  "data": {
    "id": "cube_abc",
    "name": "my-cube",
    "state": "running",
    "publicIpv4": "1.2.3.4"
  }
}

Headers on every delivery

  • X-Krova-Signature: sha256=<hex> — HMAC-SHA256 of the raw body, signed with your endpoint secret.
  • X-Krova-Event — event name (e.g. cube.running).
  • X-Krova-Delivery — unique delivery id, useful for de-duplication.

Verifying the signature (Node.js)

import { createHmac, timingSafeEqual } from "crypto"

function verify(secret, rawBody, signatureHeader) {
  const expected =
    "sha256=" + createHmac("sha256", secret).update(rawBody).digest("hex")
  return timingSafeEqual(
    Buffer.from(signatureHeader),
    Buffer.from(expected)
  )
}

Always use a constant-time comparison such as timingSafeEqual — a plain === is vulnerable to timing attacks.

Delivery and retries

A delivery is considered successful when your endpoint returns a 2xx within 10 seconds. Failed deliveries are retried up to 4 times with a 60-second delay. Delivery history is retained for 30 days and accessible via the deliveries endpoint above.

Status Codes & Errors

The API uses standard HTTP status codes. All errors return an error string.

CodeDescription
200Success
201Resource created
400Invalid request body or parameters
401Missing or invalid API key
403Insufficient permissions
404Resource not found
409Conflict (duplicate, already in progress)
422Semantic error (cube in wrong state, insufficient credits)
429Rate limited
500Internal server error (safe to retry)
503Capacity error (no ports available)

Error responses always follow this shape:

{ "error": "Human-readable description" }

Cube States

Cubes transition through these states during their lifecycle:

pendingQueued for provisioning
bootingVM is starting up
runningVM is live (billing active)
sleepingVM paused — compute billing stops; only the Cube's disk component continues, billed hourly at the same per-GB rate the running Cube paid
stoppingDeletion in progress
deletedDeleted (hidden from API)
errorSomething went wrong

Rate Limits

Every mutating endpoint (all POST and DELETE requests) is limited to 10 requests per 60 seconds, per client IP. Exceeding the limit returns 429 Too Many Requests with a Retry-After header indicating how many seconds to wait.

Read endpoints (GET) are not rate-limited and are suitable for polling.

Ready to automate?

Generate an API key from your Space Settings and start building.