00 · Conventions
Conventions
The same set of rules applies to every endpoint in this document. Read this once, then jump to the resource you need.
Authentication
Every request must include a bearer token in the Authorization header:
Authorization: Bearer nkv_live_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6q7r8s9t0u1v2
Three scopes are available, each strictly broader than the previous:
read — read-only access to all resources except billing.
write — read + create/update/destroy. Cannot generate top-up addresses.
billing — everything write can do, plus the billing endpoints.
Tokens are minted at Account → API tokens. The plaintext is shown once at creation — we store only its SHA-256 digest. Clock skew tolerance for any time-based check is 30 seconds.
JSON & types
- Request bodies must be
application/json. We do not accept multipart/form-data except on POST /images.
- Keys are
snake_case. Unknown keys in requests are rejected with 400; unknown keys in responses can be safely ignored by your client.
- Timestamps are RFC 3339 in UTC, e.g.
2026-05-12T08:47:12Z.
- Money is rendered as a JSON number in USD with two decimal places. Coin amounts are rendered as a decimal string to preserve precision.
- Resource IDs are short, prefix-typed, opaque strings. Don\'t parse them.
Idempotency
Every POST endpoint accepts an optional Idempotency-Key header. A UUIDv4 is the standard choice. The first response for a given key is cached for 24 hours; subsequent requests with the same key replay the cached response. A different request body with the same key returns 409 conflict_state.
curl -X POST .../v1/servers \
-H "Idempotency-Key: $(uuidgen)" \
-H "Authorization: Bearer $NOKYC_TOKEN" \
-H "Content-Type: application/json" \
-d '{...}'
List endpoints return up to 25 items by default, 100 max. Cursor-based; no offset. A response that has more items returns has_more: true and a next_cursor. Pass it as ?cursor=... to fetch the next page.
{
"object": "list",
"data": [ /* up to page_size items */ ],
"has_more": true,
"next_cursor": "c_8a1f2c34"
}
Use page_size to override the default. filter[field]=value for equality filters where supported. sort=-created_at for descending sort.
Errors
Errors always return JSON in the same envelope:
{
"error": {
"code": "invalid_request",
"message": "ssh_keys must contain at least one valid key id.",
"errors": [
{ "field": "ssh_keys", "issue": "min_items", "min": 1 }
],
"request_id": "req_2f8b1c4e9d"
}
}
Always log request_id in your client — it is the only thing we can use to find your call in our logs if you ask us to look at one. (We will only look at lines you give us a request_id for; we do not browse customer logs.)
HTTP status codes and their semantic codes:
| HTTP | Code | Meaning |
| 400 | invalid_request | Body or query parameters did not validate. See errors[]. |
| 401 | unauthenticated | Missing, malformed, or expired bearer token. |
| 403 | forbidden_scope | Token does not have the required scope. |
| 404 | not_found | Resource does not exist or is not visible to your account. |
| 409 | conflict_state | Resource is in a state that does not allow this op (e.g. attaching an already-attached volume). |
| 422 | unprocessable | Body parsed but semantic validation failed (e.g. unavailable region for a plan). |
| 429 | rate_limited | Token bucket exhausted. Retry-After header in seconds. |
| 451 | jurisdictional_block | Endpoint refused for legal reasons. Canary will reflect. |
| 500 | internal | Server-side error. Retry only idempotent ops. |
| 503 | maintenance | Endpoint temporarily down during a published maintenance window. |
Rate limits
60 requests/minute per token with a 10-request burst. Bucket replenishes at 1 req/s. Provisioning endpoints (POST /servers, POST /images) use a separate 200 req/minute bucket because they are inherently bursty.
X-RateLimit-Limit: 60
X-RateLimit-Remaining: 57
X-RateLimit-Reset: 1747049892
Retry-After: 23 # only on 429
Versioning
The API is versioned in the path: /v1/.... We commit to:
- No backwards-incompatible change within a major version.
- Additive changes (new fields, new endpoints) on the existing version, announced in the changelog.
- A 12-month deprecation window before any endpoint or field is removed in the next major.
- Beta endpoints are gated behind
X-Nokyc-Beta: true and may break without notice.
Each resource below is documented as a triplet: the object schema, the endpoints that act on it, and one or more example requests with their actual response bodies.
A server is one virtual machine on our hypervisor pool, or one physical node in the dedicated tier. Every other resource on this page either describes a server, attaches to one, or generates events from one.
Object
| Field | Type | Description |
| id ro |
string |
Unique identifier. Prefix srv_. |
| object ro |
string |
Always "server". |
| name |
string · 1–63 |
Customer-provided name. [a-z0-9-], must start with a letter. |
| status ro |
enum |
provisioning, installing, running, stopped, suspended, destroying. |
| plan |
string |
Plan id (see Plans). Immutable after deploy. |
| region |
string |
Region code (see Regions). Immutable. |
| image |
string |
Image id at install time. Updated when the server is rebuilt. |
| ipv4 ro |
object · nullable |
{address, gateway, rdns}. Null until running. |
| ipv6 ro |
object · nullable |
{address, prefix, rdns}. Always a routed /64. |
| ssh_keys |
array<string> |
SSH key ids injected at install time. |
| user_data_b64 |
string · write-only |
Base64-encoded cloud-init user-data. Up to 64 KiB. |
| tags |
array<string> |
Free-form tags. Useful for filtering. |
| labels |
object<string,string> |
Up to 32 key/value pairs. Keys must match [a-z0-9_-]{1,63}. |
| created_at ro |
string · RFC 3339 |
UTC timestamp. |
| updated_at ro |
string · RFC 3339 |
UTC timestamp. Bumped on any mutation. |
Endpoints
-
GET
/servers
List your servers.
-
POST
/servers
Create a new server.
-
GET
/servers/{id}
Retrieve a single server.
-
PATCH
/servers/{id}
Update name, tags, or labels.
-
DELETE
/servers/{id}
Destroy a server. Irreversible.
-
POST
/servers/{id}/reboot
Reboot. Add ?hard=true to skip ACPI.
-
POST
/servers/{id}/rebuild
Reinstall the OS image. Destroys disk data.
-
POST
/servers/{id}/rescue
Boot into the rescue ISO.
-
POST
/servers/{id}/stop
Graceful stop (ACPI poweroff).
-
POST
/servers/{id}/start
Start a stopped server.
-
GET
/servers/{id}/console
One-time signed noVNC URL (5 min validity).
-
GET
/servers/{id}/metrics
30-day CPU / RAM / disk / bandwidth rollup.
-
POST
/servers/{id}/rdns
Set reverse DNS for IPv4 and/or IPv6.
An image is the disk template a server boots from. Official images are maintained by us; custom images are ISOs you uploaded.
Object
| Field | Type | Description |
| id ro |
string |
Prefix img_ (official) or custom_ (custom upload). |
| object ro |
string |
Always "image". |
| type ro |
enum |
official or custom. |
| family ro |
string |
debian, ubuntu, alpine, etc. custom for uploads. |
| version ro |
string |
Upstream version (e.g. 13, 24.04, rolling). |
| variant ro |
string · nullable |
Image variant: minimal, cloud, etc. |
| arch ro |
array<string> |
Supported architectures: amd64, arm64. |
| size_gb ro |
number |
On-disk size, GiB. Useful for predicting deploy time. |
| cloud_init ro |
boolean |
Whether the image ships cloud-init. |
| released_at ro |
string · ISO 8601 |
Date this version was published. |
| deprecated_at ro |
string · ISO 8601 · nullable |
If set, image is read-only and will be removed 90 days after this date. |
Endpoints
-
GET
/images
List images (filter by family, arch, type).
-
GET
/images/{id}
Retrieve a single image.
-
POST
/images
Upload a custom ISO. multipart/form-data, 4 GiB max.
-
DELETE
/images/{id}
Delete a custom image (no-op on official images).
A plan describes one purchasable hardware tier. Prices are USD-reference; the actual amount charged in coin is derived at the spot rate when the daily charge runs.
Object
| Field | Type | Description |
| id ro |
string |
Plan code. Stable across pricing changes. |
| object ro |
string |
Always "plan". |
| category ro |
enum |
vps or dedicated. |
| cpu ro |
integer |
vCPU count (VPS) or physical core count (dedicated). |
| cpu_model ro |
string |
Marketing-free silicon string, e.g. "AMD EPYC 7763 (Milan)". |
| ram_mb ro |
integer |
RAM in MiB. |
| disk_gb ro |
integer |
NVMe disk in GiB. |
| transfer_tb ro |
number |
Monthly outbound transfer in TiB. Inbound is unmetered. |
| price_usd_month ro |
number |
Reference monthly price in USD. |
| price_usd_hour ro |
number |
Hourly cost (used by the daily-charge prorater). |
| available_in ro |
array<string> |
Region codes where this plan is currently provisionable. |
Endpoints
-
GET
/plans
List plans.
-
GET
/plans/{id}
Retrieve a plan.
A region is one of our four PoPs. Each is operated by a separate legal entity and routed independently.
Object
| Field | Type | Description |
| id ro |
string |
Three-letter code: par, rkv, zrh, buh. |
| object ro |
string |
Always "region". |
| name ro |
string |
Human name (Paris, Reykjavík, Zürich, Bucharest). |
| country ro |
string |
ISO 3166-1 alpha-2. |
| pop_id ro |
string |
Internal PoP designation (e.g. nky-par-01). |
| available ro |
boolean |
true if at least one plan can currently be provisioned. |
| ipv4_pool_remaining ro |
integer |
IPv4 addresses left in the regional pool. Sub-50 means provisioning may stall. |
| tor_onion ro |
string · nullable |
Per-region onion endpoint, if available. |
Endpoints
-
GET
/regions
List regions.
-
GET
/regions/{id}
Retrieve a region.
Public keys injected into server authorized_keys at install time. The private half never leaves your machine and is never seen by us.
Object
| Field | Type | Description |
| id ro |
string |
Prefix k_. |
| object ro |
string |
Always "ssh_key". |
| name |
string · 1–63 |
Display name for your benefit. |
| type ro |
enum |
ssh-ed25519 · ssh-rsa · ecdsa-sha2-nistp{256,384,521}. |
| fingerprint ro |
string |
SHA-256, base64-encoded. Format SHA256:<digest>. |
| public_key |
string |
OpenSSH-format public key (omitted from list responses for size). |
| created_at ro |
string · RFC 3339 |
|
Endpoints
-
GET
/ssh-keys
List SSH keys.
-
POST
/ssh-keys
Add a new SSH key.
-
GET
/ssh-keys/{id}
Retrieve a key.
-
DELETE
/ssh-keys/{id}
Remove a key. Does not affect already-deployed servers.
02 · resources
API tokens
Long-lived secrets used to authenticate against this API. The plaintext token is returned once at creation; only its SHA-256 digest is stored server-side.
Object
| Field | Type | Description |
| id ro |
string |
Prefix tok_. |
| object ro |
string |
Always "api_token". |
| name |
string |
For your own bookkeeping. |
| scope |
enum |
read, write, or billing. |
| token |
string · write-only |
Plaintext token. Returned once, in the POST response. Format nkv_live_<52 chars>. |
| last_used_at ro |
string · RFC 3339 · nullable |
Updated at most once per minute to bound write amplification. |
| expires_at |
string · RFC 3339 · nullable |
Optional. If set, the token stops working at this timestamp. |
| created_at ro |
string · RFC 3339 |
|
Endpoints
-
GET
/account/tokens
List tokens.
-
POST
/account/tokens
Create a token (returns plaintext once).
-
DELETE
/account/tokens/{id}
Revoke a token immediately.
Read-mostly endpoints describing your current balance, your transaction history, and the top-up flow.
The balance object
| Field | Type | Description |
| object ro |
string |
Always "balance". |
| usd ro |
number |
USD-reference balance, two decimals. |
| as_of ro |
string · RFC 3339 |
Snapshot time. |
| runway_until ro |
string · date · nullable |
Calendar date the balance will hit zero at current burn. null if no running servers. |
| active_servers ro |
integer |
Servers currently incurring daily charges. |
| daily_cost_usd ro |
number |
Sum of daily prorated cost for all running servers. |
| low_balance_at ro |
string · RFC 3339 · nullable |
When we will email a low-balance warning (currently 7d before runway). |
Endpoints
-
GET
/billing/balance
Current balance + runway.
-
GET
/billing/ledger
Paginated transaction history (charges, top-ups, credits).
-
POST
/billing/topup
Generate a one-time deposit address for a given coin.
-
GET
/billing/topup/{id}
Poll a top-up intent (pending / confirming / credited / expired).
Persistent block storage that can be attached to a server in the same region. Volumes survive server destruction.
Object
| Field | Type | Description |
| id ro |
string |
Prefix vol_. |
| object ro |
string |
Always "volume". |
| name |
string |
Display name. |
| size_gb |
integer · 10–4096 |
Size in GiB. Can be increased online (not decreased). |
| region |
string |
Region code. Must match attached server's region. |
| attached_to ro |
string · nullable |
Server id, or null if detached. |
| device ro |
string · nullable |
Linux block device path when attached (/dev/vdb, …). |
| encrypted |
boolean |
Volume-level LUKS, customer-held key passed via cloud-init. |
| filesystem |
string · nullable |
For your bookkeeping. We don't format volumes. |
| created_at ro |
string · RFC 3339 |
|
Endpoints
-
GET
/volumes
List volumes.
-
POST
/volumes
Create a volume.
-
GET
/volumes/{id}
Retrieve.
-
PATCH
/volumes/{id}
Resize (only size_gb increase) or rename.
-
DELETE
/volumes/{id}
Destroy. Must be detached first.
-
POST
/volumes/{id}/attach
Attach to a server (body: {"server":"srv_…"}).
-
POST
/volumes/{id}/detach
Detach.
Point-in-time image of a server's root disk. Snapshots are deduplicated and stored encrypted at rest. Cannot be exported — they exist to let you rebuild from a known-good state.
Object
| Field | Type | Description |
| id ro |
string |
Prefix snap_. |
| object ro |
string |
Always "snapshot". |
| server ro |
string |
Server id this snapshot was taken from. Frozen at creation. |
| name |
string |
Display name. |
| size_gb ro |
number |
Deduped size; the actual storage you pay for. |
| status ro |
enum |
creating · ready · error. |
| expires_at |
string · RFC 3339 · nullable |
Optional auto-delete date. |
| created_at ro |
string · RFC 3339 |
|
Endpoints
-
GET
/snapshots
List snapshots.
-
POST
/servers/{id}/snapshots
Take a snapshot of a server.
-
GET
/snapshots/{id}
Retrieve.
-
POST
/servers/{id}/restore
Restore a server from a snapshot (rebuild from snap_…).
-
DELETE
/snapshots/{id}
Delete a snapshot.
05 · networking
Firewall groups
Stateful, drop-by-default packet filter enforced at the hypervisor — outside the guest. A group is a named set of rules; each rule allows ingress or egress traffic matching a 5-tuple.
Object
| Field | Type | Description |
| id ro |
string |
Prefix fwg_. |
| object ro |
string |
Always "firewall_group". |
| name |
string |
Display name. |
| applied_to |
array<string> |
Server ids that inherit this group. |
| rules |
array<rule> |
See rule object below. Order matters: first match wins. |
| default_action |
enum |
drop (recommended) or accept. |
| created_at ro |
string · RFC 3339 |
|
Rule object
| Field | Type | Description |
| id ro |
string |
Prefix fwr_. Assigned server-side. |
| direction |
enum |
in or out. |
| proto |
enum |
tcp · udp · icmp · any. |
| port |
string · nullable |
Single port ("22"), comma list ("80,443"), or range ("5000-5100"). Null for non-port protocols. |
| source |
string |
CIDR. 0.0.0.0/0 and ::/0 are accepted. |
| action |
enum |
accept, drop, or reject (sends RST/ICMP-unreach). |
| note |
string · nullable |
Optional comment. |
Endpoints
-
GET
/firewall/groups
List firewall groups.
-
POST
/firewall/groups
Create a group.
-
GET
/firewall/groups/{id}
Retrieve.
-
PATCH
/firewall/groups/{id}
Update name, default_action, applied_to.
-
DELETE
/firewall/groups/{id}
Delete the group (must not be applied).
-
POST
/firewall/groups/{id}/rules
Append a rule.
-
PUT
/firewall/groups/{id}/rules
Replace the rule list (atomic).
-
DELETE
/firewall/groups/{id}/rules/{rule_id}
Remove a single rule.
06 · automation
Webhook subscriptions
Subscribe a URL to events from your account. We POST a JSON event to your endpoint each time something interesting happens, signed with an HMAC-SHA256 you can verify.
Object
| Field | Type | Description |
| id ro |
string |
Prefix whk_. |
| object ro |
string |
Always "webhook". |
| url |
string · HTTPS only |
Endpoint we POST to. HTTPS with a valid certificate required. |
| events |
array<string> |
Subscribed events. ["*"] subscribes to all. |
| active |
boolean |
If false, deliveries are paused. |
| secret_hint ro |
string |
Last 4 chars of the signing secret (whsec_…) for your reference. Full secret returned once at creation. |
| created_at ro |
string · RFC 3339 |
|
Endpoints
-
GET
/webhooks
List subscriptions.
-
POST
/webhooks
Create. Response includes secret (plaintext, once).
-
GET
/webhooks/{id}
Retrieve.
-
PATCH
/webhooks/{id}
Update url, events, active.
-
DELETE
/webhooks/{id}
Unsubscribe.
-
POST
/webhooks/{id}/test
Send a synthetic ping event.
-
GET
/webhooks/{id}/deliveries
Last 7 days of delivery attempts.
07 · observability
Audit log
Every authenticated, state-changing request made against your account, with the actor that made it. 90-day retention.
Object
| Field | Type | Description |
| id ro |
string |
Prefix audit_. |
| object ro |
string |
Always "audit_event". |
| action ro |
string |
Dotted operation name (e.g. server.create). |
| resource ro |
string · nullable |
Affected resource id. |
| actor_token ro |
string · nullable |
Token id used, if API. |
| actor_session ro |
string · nullable |
Session id used, if dashboard. |
| request_id ro |
string |
Correlation id (shared with the X-Request-Id header on the original response). |
| ip_country ro |
string |
ISO-3166 country code at probe time. No IP stored. |
| result ro |
enum |
ok · error · denied. |
| created_at ro |
string · RFC 3339 |
|
Endpoints
-
GET
/audit/events
List audit events. Filterable by action, resource, from, to.
-
GET
/audit/events/{id}
Retrieve one event.
Example · create a server
End-to-end deploy in one shell session
The minimum viable flow from "I have an account" to "I have a running server":
Request
curl https://api.nokycvps.com/v1/servers \
-H "Authorization: Bearer $NOKYC_TOKEN" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{
"name": "edge-paris",
"plan": "vps-s2",
"region": "par",
"image": "debian-13-minimal",
"ssh_keys": ["k_5f3a8c91"],
"tags": ["production","edge"],
"labels": { "owner":"ops" },
"user_data_b64": null
}'
Response — 201 Created
{
"id": "srv_8a1f2c34",
"object": "server",
"name": "edge-paris",
"status": "provisioning",
"plan": "vps-s2",
"region": "par",
"image": "debian-13-minimal",
"ipv4": null,
"ipv6": null,
"ssh_keys": ["k_5f3a8c91"],
"tags": ["production","edge"],
"labels": { "owner":"ops" },
"created_at": "2026-05-12T08:47:12Z",
"updated_at": "2026-05-12T08:47:12Z"
}
Two webhook events later (typically 60–120 s):
jsonserver.running webhook
{
"id": "evt_3c8a1f4b",
"object": "event",
"type": "server.running",
"created_at": "2026-05-12T08:48:34Z",
"data": {
"server": {
"id": "srv_8a1f2c34",
"status": "running",
"ipv4": { "address":"198.51.100.42", "gateway":"198.51.100.1", "rdns":null },
"ipv6": { "address":"2001:db8::1", "prefix":"2001:db8::/64", "rdns":null }
}
}
}
You can ssh [email protected] now.
08 · Webhook events
Event catalogue
Every event is one of the types below. The data envelope contains the full object as it existed at event time — you should not re-fetch unless you need a guaranteed-fresh view.
| Event | When it fires |
| server.created |
A new server has begun provisioning. |
| server.running |
Server has finished installing and is reachable. |
| server.stopped |
Server gracefully shut down. |
| server.rebooted |
Server completed a reboot cycle. |
| server.rebuilt |
Server finished a rebuild from an image. |
| server.suspended |
Balance went negative; server stopped. |
| server.destroyed |
Server's disk has been wiped and IPs released. |
| snapshot.ready |
Snapshot moved from creating to ready. |
| volume.attached |
Volume successfully attached. |
| volume.detached |
Volume detached. |
| topup.pending |
Deposit detected on chain; waiting for confirmations. |
| topup.confirmed |
Deposit confirmed; balance credited. |
| topup.expired |
Rate lock expired before confirmation. |
| billing.charged |
Daily charge applied. Includes amount and resulting balance. |
| billing.low_balance |
Balance projects to zero within 7 days at current burn. |
| token.created |
A new API token was created. |
| token.revoked |
An API token was revoked. |
| firewall.applied |
A firewall group was attached to a server. |
09 · Webhook signing
Verifying webhook authenticity
Every delivery includes an X-Nokyc-Signature header. You compare it against an HMAC-SHA256 of the request body, keyed by the secret returned to you at subscription time.
Header format
X-Nokyc-Signature: t=1747049892,v1=4f8c1a...e9d
X-Nokyc-Delivery-Id: del_7c3a1f9b
Content-Type: application/json
t — Unix epoch the signature was generated.
v1 — lowercase hex HMAC-SHA256 of "{t}.{raw_body}".
- Reject any delivery where
t drifts more than 5 minutes from your clock.
Verification — Python
pythonverify_signature.py
import hmac, hashlib, time
def verify(raw_body: bytes, header: str, secret: str, tolerance: int = 300) -> bool:
parts = dict(p.split("=", 1) for p in header.split(","))
t = int(parts["t"])
v1 = parts["v1"]
if abs(time.time() - t) > tolerance:
return False
payload = f"{t}.".encode() + raw_body
expected = hmac.new(secret.encode(), payload, hashlib.sha256).hexdigest()
return hmac.compare_digest(v1, expected)
Retry policy
Each delivery is attempted up to 6 times at the following back-off intervals:
attempt 1: immediate
attempt 2: + 1 minute
attempt 3: + 5 minutes
attempt 4: + 30 minutes
attempt 5: + 5 hours
attempt 6: + 24 hours
=> give up
A delivery succeeds when your endpoint returns HTTP 2xx within 10 seconds. 410 Gone short-circuits all future attempts and pauses the subscription. Failed deliveries are visible via GET /webhooks/{id}/deliveries.
Additive changes within v1. The next major version (v2) is not currently scheduled.
-
- Added
/servers/{id}/metrics endpoint (30-day rollup).
- Added
runway_until and low_balance_at to the balance object.
- Webhooks now include
X-Nokyc-Delivery-Id for replay-safety.
-
- Added Volumes resource (block storage with attach/detach).
- Firewall groups gained
default_action and note on rules.
- Server
labels capped at 32 pairs (was 16).
-
- Added Snapshots resource and
/servers/{id}/restore.
- Added
ipv4_pool_remaining on regions.
-
- Switched list endpoints to cursor pagination.
offset/limit deprecated; will be removed in v2.
- Added
Idempotency-Key support on every POST.
- Added Audit log resource.
-
- Initial stable release.
- Servers, Images, Plans, Regions, SSH Keys, API Tokens, Billing, Webhooks.