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>:
| Resource | Prefix | Example |
|---|---|---|
| Virtual machine | vm_ | vm_01H8FZQ4XW9KMP4XQNR3VT7DC2 |
| Managed database | db_ | db_01H8FZQ4XW9KMP4XQNR3VT7DC2 |
| Block volume | vol_ | vol_01H8FZQ4XW9KMP4XQNR3VT7DC2 |
| Snapshot | snap_ | snap_01H8FZQ4XW9KMP4XQNR3VT7DC2 |
| API key | key_ | 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 request fails validation:422validation_error(Errors).cursor— opaque next-page pointer. The response body returns it aspagination.next_cursorwhen there are more pages; pass it back ascursorto 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_error",
"message": "request validation failed",
"details": {
"errors": [
{
"loc": ["body", "event_type"],
"msg": "Field required",
"type": "missing"
}
]
},
"request_id": "7e4b1d9c2a6f4e8b9d3c5a1f0e2b4c68"
}
}
code— stable machine-readable string. Switch on this, not on the status code, when the same status has multiple meanings (e.g.402can bequota_exceeded,not_entitledorworkspace_frozen).message— human-readable English for logs. Not localized; if you show an error to end users, mapcodeto your own text.details— present on some codes. On a validation error,details.errorslists the failing inputs; on many other errors it carries areasonthat refines the code, with the values that go with it. Its shape depends on the code.request_id— on every error body the API sends: the same id as theX-Request-IDheader every response from the API carries. A response from the edge in front of it (a502or504, for example, or a413for a body too large to reach the API) carries neither the API's error body nor theX-Request-IDheader (Errors). Log it, and include it when contacting support.next_code— sent only while a code is being renamed: the code this error will carry afterwards. When it's present, switch on it instead ofcode. No code is being renamed today, so the API doesn't send it.
Errors lists every code and reason, and which of the Webhooks and Inboxes APIs sends each. (Earlier versions of this page showed validation_failed as a 400, with fields and request_id in the body. The APIs have never sent the first two: a validation failure is a 422 validation_error, with details. request_id was not in the body then either; the API's error bodies carry it now.)
HTTP status codes
We use a small, predictable set:
| Status | When |
|---|---|
200 | Success with body. |
201 | Resource created. Location header points at the new resource. |
202 | Operation accepted; check operation.status to track progress. |
204 | Success, no body (typical for DELETE). |
400 | The request couldn't be read as sent. error.message says why. |
401 | No usable credential. See Authentication. |
402 | Your plan or billing state stops the request (a quota, a feature not in your plan, a frozen workspace). Retrying won't help. |
403 | Authenticated but not authorized (scope missing, wrong project, etc.). On Webhooks, also a credential it doesn't accept. |
404 | Resource doesn't exist, OR the caller can't see it. We deliberately don't distinguish — leaking existence is itself an access leak. |
409 | State conflict (name already taken, an Idempotency-Key reused with a different body, etc.). |
413 | The body is over the size limit. |
422 | Validation failed. On a validation error (validation_error), error.details.errors lists the failing inputs; on the Webhooks API's other 422 codes, such as invalid_payload or unknown_event_type, it is an object naming the value or rule at fault. |
429 | Rate limited. When Retry-After is set, it tells you when to retry. |
500–504 | Server-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
POSTreturns202while the hypervisor allocates the instance. - Database
POSTreturns202while 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:
- Poll —
GET /v1/operations/{op_id}once a second. Cheap; counts as a regular API call against rate limits. - Webhook — subscribe a callback URL in the dashboard. We POST when the operation reaches a terminal state (
succeededorfailed).
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
- Authentication covers the auth header shape and scope model.
- The reference pages — VMs, Managed databases, Block storage, Object storage, Regions — show the conventions in action.