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.

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 this reference says it is one. session.phase is the case that catches people: it is advisory display text over an open domain. Branch on run.status and on the event stream instead.

Every /v1 request carries a bearer token:

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

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:

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

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.

  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.

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: 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. A cursor from GET /v1/sessions sent to GET …/publications 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.
  • Query parameters are strict. An unrecognized parameter is 400 invalid_request, not ignored. limit accepts a numeric string, so ?limit=50 is fine, but ?limit=0, ?limit=101, and ?limit=abc are each a 400.
  • hasMore: true means the page was capped, so call again immediately instead of sleeping.

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 two codes add a field: insufficient_scope adds requiredScope, and cloud_rate_limit_exceeded adds retryAfterSeconds. Match on the code, not on the status alone, and never on a message string. Full table in Errors.
  • Successful writes set Location to the canonical path of the resource, including on an idempotent replay. For a turn, Location points at the run.
  • 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 /v1 response carries rate-limit headers. Read them 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
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 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 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.