Skip to content

API conventions

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, with every object shape in the API reference.

Organization 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.

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:

Terminal window
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. 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.

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:

{
"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.

Every /v1 request carries a bearer token:

Terminal window
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 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.

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:

Terminal window
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\":\"<model-id>\"}"
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.

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.

  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.

A turn and the run that executes it are one durable pair that shares one identifier. The turnId you mint is the runId.

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.

GET /v1/sessions/{sessionId}/execution reports the active run and lease fence for a session, and nothing else — no content, no transcript:

{
"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}.

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.

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 for the mapping this supports.

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.

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.

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.

  • 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. A 502, 503 or 504 with no code came from the proxy in front of the API, not from WAMP; Errors 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.

The Operations 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 the token exchange and installation intents, on the Account origin
Capabilities discovery: models, runtimes, environments, limits, and the runtime-release probe
Repository grants the repository picker (GET /v1/repositories), grant inspection, and healing a stale grant id
Sessions create, read, list, rename, archive
Turns and Runs submit a turn, read a run, cancel
Transcriptions convert one bounded audio recording to text
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 the ordered session event log
Artifacts manifests and content bytes
Publications review, open a pull request, merge
the API reference 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, 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.

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.