NimbusNexus

Conventions

The API has one shape, repeated across every resource. Once you know the shape, you know every endpoint — the only thing that changes from one resource to the next is the field set on the resource itself.

Resource ids

Every resource has a stable opaque id, in the form <prefix>_<26-char-base32>:

ResourcePrefixExample
Virtual machinevm_vm_01H8FZQ4XW9KMP4XQNR3VT7DC2
Managed databasedb_db_01H8FZQ4XW9KMP4XQNR3VT7DC2
Block volumevol_vol_01H8FZQ4XW9KMP4XQNR3VT7DC2
Snapshotsnap_snap_01H8FZQ4XW9KMP4XQNR3VT7DC2
API keykey_key_01H8FZQ4XW9KMP4XQNR3VT7DC2

Ids are stable for the life of the resource — never reused, never rewritten on rename. Use them in URLs, foreign keys, and audit logs; use human-readable name fields only for display.

Pagination

List endpoints accept two query parameters:

  • limit — page size. Default 50, max 200. Past 200 the server returns 400.
  • cursor — opaque next-page pointer. The response body returns it as pagination.next_cursor when there are more pages; pass it back as cursor to advance.

Cursors are opaque — don't try to decode or construct them. They include sort + filter state from the original request, so changing filters mid-pagination produces undefined behavior. Start fresh (no cursor) when your filters change.

The pattern in code:

# Page 1
curl 'https://api.nimbusnexus.net/v1/vms?limit=50' \
  -H "Authorization: Bearer $NIMBUS_KEY"

# Page 2 (cursor from page 1's response.pagination.next_cursor)
curl 'https://api.nimbusnexus.net/v1/vms?limit=50&cursor=eyJpZCI6...' \
  -H "Authorization: Bearer $NIMBUS_KEY"

When pagination.next_cursor is absent or null, you're on the last page.

Errors

Every error response uses the same JSON shape:

{
  "error": {
    "code": "validation_failed",
    "message": "size must be one of the published VM sizes",
    "fields": {
      "size": "unknown size 'gp-7-7'"
    },
    "request_id": "req_01H8FZ..."
  }
}
  • code — stable machine-readable string. Switch on this, not on the status code, when the same status has multiple meanings (e.g. 409 can be already_exists or state_conflict).
  • message — human-readable for logs. Not localized; pull text from the dictionary if you're rendering it to end users.
  • fields — present on 400 validation_failed. Keyed by the request field that failed.
  • request_id — always present. Include it when contacting support.

HTTP status codes

We use a small, predictable set:

StatusWhen
200Success with body.
201Resource created. Location header points at the new resource.
202Operation accepted; check operation.status to track progress.
204Success, no body (typical for DELETE).
400Validation failed. error.fields has per-field details.
401Authentication failed. See Authentication.
403Authenticated but not authorized (scope missing, wrong project, etc.).
404Resource doesn't exist, OR the caller can't see it. We deliberately don't distinguish — leaking existence is itself an access leak.
409State conflict (resource is mid-operation, name already taken, can't delete a non-empty bucket, etc.).
422Request shape is JSON-valid but semantically wrong in a way that's not a single-field validation issue.
429Rate limit exceeded. Retry-After header tells you when to retry.
500–504Server-side problem. Always safe to retry idempotent requests.

Idempotency

Mutating endpoints (POST, PUT, PATCH, DELETE) accept an optional Idempotency-Key header. Pass any unique value (a UUID is conventional); we cache the response for 24 hours and return the same response on a repeat with the same key.

curl -X POST https://api.nimbusnexus.net/v1/vms \
  -H "Authorization: Bearer $NIMBUS_KEY" \
  -H "Idempotency-Key: 8e1c91a4-04d0-4d77-8e6e-2f7c1a5b8e84" \
  -H "Content-Type: application/json" \
  -d '{ "name": "web", "size": "gp-2-4", "region": "us-east-1", "image": "ubuntu-24.04" }'

Use idempotency keys when your client might retry a request after a network failure but can't tell whether the original succeeded. The default-no-key behavior is "best effort retry"; with a key, retries are safe.

GET endpoints are always idempotent and don't need the header.

Long-running operations

Some actions don't complete during the request lifecycle:

  • VM POST returns 202 while the hypervisor allocates the instance.
  • Database POST returns 202 while replicas come up.
  • Volume resize and VM migration return 202.

The response body for any 202 includes an operation object:

{
  "id": "vm_01H8FZQ4XW9KMP4XQNR3VT7DC2",
  "state": "provisioning",
  "operation": {
    "id": "op_01H8FZ...",
    "kind": "vm_create",
    "status": "running",
    "progress": 0.4,
    "started_at": "2026-05-19T18:30:00Z"
  }
}

Two ways to track progress:

  1. PollGET /v1/operations/{op_id} once a second. Cheap; counts as a regular API call against rate limits.
  2. Webhook — subscribe a callback URL in the dashboard. We POST when the operation reaches a terminal state (succeeded or failed).

Most operations finish in under 60 seconds. The exception is VM image pulls on first use (~90 seconds) and Kafka cluster setup (~180 seconds).

Timestamps

All timestamps are RFC 3339 / ISO 8601 in UTC with the Z suffix:

2026-05-19T18:30:00Z

Never local time, never with an offset, never an integer epoch (except in webhook signature headers, which carry a Unix timestamp because that's the convention HMAC signers use). Date.parse() in JavaScript and equivalent stdlib parsers in other languages handle this format natively.

What's next