Open the console

API

Sessions

Conversations as ADK recorded them, in your agent's own database: create one, list them, read one back in the live stream's own shape, and delete it. The lifecycle below is the order the calls go in.

The samples use $API and $TOKEN, which the API overview sets.

The lifecycle

A conversation is addressed by (agent, person, session id) — all three — and it lives where ADK wrote it: your agent's own database. The routes below read it through ADK's own session service, so what you get back is exactly what the agent saw.

Choose a session id or create one with starting state, invoke into it once per turn, and read the conversation back by that same id — from the agent's own database, in the same event shape the live stream uses.the same id, for every later turnChoose an idor create, with stateInvokeonce per turnRead it backevents, from ADKcreates if newsame session id
Choose a session id or create one with starting state, invoke into it once per turn, and read the conversation back by that same id — from the agent's own database, in the same event shape the live stream uses.
  1. Choose a session id — any string you like, sess-ada-1 — or create the conversation with POST /v1/projects/{project}/sessions when the agent should start with state already in place.
  2. Invoke with it. POST /v1/invocations carrying session_id, and end_user_id if the run belongs to one of your users. An unknown id is created on the spot. state_delta seeds conversation state on that one turn instead.
  3. Invoke again with the same id for the next turn. That is the whole of “continuing a conversation” — the agent is given the history automatically, out of its own database.
  4. List the conversations one person has had with one agent, newest first: GET /v1/projects/{project}/sessions?agent=….
  5. Read one back with GET …/{agent}/{session_id}/events — every message, tool call, pause and MCP Apps view, as the frames the event stream sends.
  6. Delete it when it should be gone. The runs it held stay billed and recorded.

Whose conversation: yours, or one of your users'

Every route acts for the caller unless it is given user — your own id for one of your end users, the same value you pass as end_user_id when invoking — and naming one needs a tenant admin. A conversation you create as yourself and then invoke into for a customer is two different conversations: pass the same person to both.

A 409 that says “redeploy” means exactly that

These routes read each agent's database as a narrow role that only ADK's session tables are granted to, and that role is created when the agent is deployed. An agent deployed before it existed answers 409 on every route here until it is redeployed once — nothing about the agent needs to change, and its existing conversations are all there afterwards.

Which call answers which question

You wantCallWhy
what was said in a conversationGET …/sessions/{agent}/{id}/eventsADK's own record, in the live stream's shape
someone's conversations with one agentGET …/sessions?agent=…newest first; keys of each one's state, not the values
a workflow's steps and pauseGET …/sessions/{workflow}/{id}/runa graph's shape, which a transcript cannot show
how one run ended, and what it costGET /v1/runs/{invocation_id}the record of the run, which ADK's record of the conversation does not hold
what is waiting on youGET /v1/runs?status=pausedthe only call that sees both kinds of pause — a tool confirmation and a workflow asking for input

The runs routes are on the invocations API.

Reopen a chat from its events, not from per-run reads

The events route returns a conversation in the order ADK recorded it, across every run. A per-run read returns each run's events as a block, so a run that paused for an afternoon's approval comes back positioned where it started. And because the events are the live stream's own frames, the fold you already wrote for the socket renders history unchanged.

A paused run has no terminal event

Live, a pause is where the run stopped deliberately and no finished is coming — read the interrupt id instead of waiting. Read back, the same is true: the run ends at a paused event carrying interrupt_kind. The sessions concept covers it, and autonomy is how you answer it.

Every call is metered

A read — listing, a conversation's state, its events, a workflow's run — counts as one sessions.ops.read; creating or deleting one counts as one sessions.ops.write. Counted after the call succeeded, so a refusal costs nothing. Page a long conversation with recent rather than polling it whole.

The endpoints

Start a conversation

POST/v1/projects/{project}/sessionsdeploy-service

ADK's own create_session, in the agent's own database: an id of your choosing or one ADK mints, and the state the conversation starts with.

You do not have to call this

Invoking with a session id that does not exist creates the conversation, so a backend can choose its own id — sess-ada-1 — and never call this. What it adds is STARTING STATE the agent can read on its very first turn, or an id ADK minted for you.

An id already in use is a 409, never a reuse

Creating over an existing conversation would hand somebody's history your starting state. The route looks first and refuses, naming the id.

Conversation-scoped keys only, and the refusal names the key

ADK scopes state by prefix. app: is shared by every user of the agent, user: by every conversation of that user, temp: is never persisted, and agentkit: is the platform's own — so each is refused with a 400 naming it. A key with no prefix belongs to this conversation alone. 64 KiB serialized at most: a file belongs in an artifact, not in state, because state is replayed into every turn.

user makes it one of your customers' — and must match the invoke

Without it the conversation belongs to your own credential. A backend that creates one as itself and then invokes with end_user_id has made two different addresses, and the agent never sees the state you seeded. Pass the same person here as user and the invocation lands in this conversation.

Request
curl -X POST "$API/v1/projects/salmon/sessions" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "agent": "hotel-concierge",
    "session_id": "sess-ada-1",
    "user": "ada",
    "title": "Late checkout for room 412",
    "state": {"plan": "pro", "locale": "en-GB"}
  }'

Body

FieldTypeDescription
agentstringrequiredThe agent or workflow the conversation is with, by slug.
session_idstringoptionalYour own id, up to 128 characters. Omitted, ADK mints one and the response carries it.
stateobjectoptionalStarting state. Keys without a prefix only; at most 64 KiB serialized.
titlestringoptionalWhat a list of conversations shows for this one — typically its first message. Kept on one line and cut to 120 characters. Stored with the conversation; the agent never sees it, and it is not in state.
userstringoptionalOne of YOUR end users, by your own id (ada or eu:ada). Needs a tenant admin.

Responses

StatusMeaning
201Created.
400A refused or oversized state key, named — or A user that is * or contains : or / after its optional eu:.
403user without a tenant admin, or a project outside the credential's own scope.
404No such agent deployed in this project.
409That session id is already in use. Or: The agent was deployed before conversation history could be read. Redeploy it once — nothing about it needs to change.
422An unknown body field. The body is closed, so a typo is refused rather than dropped.
503The agent's database could not be reached just now. Retry.

Response body

201 {
  "id": "sess-ada-1",
  "user_id": "eu:ada",
  "title": "Late checkout for room 412",
  "last_update_time": 1762502041.18,
  "state": { "plan": "pro", "locale": "en-GB" }
}

List conversations

GET/v1/projects/{project}/sessionsdeploy-service

Every conversation one person has had with one agent, newest first.

Per agent, and agent is required

A conversation lives in its agent's own database, so there is no listing across agents to ask for — one call per agent you care about.

Whose: yours, or one of your users'

Without user this lists the CALLER's conversations — and a service credential's own list is usually empty, because a backend's conversations belong to the customers it invoked for. user=ada lists theirs, and needs a tenant admin.

state_keys, not state

A page of conversations does not ship every one's state. The listing names the keys; reading one conversation returns the values.

Request
curl "$API/v1/projects/salmon/sessions?agent=hotel-concierge&user=ada" \
  -H "Authorization: Bearer $TOKEN"

Parameters

FieldTypeDescription
agentqueryrequiredThe agent or workflow, by slug.
userqueryoptionalOne of your end users, by your own id. Needs a tenant admin. Omitted, the caller's own.

Responses

StatusMeaning
200The conversations, newest first. Empty is an answer, not an error.
400A user that is * or contains : or / after its optional eu:.
403user without a tenant admin, or a project outside the credential's own scope.
404No such agent deployed in this project.
409The agent was deployed before conversation history could be read. Redeploy it once — nothing about it needs to change.
503The agent's database could not be reached just now. Retry.

Response body

{
  "sessions": [
    {
      "id": "sess-ada-1",
      "user_id": "eu:ada",
      "title": "Late checkout for room 412",
      "last_update_time": 1762502388.42,
      "state_keys": ["locale", "plan"]
    }
  ]
}

Read a conversation's state

GET/v1/projects/{project}/sessions/{agent}/{session_id}deploy-service

One conversation's id, owner, last update and state — without its events. The events route reads what was said.

State as ADK hands it to the agent

Including the app: and user: values ADK merges into every session it loads — readable here, never writable through this API. The platform's own agentkit: records are left out: a stored MCP Apps view is a whole HTML document, and it comes back as a surface event from the events route instead.

Request
curl "$API/v1/projects/salmon/sessions/hotel-concierge/sess-ada-1?user=ada" \
  -H "Authorization: Bearer $TOKEN"

Parameters

FieldTypeDescription
userqueryoptionalOne of your end users, by your own id. Needs a tenant admin.

Responses

StatusMeaning
200The conversation.
400A user that is * or contains : or / after its optional eu:.
403user without a tenant admin, or a project outside the credential's own scope.
404No such agent in this project, or no such conversation for this person.
409The agent was deployed before conversation history could be read. Redeploy it once — nothing about it needs to change.
503The agent's database could not be reached just now. Retry.

Response body

{
  "id": "sess-ada-1",
  "user_id": "eu:ada",
  "last_update_time": 1762502388.42,
  "state": { "plan": "pro", "locale": "en-GB" }
}

Read a conversation back

GET/v1/projects/{project}/sessions/{agent}/{session_id}/eventsdeploy-service

Everything said in a conversation, as the same event frames the live stream sends — so one fold renders history and a live run alike.

THE SAME SHAPE AS /ws, ON PURPOSE

Each stored ADK event is classified by the same function the engine classifies a live one with, and its payload is the same dump the engine publishes. A person's own message is a started event carrying payload.text. A client that folds frames into turns needs one fold, not two that agree today and drift the day one is edited.

Each run ends the way it ends live

Terminal events are synthesized, one per run: paused — carrying interrupt_id, interrupt_kind and message — when the run is still waiting, and finished otherwise. So ends_the_run (endsTheRun in Node) ends each turn here exactly as on the socket. A pause that was answered stays in the history as ADK's own paused event, which carries no interrupt_kind, so the same rule reads past it.

MCP Apps views come back where they appeared

A view an agent showed is stored in session state when it is shown and returns here as a surface event at that point in the timeline, so a reopened conversation re-renders it. A view shown before the agent was redeployed onto that runtime was never stored.

Paging: recent opens a long one, after catches up

recent=N is the newest N ADK events — counted BEFORE the synthesized frames are added, so a page carries a few more than N, and its oldest run may begin mid-way without its started. after is seconds since the epoch and is INCLUSIVE (ADK filters >=), so catching up from the newest event you hold returns that event again. seq is numbered per response and cannot dedupe across two reads; compare emitted_at instead. There is no before: reading further back means reading without recent.

What history cannot show

queued, typing and action_requested are the platform's own parallel stream, not ADK events, so they were never stored. segment and target_version are empty. And this is ADK's record of the CONVERSATION, not of each run's fate: a run the platform stopped around ADK — a refused payment, a refused option — may read here as finished, or be absent. GET /v1/runs/{invocation_id} is the record of how a run ended and what it cost.

Request
curl "$API/v1/projects/salmon/sessions/hotel-concierge/sess-ada-1/events?user=ada&recent=200" \
  -H "Authorization: Bearer $TOKEN"

Parameters

FieldTypeDescription
userqueryoptionalOne of your end users, by your own id. Needs a tenant admin.
recentqueryoptionalThe newest N stored events, 1 to 10000.
afterqueryoptionalSeconds since the epoch. Events at or after that moment.

Responses

StatusMeaning
200The events, oldest first.
400A user that is * or contains : or / after its optional eu:.
403user without a tenant admin, or a project outside the credential's own scope.
404No such agent in this project, or no such conversation for this person.
409The agent was deployed before conversation history could be read. Redeploy it once — nothing about it needs to change.
422recent outside 1 to 10000, or a parameter that is not a number.
503The agent's database could not be reached just now. Retry.

Response body

{
  "events": [
    {
      "invocation_id": "e-4f1c…",
      "session_id": "sess-ada-1",
      "user_id": "eu:ada",
      "project_id": "salmon",
      "target_slug": "hotel-concierge",
      "target_kind": "agent",
      "seq": 1,
      "kind": "started",
      "payload": { "text": "Is late checkout available?" },
      "emitted_at": "2026-11-07T09:14:01Z"
    },
    { "seq": 2, "kind": "content", "payload": { "content": { "parts": [ … ] } } },
    { "seq": 3, "kind": "finished", "payload": {} }
  ]
}

Read a workflow's run state

GET/v1/projects/{project}/sessions/{agent}/{session_id}/rundeploy-service

One workflow run: the inputs it launched with, the steps it has completed, and what it is paused on.

For workflows, not conversations

A graph's progress is a shape a transcript cannot show — which nodes ran, in what order, and which one is waiting. An agent has no steps and this tells you nothing useful about one.

Order from the events, status from ADK's checkpoint

The checkpoint is a jsonb column, and jsonb re-sorts object keys, so it cannot say what ran first. Steps are deduped on node_info.path, because a replayed node gets a fresh event id. This is the projection rt-service always made, now one function shared by both services.

Request
curl "$API/v1/projects/salmon/sessions/event-planner/$SESSION_ID/run" \
  -H "Authorization: Bearer $TOKEN"

Parameters

FieldTypeDescription
userqueryoptionalWhose run. One of your end users, by your own id; needs a tenant admin.

Responses

StatusMeaning
200The run, its steps and its pause if it has one.
400A user that is * or contains : or / after its optional eu:.
403user without a tenant admin, or a project outside the credential's own scope.
404No such workflow in this project, or no such run for this person.
409The agent was deployed before conversation history could be read. Redeploy it once — nothing about it needs to change.
503The agent's database could not be reached just now. Retry.

Response body

{
  "status": "paused",
  "invocation_id": "e-9a3f…",
  "inputs": { "city": "Lisbon", "headcount": 40 },
  "launched_at": 1762502041.18,
  "steps": [
    { "node": "venue_scout", "path": "event_planner@1/venue_scout@1",
      "status": "done", "text": "…", "output": { … }, "updated_at": 1762502052.7 }
  ],
  "pause": {
    "invocation_id": "e-9a3f…", "interrupt_id": "int-77",
    "message": "Which venue?", "resolved": false, "kind": "input"
  }
}

Delete a conversation

DELETE/v1/projects/{project}/sessions/{agent}/{session_id}deploy-service

ADK's own delete_session: the conversation and every event in it. There is no undo and no archive.

The runs survive it

What goes is the conversation. The invocations it contained remain in the run history and the warehouse, because they were billed and audited — deleting a conversation is not a way to unbill a run.

Erasure on a customer's request is user=

A tenant admin can delete one of their users' conversations by naming them. Without user, only the caller's own.

Request
curl -X DELETE "$API/v1/projects/salmon/sessions/hotel-concierge/sess-ada-1?user=ada" \
  -H "Authorization: Bearer $TOKEN"

Parameters

FieldTypeDescription
userqueryoptionalOne of your end users, by your own id. Needs a tenant admin.

Responses

StatusMeaning
204Gone.
400A user that is * or contains : or / after its optional eu:.
403user without a tenant admin, or a project outside the credential's own scope.
404No such agent in this project, or no such conversation for this person.
409The agent was deployed before conversation history could be read. Redeploy it once — nothing about it needs to change.
503The agent's database could not be reached just now. Retry.

Response body

204 — no body. There is deliberately nothing to read back.

Reading this as a model? llms.txt indexes every page, and each page has a “Copy for AI” control.