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.
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.
/regionsNo authList regions with available capacity.
Example request
curl https://krova.cloud/api/v1/regionsExample response
{
"regions": [
{
"id": "abc123...",
"name": "Germany (Nuremberg)",
"slug": "eu-nuremberg"
}
]
}/imagesNo authList the OS images you can pass as image when creating a Cube.
Example request
curl https://krova.cloud/api/v1/imagesExample response
{
"images": [
{
"id": "ubuntu-24.04",
"name": "Ubuntu 24.04 LTS",
"version": "24.04",
"description": "Ubuntu 24.04 LTS (Debian-based)"
}
]
}/pricingNo authPer-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/pricingExample 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.
/spaces/{spaceId}/cubescube.viewList 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}/cubesExample 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
}
}/spaces/{spaceId}/cubescube.createCreate a new Cube. Provisioning runs asynchronously — poll the get-cube endpoint to track state.
Request Body (JSON)
| Field | Type | Required | Description |
|---|---|---|---|
| name | string | Yes | Cube name, 1–64 chars. Also becomes the hostname inside the Cube. |
| resources.vcpu | number | Yes | Integer CPU cores. |
| resources.ramGb | number | Yes | RAM in GB. |
| resources.diskGb | number | Yes | Disk in GB. |
| image | string | Yes | Image id from /images. |
| sshPublicKey | string | Yes | SSH 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. |
| region | string | No | Region slug from /regions. |
| userData | string | No | cloud-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}/cubesExample 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"
}
}/spaces/{spaceId}/cubes/{cubeId}cube.viewGet 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"
}
}/spaces/{spaceId}/cubes/{cubeId}cube.manageDelete 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
}/spaces/{spaceId}/cubes/{cubeId}/sleepcube.managePause 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}/sleepExample response
{
"success": true
}/spaces/{spaceId}/cubes/{cubeId}/wakecube.manageResume 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}/wakeExample response
{
"success": true
}Domains
Map a custom domain to a port on a Cube via Caddy reverse-proxy.
/spaces/{spaceId}/cubes/{cubeId}/domainscube.viewExample 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"
}
]
}/spaces/{spaceId}/cubes/{cubeId}/domainscube.manageAdd 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/spaces/{spaceId}/cubes/{cubeId}/domains/{mappingId}cube.manageRemoves 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.
/spaces/{spaceId}/cubes/{cubeId}/tcp-mappingscube.viewExample 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" }]
}
]
}/spaces/{spaceId}/cubes/{cubeId}/tcp-mappingscube.manageHost 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/spaces/{spaceId}/cubes/{cubeId}/tcp-mappings/{mappingId}cube.manageYour 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.
/spaces/{spaceId}/cubes/{cubeId}/ssh-portcube.manageUpdates 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-portExample 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.
/spaces/{spaceId}/cubes/{cubeId}/snapshotscube.viewExample 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"
}
]
}/spaces/{spaceId}/cubes/{cubeId}/snapshotscube.manageTriggers 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/spaces/{spaceId}/cubes/{cubeId}/snapshots/{snapshotId}cube.manageDeletes the snapshot object from the storage backend and the DB record.
/spaces/{spaceId}/cubes/{cubeId}/restorecube.manageRestore 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}/restoreWebhooks
Receive a signed POST whenever a Cube changes state. See the Webhooks section below for event types, payload shape, and signature verification.
/spaces/{spaceId}/webhooksExample 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"
}
]
}/spaces/{spaceId}/webhooksThe 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}/webhooksExample 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"
}
}/spaces/{spaceId}/webhooks/{endpointId}Returns the endpoint without the secret.
/spaces/{spaceId}/webhooks/{endpointId}Cascade-deletes all delivery records for this endpoint.
/spaces/{spaceId}/webhooks/{endpointId}/deliveriesLast 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
| Event | Fired when |
|---|---|
| cube.running | Cube transitions to running (boot complete, wake, or state-sync detection) |
| cube.sleeping | Cube transitions to sleeping (user sleep, zero-balance auto-sleep, or unexpected pause) |
| cube.error | Cube transitions to error (boot failure detected within 5 min of start) |
| cube.deleted | Cube 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.
| Code | Description |
|---|---|
| 200 | Success |
| 201 | Resource created |
| 400 | Invalid request body or parameters |
| 401 | Missing or invalid API key |
| 403 | Insufficient permissions |
| 404 | Resource not found |
| 409 | Conflict (duplicate, already in progress) |
| 422 | Semantic error (cube in wrong state, insufficient credits) |
| 429 | Rate limited |
| 500 | Internal server error (safe to retry) |
| 503 | Capacity error (no ports available) |
Error responses always follow this shape:
{ "error": "Human-readable description" }Cube States
Cubes transition through these states during their lifecycle:
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.
