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.
Base URL and versioning
Section titled “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:
export WAMP_API=https://api.example.com # the Cloud originexport WAMP_ACCOUNT=https://accounts.example.com # the Account originTwo 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
typeas a string whose unknown future values must be ignored safely, and both the eventdataobject 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.reasonandsession.phaseall carry a declaredenumand are safe to switch on exhaustively, while an eventtypeand an error body’serrorare open and additive.session.phaseused 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 onrun.statusand on the event stream and renderphase.
Which build is answering you
Section titled “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:
{ "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-releaseand the request id.
One header scheme, two credentials
Section titled “One header scheme, two credentials”Every /v1 request carries a bearer token:
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.
Caller-owned ids and idempotent PUT
Section titled “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:
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 CreatedLocation: /v1/sessions/3f7d4f4c-2b6a-4a2e-9c1a-1f2b3c4d5e6fSend 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”
Section titled “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
Section titled “How to use this in a client”- 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.
- On a timeout, connection reset, or
502, repeat the identical request. The worst case is a200telling you it already happened. - Treat
409 …_conflictas 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.
Turn id is the run id
Section titled “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.
PUT /v1/sessions/{sessionId}/turns/{turnId}→ 202 AcceptedLocation: /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
Section titled “Inspecting execution”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}.
Pagination
Section titled “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: nullmeans the end. For the integer and id schemes,hasMore: falsemeans you have caught up;nextAfterthen 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/sessionssent toGET …/publicationsorGET …/mergesis400 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 malformedGET /v1/repositoriescursor is400 invalid_input. - Query parameters are strict on the paged endpoints only, except
GET /v1/repositories, which honorscursorandlimitand ignores other query names. Each other endpoint in the table above validates its query strictly, so an unrecognized parameter there is400 invalid_request.limitaccepts a numeric string, so?limit=50is fine, but?limit=0,?limit=101, and?limit=abcare each a400. Every other endpoint reads no query at all and silently ignores anything you attach to the URL:GET /v1/sessions/{sessionId}and its/execution,/interactionsand/repository/reviewchildren, plus…/artifacts/{artifactId},…/artifacts/{artifactId}/content,…/runs/{runId},…/publications/{publicationId}, and…/merges/{mergeId}. Do not rely on a400to catch a misspelled parameter. hasMore: truemeans the page was capped, so call again immediately instead of sleeping.
The session list filters
Section titled “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 for the mapping this
supports.
Content types and Accept negotiation
Section titled “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.
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
Section titled “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
Section titled “Response conventions”- Every error body is a JSON object with a stable machine
errorcode, and a few codes add a field:insufficient_scopeaddsrequiredScope,cloud_rate_limit_exceededaddsretryAfterSeconds, and an approval decision’scloud_interaction_conflictadds the winningoutcome. Match on the code, not on the status alone, and never on a message string. Full table in Errors. A502,503or504with no code came from the proxy in front of the API, not from WAMP; Errors says when to retry it. - Creates set
Locationto 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 noLocation. - Timestamps are ISO 8601 UTC strings (
createdAt,updatedAt,startedAt,completedAt,openedAt, and the rest). Optional timestamps are absent rather thannullwhen they have not happened. - Optional fields are omitted, not nulled, across the session, turn, run,
artifact, publication, and merge resources. Test with
in/hasOwnPropertysemantics rather than comparing tonull. - 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.
revisionis 64 hex characters (a tree revision), whilecommitSha,expectedHeadSha, andmergedCommitShaare 40 hex characters (git object ids). Sending one where the other belongs is a400. - Every authenticated
/v1response carries rate-limit headers. The two unauthenticated endpoints,GET /v1/openapi.jsonandGET /v1/docs, sit outside the limiter and carry none. Read the headers rather than hardcoding a rate; see Limits.
How to read the generated operation pages
Section titled “How to read the generated operation pages”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
SessionorSessionEventlinks 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 50are the values the server validates against, not recommendations. A value outside them is a400 invalid_request, and the response does not tell you which field failed.
Known gaps in the contract document
Section titled “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
Section titled “Related”- Authentication — minting and refreshing the bearer.
- Integrate WAMP Cloud — the shortest working sequence.
- Sessions, turns, runs — the resource model these conventions apply to.
- Errors — every status and code, and what to retry.
- Limits — sizes, counts, and rate limits.