For developers

Build on CLK Cloud

Create a key, launch a server and react to events in a few minutes. Developer documentation is in English.

Quickstart

Everything you can do in the console you can do with the API: it is the same service underneath. Base URL: https://api.cloud.clk.no/v1. JSON in, JSON out, TLS only.

1. Create a key

In the console, open API keys, pick a role (or scopes), a name and an optional expiry. The key is shown once.

export KEY=clk_live_…

2. Check it works and see what you can create

curl -H "Authorization: Bearer $KEY" https://api.cloud.clk.no/v1/me
curl -H "Authorization: Bearer $KEY" https://api.cloud.clk.no/v1/sizes
curl -H "Authorization: Bearer $KEY" https://api.cloud.clk.no/v1/images

3. Create a server

name, size and labels are required. network and sshKey can be left out when your organization has exactly one of each. Add your key with POST /v1/ssh-keys first.

curl -X POST https://api.cloud.clk.no/v1/servers \
  -H "Authorization: Bearer $KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "web-1",
    "size": "CLK Dedicated Extra Small",
    "labels": { "env": "prod" },
    "userData": "#cloud-config\npackages:\n  - docker.io\n"
  }'

# 202 Accepted
{ "jobId": "0b9c…", "jobUrl": "/v1/jobs/0b9c…" }

New accounts follow the same rules as in the console: the account must be approved, have a card, and stay within the starting limits.

Authentication

Send an organization API key as a bearer token: Authorization: Bearer clk_live_…. A key belongs to one organization, carries scopes, can expire, and can be revoked in the console. Keys start with clk_live_ so secret scanners can recognise a leak. Only a hash is stored, so a lost key has to be replaced.

Giving an AI agent access? Create it its own key with the narrowest role, a short expiry, and caps: a maximum number of servers and a maximum monthly cost, set when you create the key in the console. A create that would break a cap is refused with 422 key_limit_exceeded. Keys cannot change billing, members or other keys.

Roles

RoleScopes
read-onlyservers, ssh-keys, networks, jobs, events, webhooks, ips and volumes: read
operatorread-only, plus servers:write, ips:write and volumes:write
admineverything, including webhooks:write, ssh-keys:write and billing:read

Scopes

ScopeAllows
servers:readList and read servers, sizes, images and regions
servers:writeCreate, delete, start, stop, reboot and label servers
ssh-keys:read / writeList, add and remove SSH keys
networks:readList networks
jobs:readRead job status
events:readRead the event feed
webhooks:read / writeList, create and delete webhook endpoints
ips:read / writeList, allocate, attach, move and release public IPv4 addresses
volumes:read / writeVolumes and snapshots (when enabled)
billing:readCosts and invoices

Async operations

Create, delete, start, stop and reboot answer 202 Accepted with a job. Poll GET /v1/jobs/{id} until status is succeeded or failed (error says why), or subscribe to webhooks and skip polling.

curl -H "Authorization: Bearer $KEY" https://api.cloud.clk.no/v1/jobs/0b9c…
{ "id": "0b9c…", "status": "succeeded", "createdAt": "2026-10-08T12:00:00Z" }

curl -H "Authorization: Bearer $KEY" "https://api.cloud.clk.no/v1/servers?label=env:prod"

Servers can be filtered by label (?label=env:prod, repeat for AND), and costs grouped by label (/v1/usage?group_by=label:env), so labels are the way to split cost per team or project.

Webhooks

Register an https endpoint with POST /v1/webhooks (or in the console) and choose the event types. The response includes a signing secret, shown once. Endpoints must resolve to public addresses; private, loopback and link-local destinations are refused. Up to 10 endpoints per organization.

Delivery body
{
  "id": "5d0e…",
  "type": "server.created",
  "created_at": "2026-10-08T12:01:10Z",
  "organization_id": "a1b2…",
  "data": { … }
}

Each delivery is a POST with Content-Type: application/json, a 10 second timeout, and the headers CLK-Event-Id, CLK-Event-Type and CLK-Signature. Any 2xx counts as delivered. Failures are retried after 1, 5, 30 minutes and 2, 6 and 12 hours. Delivery is at least once: use the event id to ignore duplicates. A delivery log with replay is in the console.

Verify the signature

CLK-Signature: t=<unix seconds>,v1=<hex>, where v1 is the HMAC-SHA256 of "t.body" with your secret. Compute it over the raw request bytes and reject old timestamps. While a secret is being rotated, the header carries a second v1 signed with the previous secret for 24 hours.

Node.js
import { createHmac, timingSafeEqual } from 'node:crypto'

// rawBody must be the exact bytes received, before any JSON parsing.
export function verify(rawBody, header, secret, toleranceSeconds = 300) {
  const parts = Object.fromEntries(header.split(',').map((p) => p.split('=')))
  const t = Number(parts.t)
  if (Math.abs(Date.now() / 1000 - t) > toleranceSeconds) return false
  const expected = createHmac('sha256', secret).update(`${t}.${rawBody}`).digest('hex')
  // During a secret rotation the header holds two v1= values; accept either.
  return header.split(',').filter((p) => p.startsWith('v1=')).some((p) => {
    const got = p.slice(3)
    return got.length === expected.length && timingSafeEqual(Buffer.from(got), Buffer.from(expected))
  })
}
Python
import hmac, hashlib, time

def verify(raw_body: bytes, header: str, secret: str, tolerance: int = 300) -> bool:
    items = [p.split("=", 1) for p in header.split(",")]
    t = int(next(v for k, v in items if k == "t"))
    if abs(time.time() - t) > tolerance:
        return False
    expected = hmac.new(secret.encode(), f"{t}.".encode() + raw_body, hashlib.sha256).hexdigest()
    return any(hmac.compare_digest(v, expected) for k, v in items if k == "v1")

Event types

AreaEvents
Serversserver.created · server.creation_failed · server.started · server.stopped · server.rebooted · server.deleted · server.action_failed
Accountaccount.approved · account.rejected · account.card_changed · account.closure_scheduled · account.closure_cancelled · account.suspended · account.resumed · account.blocked · account.unblocked
Networkingip.allocated · ip.released · firewall.rule_created · firewall.rule_deleted · port_forward.created · port_forward.deleted
Storagevolume.created · volume.attached · volume.detached · volume.deleted · snapshot.created · snapshot.deleted
Billinginvoice.created · invoice.paid · invoice.payment_failed · budget.threshold · credit.granted · coupon.redeemed · referral.signed_up · referral.rewarded
Platformannouncement.created · announcement.updated · announcement.resolved

Prefer polling? GET /v1/events?after=<last event id> returns the same events. Events and delivery logs are kept for 30 days.

CLI, Terraform and MCP

Three ready-made clients, all built on the same API. Source and downloads are in the clk-cli repository.

clk, the command line

export CLK_API_KEY=clk_live_…
clk servers create --name web-1 --size "CLK Dedicated Extra Small" --label env=prod --ttl 2h --wait
clk servers list --label env=prod
clk servers delete <id> --yes --wait
clk events --follow

Terraform

Resources clk_server, clk_ssh_key and clk_webhook. Only labels change in place; other changes replace the server.

resource "clk_ssh_key" "me" {
  name       = "laptop"
  public_key = file("~/.ssh/id_ed25519.pub")
}

resource "clk_server" "web" {
  name    = "web-1"
  size    = "CLK Dedicated Extra Small"
  ssh_key = clk_ssh_key.me.name
  labels  = { env = "prod" }
}

MCP server for AI agents

clk-mcp speaks the Model Context Protocol, so Claude and other agents can list sizes, create servers, read events and costs. Servers an agent creates are labelled created-by=mcp, it can only delete those, and CLK_MCP_READONLY=1 removes every tool that changes something.

claude mcp add clk -e CLK_API_KEY=clk_live_… -e CLK_MCP_DEFAULT_TTL=2h -- clk-mcp

AI agents

The API is built to be driven by agents as well as people. Give each agent its own key with the narrowest role (read-only or operator), a short expiry and caps, pass ttl when it creates a server so a forgotten server deletes itself, label everything it creates (for example agent=release-bot), and have it delete what it creates. Costs can then be split per agent with /v1/usage?group_by=label:agent, and the audit log shows what each key did.

For tools that read documentation, /llms.txt is an index and /llms-full.txt is the whole API in one plain-text file. The machine-readable OpenAPI spec is linked below.

Errors and limits

Every error has the same shape. Quote the request_id (also in the X-Request-Id header on every response) when you write to support.

HTTP/1.1 403 Forbidden
X-Request-Id: 6f1c…

{ "error": { "code": "forbidden", "message": "this key lacks the scope servers:write", "request_id": "6f1c…" } }
StatuscodeMeaning
400invalid_requestBad or missing field; the message says which
401unauthorizedKey missing, invalid, expired or revoked
403forbiddenThe key lacks the scope this endpoint needs
422key_limit_exceededThe key's own server or monthly cost cap would be exceeded
429rate_limitedToo many requests; wait Retry-After seconds

Rate limit: 120 requests per minute per key. Every response carries X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset (seconds). Other limits: user data up to 16 KB, up to 20 labels per server.

Error codes are stable; messages are for humans and may change.

Versioning

/v1 only changes in backwards compatible ways: new endpoints, optional request fields, response fields, event types and enum values can appear at any time, so ignore what you don't recognise. A breaking change ships as /v2, and /v1 keeps working for at least 12 months after that is announced. Deprecations are announced at least 6 months ahead, and the endpoint then answers with Deprecation and Sunset headers.

Every endpoint is listed in the API reference. The machine-readable OpenAPI spec is at https://api.cloud.clk.no/swagger/v3/api-docs/v1.