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 — any string you like,
sess-ada-1— or create the conversation withPOST /v1/projects/{project}/sessionswhen the agent should start with state already in place. - Invoke with it.
POST /v1/invocationscarryingsession_id, andend_user_idif the run belongs to one of your users. An unknown id is created on the spot.state_deltaseeds conversation state on that one turn instead. - 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.
- List the conversations one person has had with one agent, newest first:
GET /v1/projects/{project}/sessions?agent=…. - 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. - 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 want | Call | Why |
|---|---|---|
| what was said in a conversation | GET …/sessions/{agent}/{id}/events | ADK's own record, in the live stream's shape |
| someone's conversations with one agent | GET …/sessions?agent=… | newest first; keys of each one's state, not the values |
| a workflow's steps and pause | GET …/sessions/{workflow}/{id}/run | a graph's shape, which a transcript cannot show |
| how one run ended, and what it cost | GET /v1/runs/{invocation_id} | the record of the run, which ADK's record of the conversation does not hold |
| what is waiting on you | GET /v1/runs?status=paused | the 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.
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
| Field | Type | Description | |
|---|---|---|---|
| agent | string | required | The agent or workflow the conversation is with, by slug. |
| session_id | string | optional | Your own id, up to 128 characters. Omitted, ADK mints one and the response carries it. |
| state | object | optional | Starting state. Keys without a prefix only; at most 64 KiB serialized. |
| title | string | optional | What 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. |
| user | string | optional | One of YOUR end users, by your own id (ada or eu:ada). Needs a tenant admin. |
Responses
| Status | Meaning |
|---|---|
| 201 | Created. |
| 400 | A refused or oversized state key, named — or A user that is * or contains : or / after its optional eu:. |
| 403 | user without a tenant admin, or a project outside the credential's own scope. |
| 404 | No such agent deployed in this project. |
| 409 | That 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. |
| 422 | An unknown body field. The body is closed, so a typo is refused rather than dropped. |
| 503 | The 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.
curl "$API/v1/projects/salmon/sessions?agent=hotel-concierge&user=ada" \ -H "Authorization: Bearer $TOKEN"
Parameters
| Field | Type | Description | |
|---|---|---|---|
| agent | query | required | The agent or workflow, by slug. |
| user | query | optional | One of your end users, by your own id. Needs a tenant admin. Omitted, the caller's own. |
Responses
| Status | Meaning |
|---|---|
| 200 | The conversations, newest first. Empty is an answer, not an error. |
| 400 | A user that is * or contains : or / after its optional eu:. |
| 403 | user without a tenant admin, or a project outside the credential's own scope. |
| 404 | No such agent deployed in this project. |
| 409 | The agent was deployed before conversation history could be read. Redeploy it once — nothing about it needs to change. |
| 503 | The 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.
curl "$API/v1/projects/salmon/sessions/hotel-concierge/sess-ada-1?user=ada" \ -H "Authorization: Bearer $TOKEN"
Parameters
| Field | Type | Description | |
|---|---|---|---|
| user | query | optional | One of your end users, by your own id. Needs a tenant admin. |
Responses
| Status | Meaning |
|---|---|
| 200 | The conversation. |
| 400 | A user that is * or contains : or / after its optional eu:. |
| 403 | user without a tenant admin, or a project outside the credential's own scope. |
| 404 | No such agent in this project, or no such conversation for this person. |
| 409 | The agent was deployed before conversation history could be read. Redeploy it once — nothing about it needs to change. |
| 503 | The 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.
curl "$API/v1/projects/salmon/sessions/hotel-concierge/sess-ada-1/events?user=ada&recent=200" \ -H "Authorization: Bearer $TOKEN"
Parameters
| Field | Type | Description | |
|---|---|---|---|
| user | query | optional | One of your end users, by your own id. Needs a tenant admin. |
| recent | query | optional | The newest N stored events, 1 to 10000. |
| after | query | optional | Seconds since the epoch. Events at or after that moment. |
Responses
| Status | Meaning |
|---|---|
| 200 | The events, oldest first. |
| 400 | A user that is * or contains : or / after its optional eu:. |
| 403 | user without a tenant admin, or a project outside the credential's own scope. |
| 404 | No such agent in this project, or no such conversation for this person. |
| 409 | The agent was deployed before conversation history could be read. Redeploy it once — nothing about it needs to change. |
| 422 | recent outside 1 to 10000, or a parameter that is not a number. |
| 503 | The 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.
curl "$API/v1/projects/salmon/sessions/event-planner/$SESSION_ID/run" \ -H "Authorization: Bearer $TOKEN"
Parameters
| Field | Type | Description | |
|---|---|---|---|
| user | query | optional | Whose run. One of your end users, by your own id; needs a tenant admin. |
Responses
| Status | Meaning |
|---|---|
| 200 | The run, its steps and its pause if it has one. |
| 400 | A user that is * or contains : or / after its optional eu:. |
| 403 | user without a tenant admin, or a project outside the credential's own scope. |
| 404 | No such workflow in this project, or no such run for this person. |
| 409 | The agent was deployed before conversation history could be read. Redeploy it once — nothing about it needs to change. |
| 503 | The 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.
curl -X DELETE "$API/v1/projects/salmon/sessions/hotel-concierge/sess-ada-1?user=ada" \ -H "Authorization: Bearer $TOKEN"
Parameters
| Field | Type | Description | |
|---|---|---|---|
| user | query | optional | One of your end users, by your own id. Needs a tenant admin. |
Responses
| Status | Meaning |
|---|---|
| 204 | Gone. |
| 400 | A user that is * or contains : or / after its optional eu:. |
| 403 | user without a tenant admin, or a project outside the credential's own scope. |
| 404 | No such agent in this project, or no such conversation for this person. |
| 409 | The agent was deployed before conversation history could be read. Redeploy it once — nothing about it needs to change. |
| 503 | The agent's database could not be reached just now. Retry. |
Response body
204 — no body. There is deliberately nothing to read back.