---
title: "HTTP API"
description: "Call Guuey over HTTP: authenticate with an API key, read your app's sessions, threads and transcripts, and read or delete an end user's own conversation."
---

Guuey's HTTP API is what the CLI and the chat SDKs call, and parts of the console. This page covers the routes you can call yourself: reading what happened in your app (sessions, threads, transcripts) from your own backend, and reading or deleting an end user's own conversation from your frontend.

The base URL for every route on this page is:

```
https://api.us-east-1.guuey.com/v1
```

There are two kinds of caller, and they authenticate differently:

| Caller                                        | Routes                          | Credential                                                     |
| --------------------------------------------- | ------------------------------- | -------------------------------------------------------------- |
| You, the builder (your backend, a script, CI) | `/v1/apps/…`                    | An API key in `Authorization: Bearer`                          |
| Your end user (their browser or app)          | `/v1/threads/…`, `/v1/agents/…` | The end user's own identity: a guest secret or a sign-in token |

To run a conversation, use the SDK rather than raw HTTP. See [Running a turn](#running-a-turn).

## Authenticate as the builder

Send one header on every builder route:

```http
Authorization: Bearer <key>
```

The key's prefix tells Guuey what kind of key it is:

| Prefix        | What it is                                                                                                                       | Where you get it                                                                                                                               |
| ------------- | -------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- |
| `guuey_user_` | A personal access token. It acts as you, on every app you can reach.                                                             | The console's **API keys** page, or `guuey login` (see the [CLI guide](/cli/))                                                                 |
| `guuey_wkz_`  | A workspace API key. It acts as the workspace, with no person behind it, on the workspace's own apps.                            | The workspace's **API Keys** page (see [Workspace-owned apps](/agents-as-code/#workspace-owned-apps-the-ci-credential-is-a-workspace-api-key)) |
| `guuey_svc_`  | An app service token. It can only deploy its own app: the reconcile and deployment routes in [Agents as code](/agents-as-code/). | `guuey tokens create --label "<what uses it>" --app-id <appId>` (see the [CLI guide](/cli/#deploying-from-ci-service-tokens))                  |

A key is shown in full once, when it is created. Guuey stores only a hash of it, so copy it then. If you lose it, create a new one.

A personal access token carries a scope. A key you create yourself, in the console or with `guuey login`, has the `manage` scope and can call every route your account can. A read-only key (the `read` scope, granted to an app you connect through OAuth) can only make read requests: reads of your own data that return no secret values, plus one read-shaped `POST`, the probe that checks an MCP server address before you add it. Reading an app's environment values, for example, needs `manage`. Every other route, including one Guuey doesn't recognize, answers it with `403`.

A missing, wrong, revoked or expired key all get the same answer, `401` with the code `UNAUTHENTICATED`. A key with an unknown `guuey_` prefix gets `401` too, and the message lists the prefixes Guuey accepts.

## Read your app's sessions

A session is one visitor's run on your agent's pod: when it started, its last activity, whether it ended in an error, and the thread it belongs to.

```http
GET /v1/apps/{appId}/sessions?limit=200
Authorization: Bearer <key>
```

```json
{
  "sessions": [
    {
      "id": "…",
      "appId": "…",
      "sessionStatus": "completed",
      "hasError": false,
      "createdAt": "2026-11-06T09:14:03.120Z",
      "lastActivityAt": "2026-11-06T09:16:40.002Z",
      "threadId": "…"
    }
  ],
  "truncated": false
}
```

- Sessions come newest first. `limit` is optional and defaults to `200`. A value above `2000` reads as `2000`. A value below `1`, or one that is not a whole number, is a `400`.
- `truncated: true` means more sessions exist than this page holds. There is no cursor: ask for a larger `limit`, up to `2000`.
- `sessionStatus` is `active`, `completed`, `expired`, or `null`. `hasError` turns `true` once a turn in the session has failed, and stays `true`, even after later turns succeed. `createdAt` and `lastActivityAt` can be `null`, and `threadId` is `null` until the session is linked to its thread.

Read one session by its id:

```http
GET /v1/apps/{appId}/sessions/{sessionId}
Authorization: Bearer <key>
```

It answers with one session object, the same shape as a row of the list. A session id from another app gets the same `404` as one that does not exist.

Sessions can carry other fields. Ignore any field you don't know: responses can gain fields.

## Read your app's conversations

A thread is one conversation between a visitor and your agent. These two routes list your app's threads and read one thread's transcript.

```http
GET /v1/apps/{appId}/threads?limit=50&cursor=<nextCursor>
Authorization: Bearer <key>
```

```json
{
  "appId": "…",
  "threads": [
    {
      "threadId": "…",
      "userId": "g_…",
      "userKind": "guest",
      "title": "…",
      "status": "active",
      "lastMessageAt": "2026-11-06T09:16:39.871Z",
      "lastMessagePreview": "…",
      "createdAt": "2026-11-06T09:14:03.120Z"
    }
  ],
  "nextCursor": "…"
}
```

- Threads come newest first, by their last message. `limit` is optional, from `1` to `100`, and defaults to `50`. Any other value is a `400`.
- When `nextCursor` is not `null`, pass it back as `cursor` for the next page. A cursor from another app is a `400`.
- `userId` is a handle for the person on the other side, stable across their threads in your app. A guest's handle starts with `g_`, and a handle for a user from your own sign-in starts with `byo_`. Guuey derives both by hashing, so neither carries the person's email or name. `userKind` says what kind of person it is: `guest` (an anonymous visitor), `byo` (signed in through your own sign-in) or `account` (signed in with a Guuey account).

```http
GET /v1/apps/{appId}/threads/{threadId}/messages?limit=200&cursor=<nextCursor>
Authorization: Bearer <key>
```

```json
{
  "appId": "…",
  "threadId": "…",
  "messages": [
    {
      "seq": 0,
      "at": "2026-11-06T09:14:03.120Z",
      "kind": "text",
      "authorRole": "user",
      "text": "Do you ship to Canada?",
      "toolName": null
    }
  ],
  "nextCursor": null
}
```

- Messages come in conversation order, by `seq`. `limit` is optional, from `1` to `500`, and defaults to `200`. Any other value is a `400`.
- A page can hold fewer messages than `limit` and still carry a `nextCursor`, because a page that would be too large is cut short. Keep reading until `nextCursor` is `null`.
- `kind` is `text`, `card` (a generative UI card, named by the tool that made it in `toolName`) or `event`. `authorRole` is `user`, `agent` or `system`. `text` can be `null`.
- Messages can carry other fields. Ignore any field you don't know: responses can gain fields.

### Who can read them

The sessions and conversation routes are for people, because what visitors type can be personal:

- Call them with a personal access token that has the `manage` scope, or from a console session. A workspace API key, an app service token or a read-only personal access token gets `403`.
- The person must be the app's owner, or, for an app owned by a workspace, an owner or admin of that workspace. Anyone else gets `404`, the same answer as for an app that does not exist. A thread or session id from another app gets that `404` too.
- Each read writes an event to the audit log (the workspace's, for an app owned by a workspace), with how many rows it returned.

To read only one end user's conversations, for example to answer their request for a copy of their data, see [Exporting one end user's conversations](/security/) on the Security page.

## Your end user's own conversation

These routes are called from your frontend, as the end user. They read and delete only that person's own threads. [`@guuey/chat`](/chat/) and [`@guuey/agent-client`](/sdk/) call the transcript and delete routes for you. Neither calls the thread list, so you call that one yourself. When you build your own client, you call all of them.

Every call carries exactly one identity:

- A guest sends the `guuey_guest` cookie (a browser request with `credentials: "include"`) or the guest secret in an `x-guuey-guest` header.
- A signed-in end user sends `Authorization: Bearer <token>`, with a Guuey sign-in token, or with your own sign-in's token for an app in `byo` mode (see [End-user identity](/concepts-identity/)).

A call with none of them gets `401`. See [Give visitors a stable identity](/sdk/#give-visitors-a-stable-identity) for the guest secret.

### List their threads

`GET /v1/agents/{appId}/threads` lists the caller's threads in one app, newest first. Its request and response are on the SDK page, under [Conversation continuity](/sdk/#conversation-continuity).

### Read a thread's transcript

```http
GET /v1/threads/{threadId}/messages?limit=50&nextToken=<nextToken>
x-guuey-guest: <guest secret>
```

```json
{
  "rows": [
    {
      "seq": 0,
      "at": "2026-11-06T09:14:03.120Z",
      "kind": "text",
      "authorRole": "user",
      "text": "Do you ship to Canada?"
    }
  ],
  "nextToken": null,
  "thread": {}
}
```

- `limit` is optional and defaults to `50`. A value above `100` reads as `100`. A missing, zero or unreadable value reads as `50`.
- `nextToken` comes back on every page. Pass it back for the next page until it is `null`. A page can be cut short when it would be too large, so loop on `nextToken`, not on the row count. A token from another thread is a `400`.
- `order=desc` reads newest first, so a client that stops after a few pages keeps the newest messages. The response then carries `"order": "desc"`. Without it, rows come oldest first.
- `include` adds a turn's tool results and calls to its rows. Send it once, comma-separated: `include=toolResults,toolCalls`, or just one of the two. Unknown entries are ignored. `@guuey/agent-client` reads them for you.
- `thread` is `{}` while the conversation is open, and `{ "closed": { "code", "message", "closedAt" } }` once the agent has ended it.

Each row has `seq`, `at`, `kind` (`text`, `card` or `event`), `authorRole` (`user`, `agent` or `system`), `text` (which can be `null`) and `clientMessageId`: the turn's key on the user's row, and that key with a suffix on the agent's rows for the same turn. Its type in `@guuey/agent-client` is optional, so handle a row without it. Rows carry more fields, such as a card's contents. Read those through `fetchThreadHistory` in `@guuey/agent-client`, whose types describe them, and ignore any field you don't know.

The answers, in the order Guuey checks:

- No identity at all: `401`.
- With a guest identity: `404` if the thread does not exist, then `403` if it belongs to someone else.
- With a `Bearer` token: `404` if the thread does not exist (checked before the token), then `401` if the token does not verify, then `403` if the thread belongs to someone else.

### Delete a thread

```http
DELETE /v1/threads/{threadId}
x-guuey-guest: <guest secret>
```

It answers `200` with `{ "deleted": true, "threadId": "…" }`. It takes the same identities as the transcript read and answers with the same `401`, `404` and `403`.

This erases the thread and every message in it. It does not hide them. `clearConversation()` in `@guuey/chat` and `deleteThread` in `@guuey/agent-client` both call it, so a "new chat" button wired to either one deletes the old conversation. To start fresh and keep the old conversation, call `reset()` from `useAgentInvoke` in `@guuey/agent-client`, which starts a new thread and leaves the old one stored. `@guuey/chat` has no call that keeps the old conversation. Who else can delete conversations is on the [Security](/security/) page.

### Move a guest's threads after sign-in

In an app with your own sign-in (`byo` mode), a visitor who chats as a guest and then signs in has a new identity. This route moves their guest threads in the app to the signed-in user. `@guuey/chat` calls it for you when a guest signs in, and `mergeGuestThreads` in `@guuey/agent-client` calls it when you build your own client (see [Signed-in users](/sdk/#signed-in-users-bring-your-own-auth)).

```http
POST /v1/agents/{appId}/merge-guest
Authorization: Bearer <your sign-in's token>
x-guuey-guest: <guest secret>
Content-Type: application/json

{ "threadIds": ["<the open thread>"] }
```

- The request carries both identities: the signed-in user's token from your own sign-in, and the guest's secret in `x-guuey-guest` or the `guuey_guest` cookie. The body names neither.
- The token must come from this app's own sign-in. Guuey checks it against the app's issuer and audience, and a Guuey account token answers `403`. Without the token or the guest credential, the route answers `401`. Only this app's threads move.
- `threadIds` is optional, at most 20. Put the open thread there, because it can be too new for Guuey to find on its own, and keep using that thread id afterwards.
- It answers `200` with `{ "moved": 2, "skipped": 0 }`. One call moves at most 100 threads. When the answer carries `nextCursor`, send it back as `cursor` in the body to move the rest.
- A run the guest left paused does not resume after the move, so the user has to send that message again. Rotate or clear the guest secret afterwards.
- An app without its own sign-in configured (`byo` mode with an issuer and audience) answers `400` with `BYO_NOT_CONFIGURED`. Each signed-in user, and each guest, can be merged up to 10 times an hour, and past that the route answers `429`. A `503` means try again later.

## Running a turn

A turn is one message to your agent and its streamed reply. Run it with the SDK, not raw HTTP:

- In React, `useAgentInvoke` from [`@guuey/agent-client`](/sdk/) runs turns and keeps the thread for you.
- Anywhere else, `invokeTurn` runs one turn as a stream of events. The events are [AgJSON](https://silverprotocol.io/AgJSON), silverprotocol's open event format, and `@silverprotocol/core` reduces them into messages.
- When a turn pauses to ask the user something, answer a consent ask with `createHitlAnswerRelay`.

The SDK functions and the AgJSON event types are the contract. The HTTP framing underneath them can change between releases. See [Building your own client](/sdk/#building-your-own-client).

## Errors

An error answers with a JSON body:

```json
{ "error": { "code": "NOT_FOUND", "message": "Thread … not found" } }
```

Read `code`. `message` is for people and can change.

| Status | Builder routes (`/v1/apps/…`) | End-user routes (`/v1/threads/…`, `/v1/agents/…`) |
| ------ | ----------------------------- | ------------------------------------------------- |
| `400`  | `VALIDATION`                  | `BAD_REQUEST`                                     |
| `401`  | `UNAUTHENTICATED`             | `UNAUTHORIZED`                                    |
| `403`  | `FORBIDDEN`                   | `FORBIDDEN`                                       |
| `404`  | `NOT_FOUND`                   | `NOT_FOUND`                                       |
| `500`  | `INTERNAL`                    | `INTERNAL`                                        |

The two kinds of route use different codes for the same status, as the table shows. A builder route can also answer `409` with `CONFLICT`, or `503` with `TRANSIENT_CONFLICT`, which means "try again". An end-user route can answer `503` with `UNAVAILABLE`, which means "try again shortly". When it sends `Retry-After`, wait that many seconds.

## More builder routes

Other builder routes are documented on the pages for what they do:

- Deploying from CI, rolling back and reading deployments: [Agents as code](/agents-as-code/).
- Erasing or exporting one end user's data: [Security](/security/).
- The CLI calls this API with your key. The [CLI guide](/cli/) covers each command.