# API conventions The rules every WAMP Cloud endpoint shares — one bearer scheme, caller-owned ids with idempotent PUT, three pagination schemes, content negotiation, and how to read the generated operation reference. Source: https://docs.cloud.vampikez.fun/reference/api/ This page is the part of the reference that is the same everywhere. Learn it once and every endpoint becomes predictable: how you authenticate, how you make a write safe to retry, how you page, what a response body looks like, and where to find the exact fields of any one operation. The endpoint-by-endpoint reference is generated from the contract the server serves. It starts at [Operations](/api/operations/tags/sessions/), with every object shape in [the API reference](/api/). [Organization environments](/concepts/environments/) have their own revisioned document and write-only secrets. The `environments[0]` entry in `GET /v1/capabilities` still describes the managed sandbox platform; it does not list organization environment objects. ## Base URL and versioning There is no public hostname baked into the API. Your operator gives you the origin that serves WAMP Cloud, and every path in this documentation is relative to it: ```bash export WAMP_API=https://api.example.com # the Cloud origin export WAMP_ACCOUNT=https://accounts.example.com # the Account origin ``` Two origins can be involved. The Cloud origin serves every `/v1` path. The token exchange that mints your bearer (`POST /auth/app-installation-token`) belongs to the Account service, which may be deployed on a different host — see [Authentication](/start/authentication/). In a single-host deployment both are the same origin, but never assume it: keep them as two configuration values. The version lives in the path. `/v1` is the only public prefix; there is no version header, no date-pinning parameter, and no per-account version. The contract document reports `1.0.0` as its own version, which tracks the document, not a negotiable API generation. Inside `v1`, change is additive. New event types, new fields on existing objects, and new error codes can appear without a new prefix. Two consequences for your client: - **Ignore what you do not recognize.** The contract types an event's `type` as a string whose unknown future values must be ignored safely, and both the event `data` object and the error body allow properties beyond the ones documented. A client that rejects unknown fields will break on a routine release. - **Do not treat a string field as a closed enum unless the contract declares one.** The contract is explicit about which fields are closed: `run.status`, `continuation.state`, `continuation.reason` and `session.phase` all carry a declared `enum` and are safe to switch on exhaustively, while an event `type` and an error body's `error` are open and additive. `session.phase` used to be the exception here and no longer is — it is closed, but it is still written after the fact and can trail a run's terminal transition, so branch on `run.status` and on the event stream and render `phase`. ### Which build is answering you `1.0.0` tells you which contract you are reading. It does not tell you which build is serving it — and inside `/v1` behaviour changes without the version moving, so when a page here and the server disagree there are two causes that need opposite responses: the page is wrong, or the page describes a release your deployment has not deployed yet. `GET /v1/openapi.json` names the build. Its `info` object carries one extra field: ```json { "info": { "title": "WAMP Cloud API", "version": "1.0.0", "x-wamp-release": "9251c9538f2b4c1d7a0e6b3f8c5d2a91e4f70b6c" } } ``` `x-wamp-release` is the exact release identity of the build that answered you — opaque to you, resolvable by us, and the one string to put in a support ticket. It is present on any deployment that pins a release and **absent** on one that does not; there is no placeholder value, because an identity that can be wrong would still get quoted. To tell the two causes apart before you write that ticket, use the fact that `GET /v1/openapi.json` and `GET /v1/docs` are served *by the running build*, so they describe the release answering you and cannot be ahead of it. This site is published per release and can be. So: - The **served** contract agrees with the server and this page does not → this page is describing another release. Wait for the deploy, and build against the served contract meanwhile. - The **served** contract agrees with this page and the server does not → that is a defect. Report it with `x-wamp-release` and the request id. ## One header scheme, two credentials Every `/v1` request carries a bearer token: ```bash curl -sS "$WAMP_API/v1/capabilities" -H "Authorization: Bearer $WAMP_TOKEN" ``` Integration operations use the `cloudInstallationBearer` scheme and there is no second header to learn. Two different credentials go into it, and [Authentication](/start/authentication/) is where you choose: an **installation API key**, which an administrator creates once and hands you, and an **installation token**, which your app mints from its own signing key. Both name exactly one installation, carry the same capabilities, and are accepted identically by every operation below. The contract also describes `cloudNativeBearer` for the first-party `GET /v1/me` identity check. That token is issued only to WAMP Desktop's native PKCE client; third-party integrations cannot request it and should continue to use `cloudInstallationBearer`. Specifics that are easy to get wrong: | Rule | Detail | |---|---| | Header only | A request without an `Authorization: Bearer ` header is `401 bearer_credential_required`, even if the caller holds a valid WAMP Cloud browser session cookie. `/v1` is a service API and deliberately ignores ambient cookies. | | Audience-bound | The credential is bound to the audience `wamp-cloud`. One issued for any other resource fails introspection as `401 invalid_or_expired_credential`. | | Lifetime depends on which one you hold | An installation **token** lives 600 seconds: mint on demand, cache until shortly before expiry, never persist one for hours. An installation **API key** lives until it is revoked or its optional expiry passes, and belongs in your secret manager. | | Tenant is in the credential | Do not send `X-Wamp-Organization` with a bearer. The credential already names its exact organization, and sending the header is `400 organization_header_not_allowed`. | | Capability-checked per operation | Each operation requires one capability. Missing it is `403 insufficient_scope` with the required capability named in `requiredScope`. | | Revocation is immediate for both | Capabilities are re-resolved from the live installation record on every request, so a revoked grant stops both credentials at once — a key is not a snapshot of the authority it was created with. | Two endpoints are unauthenticated: `GET /v1/openapi.json` and `GET /v1/docs`. Everything else requires the bearer. One capability boundary is worth planning around: discovery (`GET /v1/capabilities`) requires `wamp.cloud.sessions:create`, not `sessions:read`. An integration granted read-only access can list and follow sessions but cannot call discovery. ## Caller-owned ids and idempotent PUT This is the most important convention in the API. **Every command that creates work is a `PUT` to an identifier you mint** — sessions, turns, publications, merges — and that identifier is the idempotency mechanism. There is no `Idempotency-Key` header anywhere in this API, and you do not need one. The rest of the mutating surface creates nothing and is conventional: `PATCH` edits a session's title, `DELETE` archives one, and `POST …/cancel` requests cancellation. You generate a UUID, you address the resource with it, and you may repeat the identical request as many times as you like: ```bash SESSION_ID=$(uuidgen | tr 'A-Z' 'a-z') TURN_ID=$(uuidgen | tr 'A-Z' 'a-z') curl -sS -i -X PUT "$WAMP_API/v1/sessions/$SESSION_ID" \ -H "Authorization: Bearer $WAMP_TOKEN" \ -H 'Content-Type: application/json' \ -d "{\"initialTurn\":{\"id\":\"$TURN_ID\",\"message\":\"Fix the invoice export and summarize the change.\"},\"model\":\"\"}" ``` ```http HTTP/1.1 201 Created Location: /v1/sessions/3f7d4f4c-2b6a-4a2e-9c1a-1f2b3c4d5e6f ``` Send it again unchanged and you get `200 OK` with the same body and the same `Location`. Nothing is created twice. | Operation | First call | Same id, same body | Same id, changed body | |---|---|---|---| | `PUT /v1/sessions/{sessionId}` | `201` | `200`, existing session | `409 cloud_session_conflict` | | `PUT …/turns/{turnId}` | `202`, `created: true` | `202`, `created: false` | `409 cloud_turn_conflict` | | `PUT …/publications/{publicationId}` | `201` | `200` | `409 cloud_publication_conflict` | | `PUT …/merges/{mergeId}` | `201` | `200` | `409 cloud_publication_merge_conflict` | | `PUT …/sessions/{sessionId}/workspace` | `200` | `200` | no body to change | | `POST …/runs/{runId}/cancel` | `200` | `200` | no body to change | | `PATCH /v1/sessions/{sessionId}` | `200` | last write wins | last write wins | `PUT …/workspace` is the one `PUT` that is idempotent on a *postcondition* rather than on a caller-owned id: it takes no request body, and it always answers `200` with the session resource — never `201` — because what it guarantees is "this session has a live workspace", not "this resource now exists". It requires `wamp.cloud.turns:submit`, since restoring a workspace is exactly what admitting a turn does implicitly, and it is `409 cloud_resume_unavailable` when the previous state cannot be restored at all. `PATCH` is the one write that is not idempotent by id, because it is an update rather than a creation. It also requires at least one of `title`, `environment`, `model`, `runtime`: an empty object is `400 invalid_request`. ### What counts as "the same body" The comparison is over the fields that define the resource, not over every byte you sent. For a session, the replay identity is the complete normalized create command: `title`, `initialTurn` (including its caller-owned id and message), `model`, the settled `runtime`, `origin`, the requested `source`, and the requested `environment` (or its omission). A later `PATCH` changes the resource projection, not that stored command identity, so replaying the original `PUT` remains safe. For a public turn, the identity is exactly `message`, `replyTo` and the files `attachments` named, compared in order by their bytes' digest and size. Model and runtime belong to the Session; the server snapshots the settled configuration onto a newly admitted Turn and does not accept them in the Turn request. For a publication or a merge the server stores a hash of the accepted request, so any change to the body of an existing id is a conflict. ### How to use this in a client 1. Mint the UUID **before** the call and record it with your own work item. It is your handle on the resource whether or not the response arrives. 2. On a timeout, connection reset, or `502`, repeat the identical request. The worst case is a `200` telling you it already happened. 3. Treat `409 …_conflict` as a bug in your own code — it means the same id was reused for different work, not that the server is busy. The busy conflicts are separate codes; see [Errors](/reference/errors/). ## Turn id is the run id A turn and the run that executes it are one durable pair that shares one identifier. The `turnId` you mint **is** the `runId`. ```http PUT /v1/sessions/{sessionId}/turns/{turnId} → 202 Accepted Location: /v1/sessions/{sessionId}/runs/{turnId} ``` So you know the run id before the request returns, `GET …/runs/{turnId}` works immediately, and `subject.runId` on an event matches the turn you submitted. There is no separate run-id lookup step, and no endpoint that mints a run id for you. `202` rather than `200` is accurate: the turn and a `queued` run are committed durably before the response, and no sandbox has been provisioned yet. ## Inspecting execution `GET /v1/sessions/{sessionId}/execution` reports the active run and lease fence for a session, and nothing else — no content, no transcript: ```json { "execution": { "runId": "9a8b7c6d-5e4f-4a3b-8c2d-1e0f9a8b7c6d", "leaseId": "…", "providerRef": "i8cefvn9chd1d1s65euj", "expiresAt": "2026-08-11T09:30:00.000Z", "lastEngineObservedAt": "2026-08-11T09:01:12.000Z" } } ``` It requires `wamp.cloud.sessions:read`, returns `execution: null` when nothing is executing, and `404 cloud_session_access_not_found` when the session is not visible to the caller. `providerRef` is the opaque exact provider execution handle and is `null` only while the admitted lease is still starting; `lastEngineObservedAt` is `null` until an engine reports in. Treat this as an operational inspection aid for release and incident checks; it is not part of the normal follow loop, which is the event log plus `GET …/runs/{runId}`. ## Pagination Three schemes, one per collection shape. They are not interchangeable, and each list endpoint accepts exactly one of them. | Endpoint | Cursor parameter | `limit` | Response cursor | |---|---|---|---| | `GET /v1/sessions` | `cursor` (opaque string) | 1–100, default 50 | `nextCursor`, `hasMore` | | `GET /v1/repositories` | `cursor` (opaque string, max 512) | 1–100, default 50 | `nextCursor` (`null` at the end; no `hasMore`) | | `GET …/publications` | `cursor` (opaque string) | 1–100, default 50 | `nextCursor`, `hasMore` | | `GET …/publications/{publicationId}/merges` | `cursor` (opaque string) | 1–100, default 50 | `nextCursor`, `hasMore` | | `GET …/events` | `after` (integer `sequence`, min 0, default 0) | 1–100, default 100 | `nextAfter`, `hasMore` | | `GET …/turns` | `after` (integer `ordinal`, min −1, default −1) | 1–100, default 100 | `nextAfter`, `hasMore` | | `GET …/artifacts` | `after` (artifact id, a UUID) | 1–100, default 100 | `nextAfter`, `hasMore` | | `GET …/interactions` | none | none | none — the full open set | Rules that apply to all of them: - **Cursors are exclusive.** You receive items strictly after the position you send, so passing back the value you were given never repeats an item. - **`nextCursor: null` means the end.** For the integer and id schemes, `hasMore: false` means you have caught up; `nextAfter` then equals the value you sent, which makes an unconditional loop safe. - **The opaque cursor is bound to the collection that minted it.** The keyset collections mint different cursor kinds, so a cursor from `GET /v1/sessions` sent to `GET …/publications` or `GET …/merges` is `400 invalid_request`, as is any cursor you construct or mutate yourself. Store it as a string; do not decode it and do not build one. A foreign or malformed `GET /v1/repositories` cursor is `400 invalid_input`. - **Query parameters are strict on the paged endpoints only**, except `GET /v1/repositories`, which honors `cursor` and `limit` and ignores other query names. Each other endpoint in the table above validates its query strictly, so an unrecognized parameter there is `400 invalid_request`. `limit` accepts a numeric string, so `?limit=50` is fine, but `?limit=0`, `?limit=101`, and `?limit=abc` are each a `400`. Every other endpoint reads no query at all and **silently ignores** anything you attach to the URL: `GET /v1/sessions/{sessionId}` and its `/execution`, `/interactions` and `/repository/review` children, plus `…/artifacts/{artifactId}`, `…/artifacts/{artifactId}/content`, `…/runs/{runId}`, `…/publications/{publicationId}`, and `…/merges/{mergeId}`. Do not rely on a `400` to catch a misspelled parameter. - **`hasMore: true` means the page was capped**, so call again immediately instead of sleeping. ### The session list filters `GET /v1/sessions` is the only list that narrows, and it takes four extra parameters on top of `cursor` and `limit`. | Parameter | Values | Default | |---|---|---| | `archived` | `exclude`, `include`, `only` | **`exclude`** | | `originTenantKey` | 1–128 characters | none | | `originObjectType` | 1–64 characters | none | | `originObjectId` | 1–256 characters | none | The `archived` default is the sharp edge. Archiving does not remove a session — reads still reach it by id — but it does drop out of this list unless you ask for it. Send `archived=include` to page both, or `archived=only` to page just the archived ones. The three `origin*` parameters match exactly, with no prefixes, ranges, or wildcards, and `originTenantKey` is required whenever either of the other two is sent — it is what keeps the lookup indexed. Sending `originObjectType` or `originObjectId` without it is `400 invalid_request`. See [Drive Cloud from a backend](/guides/from-your-backend/) for the mapping this supports. ## Content types and Accept negotiation Requests that carry a body send `Content-Type: application/json`, and bodies are validated strictly: an unknown property is `400 invalid_request` rather than being dropped. The JSON body limit is 1 MB — see [Limits](/reference/limits/). Responses come in three shapes: | Response | Content type | |---|---| | Every `/v1` JSON operation | `application/json` | | `DELETE /v1/sessions/{sessionId}` | none — `204 No Content` with an empty body | | `GET …/artifacts/{artifactId}/content` | the artifact's own recorded content type, with raw bytes | Artifact content is the only endpoint that does not return JSON. It sets `Content-Length`, an `ETag` holding the quoted SHA-256 of the bytes, `Cache-Control: private, no-store`, `Content-Security-Policy: sandbox`, and `X-Content-Type-Options: nosniff`. When the artifact records a file name you also get `Content-Disposition: attachment` with an RFC 5987 encoded `filename*`. Treat the bytes as untrusted content from an agent: the sandbox CSP and `nosniff` are there because some artifacts are HTML. ### The two endpoints that negotiate Only the discovery pair inspects `Accept`, and each accepts a different family: | Endpoint | Accepts | Serves | Otherwise | |---|---|---|---| | `GET /v1/openapi.json` | `application/json` | the OpenAPI 3.1 document | `406 representation_not_acceptable` | | `GET /v1/docs` | `text/markdown` or `text/plain` | `text/markdown; charset=utf-8` | `406 representation_not_acceptable` | Both are unauthenticated and both send `Cache-Control: public, max-age=300`. The failure that costs an afternoon is sending a blanket `Accept: application/json` from a shared HTTP client and calling `/v1/docs` with it: that is a `406`, because the guide is markdown. A default of `*/*` satisfies both. Every other endpoint ignores `Accept` entirely. ## Response conventions - **Every error body is a JSON object with a stable machine `error` code**, and a few codes add a field: `insufficient_scope` adds `requiredScope`, `cloud_rate_limit_exceeded` adds `retryAfterSeconds`, and an approval decision's `cloud_interaction_conflict` adds the winning `outcome`. Match on the code, not on the status alone, and never on a message string. Full table in [Errors](/reference/errors/). A `502`, `503` or `504` with no code came from the proxy in front of the API, not from WAMP; [Errors](/reference/errors/#releases-and-answers-with-no-code) says when to retry it. - **Creates set `Location`** to the canonical path of the resource, including on an idempotent replay: session, turn, publication and merge. For a turn it points at the run. `PATCH`, `DELETE`, the workspace restore and cancellation set no `Location`. - **Timestamps are ISO 8601 UTC strings** (`createdAt`, `updatedAt`, `startedAt`, `completedAt`, `openedAt`, and the rest). Optional timestamps are absent rather than `null` when they have not happened. - **Optional fields are omitted, not nulled**, across the session, turn, run, artifact, publication, and merge resources. Test with `in`/`hasOwnProperty` semantics rather than comparing to `null`. - **Resource ids are UUIDs** — sessions, turns, runs, publications, merges, artifacts, and events. Two identifiers are not: an interaction id is an opaque string of up to 255 characters, and hash fields are hex. `revision` is 64 hex characters (a tree revision), while `commitSha`, `expectedHeadSha`, and `mergedCommitSha` are 40 hex characters (git object ids). Sending one where the other belongs is a `400`. - **Every authenticated `/v1` response carries rate-limit headers.** The two unauthenticated endpoints, `GET /v1/openapi.json` and `GET /v1/docs`, sit outside the limiter and carry none. Read the headers rather than hardcoding a rate; see [Limits](/reference/limits/). ## How to read the generated operation pages The [Operations](/api/operations/tags/sessions/) section is generated from the same OpenAPI document the server publishes at `GET /v1/openapi.json`, so it cannot describe an endpoint the server does not have. Pages are grouped by tag, in the order the contract declares them: | Page | Covers | |---|---| | [Authentication](/api/operations/tags/authentication/) | the token exchange and installation intents, on the Account origin | | [Capabilities](/api/operations/tags/capabilities/) | discovery: models, runtimes, environments, limits, and the runtime-release probe | | [Repository grants](/api/operations/tags/repository-grants/) | the repository picker (`GET /v1/repositories`), grant inspection, and healing a stale grant id | | [Sessions](/api/operations/tags/sessions/) | create, read, list, rename, archive | | [Turns and Runs](/api/operations/tags/turns-and-runs/) | submit a turn, read a run, cancel | | [Transcriptions](/api/operations/tags/transcriptions/) | convert one bounded audio recording to text | | [Interactions](/api/operations/tags/interactions/) | open questions and live approvals; read the exact action, then decide with `wamp.cloud.approvals:respond` to resume the same Run without a reply Turn | | [Events](/api/operations/tags/events/) | the ordered session event log | | [Artifacts](/api/operations/tags/artifacts/) | manifests and content bytes | | [Publications](/api/operations/tags/publications/) | review, open a pull request, merge | | [the API reference](/api/) | every request and response object, field by field | Each operation section gives you, in order: the method and path; a one-line summary; the SDK method that calls it, when there is one; a parameter table; the request body fields with their required flag and enforced constraints; and the response statuses with the body type of each. Two habits make those pages fast to use: - **Follow the type links.** A field typed `Session` or `SessionEvent` links into [the API reference](/api/), where the object is expanded one level with the same required and constraint columns. That appendix is where you look up a shape once and stop guessing at it. - **Read constraints as hard edges.** `length 1–100000`, `pattern ^[a-f0-9]{64}$`, `default 50` are the values the server validates against, not recommendations. A value outside them is a `400 invalid_request`, and the response does not tell you which field failed. ## Known gaps in the contract document The OpenAPI document is maintained by hand rather than generated from the server's routing table. It is complete for the public API — all 33 registered `/v1` operations are described, and it declares no `/v1` path that does not exist — and a test locks its SDK annotations against the SDK's method list, so the two cannot drift. Two limits are worth knowing before you point a code generator at it. **The declared server defaults to the production host, and three operations do not live there.** The single declared server is `{scheme}://{host}` with `host` defaulting to the production Cloud hostname and `scheme` to `https`, so a generated client points at production unless you override both server variables for staging or a self-hosted deployment. Beyond that, the document also describes `POST /auth/app-installation-token`, `POST /api/apps/installation-intents`, and `POST /api/apps/installation-intents/{intentId}/status`, which belong to the Account service. In a split-origin deployment a client generated straight from the document will send those three to the Cloud origin and fail. Send them to the Account origin. ## Related - [Authentication](/start/authentication/) — minting and refreshing the bearer. - [Integrate WAMP Cloud](/start/integrate/) — the shortest working sequence. - [Sessions, turns, runs](/concepts/sessions-turns-runs/) — the resource model these conventions apply to. - [Errors](/reference/errors/) — every status and code, and what to retry. - [Limits](/reference/limits/) — sizes, counts, and rate limits.