Skip to content
Public beta preview — you're seeing the sneak peek

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.

Send one header on every builder route:

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)
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)
guuey_svc_ An app service token. It can only deploy its own app: the reconcile and deployment routes in Agents as code. guuey tokens create --label "<what uses it>" --app-id <appId> (see the CLI guide)

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.

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.

GET /v1/apps/{appId}/sessions?limit=200
Authorization: Bearer <key>
{
"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:

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.

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.

GET /v1/apps/{appId}/threads?limit=50&cursor=<nextCursor>
Authorization: Bearer <key>
{
"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).
GET /v1/apps/{appId}/threads/{threadId}/messages?limit=200&cursor=<nextCursor>
Authorization: Bearer <key>
{
"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.

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 on the Security page.

These routes are called from your frontend, as the end user. They read and delete only that person’s own threads. @guuey/chat and @guuey/agent-client 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).

A call with none of them gets 401. See Give visitors a stable identity for the guest secret.

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.

GET /v1/threads/{threadId}/messages?limit=50&nextToken=<nextToken>
x-guuey-guest: <guest secret>
{
"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 /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 page.

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).

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.

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 runs turns and keeps the thread for you.
  • Anywhere else, invokeTurn runs one turn as a stream of events. The events are 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.

An error answers with a JSON body:

{ "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.

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

  • Deploying from CI, rolling back and reading deployments: Agents as code.
  • Erasing or exporting one end user’s data: Security.
  • The CLI calls this API with your key. The CLI guide covers each command.