Skip to content

Quickstart

By the end of this page you will have run an agent in a managed sandbox from your shell and read what it produced. Every call is real, every field name is the one the server validates, and the response shape is shown at each step.

Read this before the first call, because it shapes every request that follows.

Cloud has no POST /v1/sessions. You mint a UUID and PUT the resource at it:

PUT /v1/sessions/{sessionId}
PUT /v1/sessions/{sessionId}/turns/{turnId}

That single decision is the whole retry story. There is no Idempotency-Key header anywhere in this API because the id is the idempotency key:

You send You get
A new id 201 Created for a session, 202 Accepted for a turn, with a Location header
The same id and the same body 200 for a session, 202 with created: false for a turn — no second run
The same id and a different body 409 — the durable command is immutable, so the conflict is reported rather than silently applied

So when a request times out or a connection drops, you re-send exactly the same request. You never have to ask “did that create a second run?”.

Your operator provides the API origin, the token issuer, and your app’s installation. Getting a keypair registered and an installation approved is covered on Authentication — this page assumes you have finished that and can sign with your private key.

Terminal window
export WAMP_API=https://api.example.com # the Cloud origin
export WAMP_ACCOUNT=https://api.example.com # the Account origin; the same host unless deployed split
export TOKEN_ISSUER='<the platform JWT issuer your operator gives you>'
export WAMP_APP_SLUG='<your app slug>'
export WAMP_APP_KID='<your registered key id>'
export WAMP_INSTALLATION_ID='<the installation a customer approved>'
export WAMP_APP_PRIVATE_KEY="$(cat app-private-key.pem)"

You also need curl, jq, uuidgen, and Node 20 or newer with the jose package available (npm install jose) for the one signing step.

  1. Sign a short assertion with your private key, then exchange it for an installation bearer. The assertion is a JWT: EdDSA, your kid in the header, iss and sub set to your app slug, aud set to the token issuer, and a lifetime of at most 600 seconds.

    Terminal window
    ASSERTION=$(node --input-type=module -e "
    import { SignJWT, importPKCS8 } from 'jose';
    const key = await importPKCS8(process.env.WAMP_APP_PRIVATE_KEY, 'EdDSA');
    const now = Math.floor(Date.now() / 1000);
    process.stdout.write(await new SignJWT({})
    .setProtectedHeader({ alg: 'EdDSA', kid: process.env.WAMP_APP_KID })
    .setIssuer(process.env.WAMP_APP_SLUG)
    .setSubject(process.env.WAMP_APP_SLUG)
    .setAudience(process.env.TOKEN_ISSUER)
    .setIssuedAt(now)
    .setExpirationTime(now + 300)
    .sign(key));
    ")
    curl -sS -X POST "$WAMP_ACCOUNT/auth/app-installation-token" \
    -H 'Content-Type: application/json' \
    -d "$(jq -n --arg a "$ASSERTION" --arg i "$WAMP_INSTALLATION_ID" \
    '{assertion: $a, installationId: $i, resourceAudience: "wamp-cloud"}')"
    { "success": true, "token": "eyJhbGciOiJFZERTQSIsImtpZCI6…", "expiresIn": 600 }

    Keep it in a variable. It lives ten minutes and is the only credential /v1 accepts:

    Terminal window
    export WAMP_TOKEN='<the token value>'
  2. This call doubles as a check that the token works, and it is where you get a valid model id rather than guessing one.

    Terminal window
    curl -sS "$WAMP_API/v1/capabilities" \
    -H "Authorization: Bearer $WAMP_TOKEN"
    {
    "capabilities": {
    "schema": "wamp.cloud-capabilities",
    "version": 1,
    "organizationId": "0f2c9a41-5f1b-4c8e-9a6d-7b3e1c5d9f02",
    "models": { "items": [{ "id": "", "label": "" }], "defaults": { "slots": {}, "enabled": [] } },
    "runtimes": [
    {
    "id": "wamp",
    "label": "WAMP",
    "modelSource": "catalog",
    "continuation": { "live": "exact", "checkpoint": "exact" },
    "availability": { "state": "available" }
    }
    ],
    "environments": [{ "id": "managed", "label": "WAMP managed sandbox", "default": true }],
    "repositories": { "sources": [], "publications": [], "merges": [] },
    "artifacts": { "maxPageSize": 100 },
    "limits": { "maxActiveSessions": 100, "maxMessageCharacters": 100000, "maxPageSize": 100 }
    }
    }

    The document is filtered to what your installation may use, so two callers see different answers. Pick a model id from it:

    Terminal window
    MODEL=$(curl -sS "$WAMP_API/v1/capabilities" \
    -H "Authorization: Bearer $WAMP_TOKEN" | jq -r '.capabilities.models.items[0].id')
  3. The session is the durable conversation and its workspace. Creating one allocates no compute.

    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 "$(jq -n --arg model "$MODEL" '{
    title: "Quickstart",
    task: "Create hello.txt containing the word hello, then summarize what you did.",
    model: $model
    }')"
    HTTP/1.1 201 Created
    Location: /v1/sessions/3f7d4f4c-2b6a-4a2e-9c1a-1f2b3c4d5e6f
    {
    "session": {
    "id": "3f7d4f4c-2b6a-4a2e-9c1a-1f2b3c4d5e6f",
    "sessionUrl": "https://cloud.example.com/sessions/3f7d4f4c-2b6a-4a2e-9c1a-1f2b3c4d5e6f",
    "organizationId": "0f2c9a41-5f1b-4c8e-9a6d-7b3e1c5d9f02",
    "title": "Quickstart",
    "model": "",
    "workspace": { "state": "unavailable" },
    "continuation": {
    "canContinue": true,
    "state": "fresh",
    "conversation": { "fidelity": "none" },
    "workspace": { "fidelity": "none" },
    "environment": { "fidelity": "current" }
    },
    "phase": "idle",
    "createdBy": { "kind": "app_installation", "id": "8a0f1c6b-92d4-4e73-b5a1-0d3e7f2c9b48" },
    "createdAt": "2026-08-11T09:00:00.000Z",
    "updatedAt": "2026-08-11T09:00:00.000Z"
    }
    }

    The body fields, exactly:

    Field Required Constraint
    task yes 1–100000 characters. What this session exists to do
    title no 1–160 characters, for humans
    model conditional 1–128 characters. Required for the native wamp runtime, which is the default. Omit it only when you set a runtime that manages its own model
    runtime no Lowercase slug matching ^[a-z0-9][a-z0-9-]*$, up to 64 characters. Defaults to wamp
    origin no Your back-reference: tenantKey, objectType, objectId, and optionally endUserId, label, url
    source no A repository or public checkout to work in. Omit for a bare workspace

    Unknown fields are rejected with 400 invalid_request rather than ignored. task, origin, and source are immutable — only title, model, and runtime can be changed later with PATCH.

  4. The turn is the message; its run is the execution.

    Terminal window
    TURN_ID=$(uuidgen | tr 'A-Z' 'a-z')
    curl -sS -i -X PUT "$WAMP_API/v1/sessions/$SESSION_ID/turns/$TURN_ID" \
    -H "Authorization: Bearer $WAMP_TOKEN" \
    -H 'Content-Type: application/json' \
    -d '{"message":"Create hello.txt containing the word hello, then summarize what you did."}'
    HTTP/1.1 202 Accepted
    Location: /v1/sessions/3f7d4f4c-2b6a-4a2e-9c1a-1f2b3c4d5e6f/runs/9a8b7c6d-5e4f-4a3b-8c2d-1e0f9a8b7c6d
    {
    "turn": {
    "id": "9a8b7c6d-5e4f-4a3b-8c2d-1e0f9a8b7c6d",
    "sessionId": "3f7d4f4c-2b6a-4a2e-9c1a-1f2b3c4d5e6f",
    "ordinal": 1,
    "input": { "message": "Create hello.txt containing…", "model": "" },
    "dispatch": "pending",
    "run": { "id": "9a8b7c6d-5e4f-4a3b-8c2d-1e0f9a8b7c6d" },
    "createdAt": "2026-08-11T09:00:02.000Z"
    },
    "run": {
    "id": "9a8b7c6d-5e4f-4a3b-8c2d-1e0f9a8b7c6d",
    "sessionId": "3f7d4f4c-2b6a-4a2e-9c1a-1f2b3c4d5e6f",
    "status": "queued",
    "attempts": 0,
    "cancellationRequested": false,
    "createdAt": "2026-08-11T09:00:02.000Z",
    "updatedAt": "2026-08-11T09:00:02.000Z"
    },
    "created": true
    }

    202 is honest: the turn and a queued run are committed, and no sandbox exists yet. run.id equals the turnId you chose. created: false on a replay means you are looking at the turn you already submitted.

    Only one run per session may be non-terminal at a time. Submitting a second turn while one is in flight is 409 cloud_run_in_progress with Retry-After: 2.

  5. There is no push channel — no Server-Sent Events, no public WebSocket. You page an ordered, gap-free log with an integer cursor. after is exclusive and sequences start at 1, so after=0 means “from the beginning”.

    Terminal window
    AFTER=0
    while : ; do
    PAGE=$(curl -sS "$WAMP_API/v1/sessions/$SESSION_ID/events?after=$AFTER&limit=100" \
    -H "Authorization: Bearer $WAMP_TOKEN")
    echo "$PAGE" | jq -c '.events[] | {sequence, type}'
    AFTER=$(echo "$PAGE" | jq -r '.nextAfter')
    STATUS=$(echo "$PAGE" | jq -r --arg run "$TURN_ID" '
    [ .events[]
    | select(.subject.runId == $run)
    | select(.type | test("^wamp\\.run\\.(completed|failed|cancelled|crashed)$"))
    | .data.status ] | first // empty')
    [ -n "$STATUS" ] && break
    [ "$(echo "$PAGE" | jq -r '.hasMore')" = true ] || sleep 2
    done
    echo "run finished: $STATUS"

    A page looks like this:

    {
    "events": [
    {
    "id": "6f1c1d80-6c2a-4f0e-9d5f-0c2a2f0f4b11",
    "type": "wamp.run.started",
    "createdAt": "2026-08-11T09:00:11.412Z",
    "sequence": 1,
    "visibility": "summary",
    "subject": {
    "sessionId": "3f7d4f4c-2b6a-4a2e-9c1a-1f2b3c4d5e6f",
    "runId": "9a8b7c6d-5e4f-4a3b-8c2d-1e0f9a8b7c6d"
    },
    "data": { "status": "running" }
    }
    ],
    "nextAfter": 1,
    "hasMore": false
    }

    Four things about that loop are worth keeping when you write the real one:

    • Persist nextAfter in your own storage. It is the entire resume protocol — there is no Last-Event-ID and no subscription state on the server.
    • hasMore: true means the page was capped at limit, so call again without sleeping. hasMore: false means you have caught up, not that the run is over.
    • Match on subject.runId, or a terminal event from an earlier run in the same session ends your loop early.
    • Ignore event types you do not recognize. The catalog grows additively.

    If the agent needs a decision it emits wamp.interaction.opened and the run moves to awaiting. You answer with a normal turn carrying replyTo.interactionId — see Follow a run live.

  6. The authoritative status does not depend on having seen an event:

    Terminal window
    curl -sS "$WAMP_API/v1/sessions/$SESSION_ID/runs/$TURN_ID" \
    -H "Authorization: Bearer $WAMP_TOKEN"
    {
    "run": {
    "id": "9a8b7c6d-5e4f-4a3b-8c2d-1e0f9a8b7c6d",
    "sessionId": "3f7d4f4c-2b6a-4a2e-9c1a-1f2b3c4d5e6f",
    "status": "completed",
    "attempts": 1,
    "cancellationRequested": false,
    "createdAt": "2026-08-11T09:00:02.000Z",
    "updatedAt": "2026-08-11T09:01:44.000Z",
    "startedAt": "2026-08-11T09:00:11.000Z",
    "completedAt": "2026-08-11T09:01:44.000Z"
    }
    }

    A run has a completedAt exactly when its status is terminal — that equivalence is enforced by a database constraint, so completedAt != null is a sound “is it done” test. The statuses are queued, dispatching, running, awaiting, completed, failed, cancelled, crashed.

    What the run produced is listed as artifacts:

    Terminal window
    curl -sS "$WAMP_API/v1/sessions/$SESSION_ID/artifacts?limit=100" \
    -H "Authorization: Bearer $WAMP_TOKEN"
    {
    "artifacts": [
    {
    "id": "b41d0f2a-8c5e-4d19-9f77-2a6b8c3d4e5f",
    "sessionId": "3f7d4f4c-2b6a-4a2e-9c1a-1f2b3c4d5e6f",
    "runId": "9a8b7c6d-5e4f-4a3b-8c2d-1e0f9a8b7c6d",
    "kind": "result.summary",
    "role": "output",
    "contentType": "text/markdown; charset=utf-8",
    "size": 412,
    "sha256": "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08",
    "state": "available",
    "metadata": { "truncated": false },
    "createdAt": "2026-08-11T09:01:44.000Z"
    }
    ],
    "nextAfter": "b41d0f2a-8c5e-4d19-9f77-2a6b8c3d4e5f",
    "hasMore": false
    }

    Then fetch the bytes. The result.summary artifact is the agent’s own account of what it did:

    Terminal window
    ARTIFACT_ID=$(curl -sS "$WAMP_API/v1/sessions/$SESSION_ID/artifacts?limit=100" \
    -H "Authorization: Bearer $WAMP_TOKEN" \
    | jq -r '[.artifacts[] | select(.kind == "result.summary")] | last | .id')
    curl -sS "$WAMP_API/v1/sessions/$SESSION_ID/artifacts/$ARTIFACT_ID/content" \
    -H "Authorization: Bearer $WAMP_TOKEN"

    The response is the raw bytes with the artifact’s own Content-Type and an ETag equal to its sha256. If retention has already reclaimed the bytes the manifest still lists it with "state": "pruned" and this call is 410.

  7. Terminal window
    curl -sS -i -X DELETE "$WAMP_API/v1/sessions/$SESSION_ID" \
    -H "Authorization: Bearer $WAMP_TOKEN"
    # HTTP/1.1 204 No Content

    Archiving is a soft delete and it is idempotent. The session and its event log remain readable; it stops counting against your organization’s active-session limit.