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.
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 this reference says it
is one.
session.phaseis the case that catches people: it is advisory display text over an open domain. Branch onrun.statusand on the event stream instead.
One authentication scheme
Section titled “One authentication scheme”Every /v1 request carries a bearer token:
curl -sS "$WAMP_API/v1/capabilities" -H "Authorization: Bearer $WAMP_TOKEN"The contract names this scheme cloudInstallationBearer, and it is the only
one it declares. 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 token is minted for the audience wamp-cloud. A token for any other resource fails introspection as 401 invalid_or_expired_credential. |
| Short-lived | An installation token lives 600 seconds. Mint on demand and cache until shortly before expiry; do not persist one for hours. |
| 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. |
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 mutating operation
except cancellation is a PUT to an identifier you mint, and that identifier
is the idempotency mechanism. There is no Idempotency-Key header anywhere in
this API, and you do not need one.
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')
curl -sS -i -X PUT "$WAMP_API/v1/sessions/$SESSION_ID" \ -H "Authorization: Bearer $WAMP_TOKEN" \ -H 'Content-Type: application/json' \ -d '{"task":"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 |
POST …/runs/{runId}/cancel |
200 |
200 |
no body to change |
PATCH /v1/sessions/{sessionId} |
200 |
last write wins | last write wins |
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, 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 task, model, the settled runtime,
origin, and source. title is not part of it — replaying with a
different title returns 200 and the stored title is unchanged. Use PATCH to
rename.
For a turn, the identity is message, replyTo, and any model or runtime
you sent explicitly. Fields you omitted stay wildcards, so omitting model on
the replay of a turn that inherited the session’s model still matches.
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.
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 …/publications |
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. A cursor from
GET /v1/sessionssent toGET …/publicationsis400 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. - Query parameters are strict. An unrecognized parameter is
400 invalid_request, not ignored.limitaccepts a numeric string, so?limit=50is fine, but?limit=0,?limit=101, and?limit=abcare each a400. hasMore: truemeans the page was capped, so call again immediately instead of sleeping.
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 two codes add a field:insufficient_scopeaddsrequiredScope, andcloud_rate_limit_exceededaddsretryAfterSeconds. Match on the code, not on the status alone, and never on a message string. Full table in Errors. - Successful writes set
Locationto the canonical path of the resource, including on an idempotent replay. For a turn,Locationpoints at the run. - 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
/v1response carries rate-limit headers. Read them 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 |
| Repository grants | discovering repositories an administrator granted, and healing a stale grant id |
| Sessions | create, read, list, rename, archive |
| Turns and Runs | submit a turn, read a run, cancel |
| Interactions | the open questions a run is blocked on |
| 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 25 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. Three limits are worth knowing before you point a code
generator at it.
The declared server is the wrong origin for three operations. The document
declares a single server, /, meaning “the origin serving this document” — the
Cloud origin. But it 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.
The token request needs one more field than the document lists. The
declared body for POST /auth/app-installation-token requires assertion and
installationId. The server also requires resourceAudience — the exact
resource the token is for, wamp-cloud for this API — and rejects a body
without it. Follow Authentication for the request
that works.
Webhooks are not in the document. OpenAPI 3.1 can describe outbound callbacks, and this document declares none, so the payload you receive and the signature you verify are specified here instead: Events for the payload and Receive webhooks for delivery, signing, and registration.
Related
Section titled “Related”- Authentication — minting and refreshing the bearer.
- Quickstart — 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.