API standards
qibdo Cloud’s API follows the API Improvement Proposals (AIP).
Every service exposes the same resource-oriented shape, so once you learn how one service works, you know
how they all work.
Why AIP
Section titled “Why AIP”The API Improvement Proposals are a well-documented, openly
standardised set of rules, best practices, and patterns for API design. We adopted them
deliberately: rather than invent platform-specific conventions, every service inherits one mature,
consistent design language, the same one that large public clouds already use. Resources,
collections, and the verbs that act on them follow the same rules everywhere, so learning one
service lets you predict the rest, and an engineer who has used a public cloud is productive on
qibdo from day one.
That choice is reinforced by how the API is built. Each service is defined once in gRPC and Protocol Buffers, which gives us a single, strongly-typed, well-defined contract. A gateway then transcodes between REST/JSON and gRPC in both directions, following AIP-127 (HTTP and gRPC transcoding): REST clients call ordinary HTTP/JSON endpoints, gRPC clients use the typed interface, and both reach the exact same API. There is no separate, hand-written REST layer that can drift from the contract.
Because that one contract is the source of truth, the OpenAPI specification, the generated SDKs, and these reference pages are all derived from it. What you read here is what the API actually does.
Resource-oriented design
Section titled “Resource-oriented design”Resource-oriented design is the RESTful model: the API is organized around resources
(a VM, a bucket, a role) grouped into collections, each addressed by a URL and manipulated
with standard HTTP methods. qibdo adopts that model through AIP’s resource conventions
(AIP-121) and exposes it over REST by transcoding the gRPC
contract. URLs are hierarchical and name each parent in the path; the target resource is
identified by {id}, and parents are named by their resource type.
# A workspace in Taxonomy/taxonomy/v1/organisations/{organisation}/workspaces/{id}
# A virtual machine in Compute/compute/v1/workspaces/{workspace}/zones/{zone}/engines/qibdo/vms/{id}Most resources are scoped to a workspace, the boundary that every service uses to isolate your teams, environments, and projects (see Taxonomy).
Standard methods
Section titled “Standard methods”Five standard methods cover the lifecycle of every resource. The verb, URL, and request body are consistent across services:
| Method | HTTP | Path | Body |
|---|---|---|---|
| Get | GET | …/<collection>/{id} | (none) |
| List | GET | …/<collection> | (none) |
| Create | POST | …/<collection> | the resource |
| Update | PATCH | …/<collection>/{id} | the resource + an update mask |
| Delete | DELETE | …/<collection>/{id} | (none) |
For example, to create and then fetch a VM:
# Create (returns an operation, see Standard responses)curl --request POST \ --url https://api.qibdo.example.com/compute/v1/workspaces/$WS/zones/$ZONE/engines/qibdo/vms \ --header "Authorization: Bearer $QIBDO_API_TOKEN" \ --header 'Content-Type: application/json' \ --data '{ "name": "web-1", "cpu": { "v_cpus_boot": 2 } }'
# Getcurl --url https://api.qibdo.example.com/compute/v1/workspaces/$WS/zones/$ZONE/engines/qibdo/vms/$ID \ --header "Authorization: Bearer $QIBDO_API_TOKEN"Custom methods
Section titled “Custom methods”Actions that don’t map cleanly to create/read/update/delete use a custom method: a verb appended to the resource URL after a colon. Lifecycle actions on a VM, for instance:
POST …/vms/{id}:startPOST …/vms/{id}:stopPOST …/vms/{id}:restartPOST …/vms/{id}:suspendPOST …/vms/{id}:resumePOST …/vms/{id}:resetOther examples include …/workspaces/{id}:move and /iam/v1/tokens:revoke. Like the standard
mutations, these custom methods return an operation.
Updating resources
Section titled “Updating resources”Updates use PATCH with a field mask so you change only the fields you intend to:
- Send the resource with the fields you want to set, plus an
update_masklisting those field paths. Only the masked fields are written; everything else is left untouched. - Omit the mask to update every field present in your request body.
- Use
*as the mask for a full replacement (PUT-style semantics).
This makes partial updates safe and explicit: you never accidentally clear a field you didn’t send.
Read, modify, write
Section titled “Read, modify, write”You can send a resource straight back the way you received it. Fetch it with GET, change the
fields you care about, and PATCH the whole thing — server-managed fields like create_time and
id come along unchanged and are accepted, because their values still match what the server holds.
# Fetch, change one field, send the whole thing back. This is safe.curl --url https://api.qibdo.example.com/vault/v1/workspaces/$WS/engines/qibdo/secrets/$ID \ --header "Authorization: Bearer $QIBDO_API_TOKEN" \ | jq '.name = "billing-api-key"' \ | curl --request PATCH \ --url https://api.qibdo.example.com/vault/v1/workspaces/$WS/engines/qibdo/secrets/$ID \ --header "Authorization: Bearer $QIBDO_API_TOKEN" \ --header 'Content-Type: application/json' \ --data @-You do not have to strip a response down to the fields you changed before sending it.
Fields you cannot change
Section titled “Fields you cannot change”Some fields are fixed once the resource exists — a resource’s location_id, for instance. How the
API answers depends on what your request said, and the difference is deliberate:
| Your request | Result |
|---|---|
Names the field in update_mask | Rejected. Naming a field in a mask says you intend to change it |
| Includes the field, unchanged, with no mask | Accepted. It is the value you were given back; nothing is being asked of it |
| Includes the field with a different value, no mask | Rejected, naming the field |
Output-only fields such as create_time are ignored if you send them, changed or not, per
AIP-203.
Listing: pagination, filtering, and ordering
Section titled “Listing: pagination, filtering, and ordering”A List endpoint accepts these four standard query parameters for narrowing results:
| Parameter | Purpose |
|---|---|
page_size | Maximum items per page (default 20, capped at 100). |
page_token | Opaque cursor for the next page (from a prior response’s next_page_token). |
filter | An AIP-160 filter expression to narrow results. |
order_by | A comma-separated list of fields to sort by (append desc to reverse). |
There are no per-field filter parameters; all filtering goes through the single filter
string. See Filtering for the language, and
Standard responses for the list response shape.
A small number of listings depart from this shape, because the data behind them is not tabular and cannot honour tabular query semantics. A field predicate assumes a set of records with addressable, indexed attributes; where that assumption does not hold, offering one would be misleading at best and ruinously expensive at worst.
The rule, rather than a census: a listing declares the parameters it accepts, and its reference
page plus the discovery endpoint are authoritative. A listing may narrow by something other than
filter (object storage takes prefix and delimiter, because a bucket is a flat keyspace rather
than a table), be enumerated by the store that owns it (time-series and log data are queried by
their own languages), or accept a restricted dialect of the filter language rather than all of it.
Each such listing states what it accepts on the service page that owns it.
Cross-collection reads
Section titled “Cross-collection reads”Some Get endpoints let you fetch a resource by its id without knowing which parent it lives
under, by using - as that parent path segment (AIP-159). For
example, to read a resource pool by id across every region and zone:
GET /topology/v1/regions/-/zones/-/resource-pools/{id}You can substitute - for any parent the endpoint marks as supporting it, and you need
permission at a scope that covers the resolved resource. Each resource’s reference notes which
parents accept -; List endpoints do not (listing resource pools, for instance, is scoped to a
single region and zone) and require every parent to be a concrete id.
Versioning
Section titled “Versioning”The major version is part of the path (/compute/v1/..., /iam/v1/...). A new major version
is introduced only for breaking changes and is served alongside the previous one; within a
major version, changes are additive and backward-compatible.
Conventions worth knowing
Section titled “Conventions worth knowing”snake_casefields. JSON field names mirror the Protobuf fields, insnake_case.- Server-managed fields are read-only. Fields such as
id,created, andupdatedare output-only; if you send them on create or update, they’re ignored. - Timestamps are RFC 3339. For example
2026-06-04T12:30:00Z. - Monetary values are an exact decimal string plus an ISO 4217 currency code. For example
{"amount": "12.34", "currency_code": "USD"}.
Monetary values
Section titled “Monetary values”Every monetary value the API publishes has the same two-field shape: an amount and a
currency_code. Hand both to a locale-aware currency formatter and you are done — there is no
arithmetic to perform, no unit to convert, and no currency exponent to look up.
{ "amount": "12.34", "currency_code": "USD" }The amount is a string, and deliberately so. A JSON number is a binary floating-point value,
which cannot represent a figure as ordinary as 0.10 exactly; sending money that way would mean
publishing a value that is close to the one we hold rather than equal to it. A decimal in text has
no such loss, and every language can parse one.
Four guarantees come with it, and they are worth knowing because they let you compare and store these values without defensive handling:
- The amount is exact. It is the value the platform holds, not a rounding of it.
- It is normalised. No trailing zeros, no exponent notation, and no negative zero — so two
numerically equal amounts are always character-for-character identical, and comparing or
de-duplicating them never reports a difference that is not there. A figure stored as
12.3400is published as12.34, and a whole hundred as100. - It carries at most eight fraction digits, which is the finest granularity the platform stores. Nothing published will outrun what you can persist alongside it.
- The sign is on the amount itself, so a negative value is unambiguous.
The currency_code is an ISO 4217 alphabetic code, such as USD. It is a plain string rather than
a fixed list because ISO 4217 is an external standard that changes without reference to this
platform.
Values are never pre-rendered for display. Presentation depends on the reader’s locale, which the platform does not know, so it publishes the value and leaves the formatting to you.