Authentication and sessions

The three chat wires take the same credential. Durability is a separate choice: the OpenAI and Anthropic wires are stateless until you opt in, and AG-UI is durable from the first turn.

Authentication

The credential is a workspace API key, minted in Studio under Integrations → API keys and shown once when you create it. It looks like sbk_live_<selector>_<secret>, and it carries the list of agents and workflows it may call. See API keys for the format, the permissions and how to revoke one.

All three chat wires expect that key as a bearer credential: Authorization: Bearer <key> (the convention the OpenAI SDK sends) or x-api-key: <key> (the Anthropic SDK's header) both work on any of them. Authorization wins when a request carries both. A missing, unknown, revoked or expired credential returns 401 with the matching wire's error shape ({"error": {...}} for OpenAI, {"type": "error", "error": {...}} for Anthropic, {"type": "RUN_ERROR", "message": "..."} for AG-UI). One answer covers all four cases, so a 401 never says which part of a guess was right. A key the box accepts but that was not granted the agent you asked for returns 403 instead.

The two workflow routes a key opens, POST /api/workflows/<name>/start and POST /api/workflows/<name>/chat, take the same key. The rest of the workflow surface does not: it sits behind the container's own gate, which the control plane and Stackbone Studio satisfy for you.

Under stackbone dev every gate is off, so a local box answers an un-credentialed request on all of these. Keys are worth testing against a deployed box, where they are actually checked.

Session keys

A plain chat call is stateless: you replay the full messages[] array each turn, the same as calling OpenAI or Anthropic directly. The server keeps no transcript and records each turn on its own, so a stateless conversation is never grouped into a single session.

Two optional headers change that:

Header What it does Where history lives
x-stackbone-session Durable server-side session. The agent threads the conversation's history and tool state across calls, so you send only the newest user turn and the server carries the rest. Required for tool approvals. On the server
x-stackbone-conversation Grouping only. You still replay the full messages[], but the server joins the turns into one session so they show up together instead of as separate one-off runs. No durable state, no tool approvals. On the client (you replay it)

Both take any non-empty string you choose, stable for the life of one conversation. Send one, not both. A request carrying both counts as a durable session, and the server ignores x-stackbone-conversation. With neither header, each turn is an independent run and the server groups nothing.

AG-UI uses neither header. Its threadId field on the request body carries the same durable, only-send-the-newest-turn semantics as x-stackbone-session, and it is mandatory rather than opt-in. A non-empty threadId is always a durable session. Leave it empty and you get one stateless turn with no pause capability.

Run tags

Send x-stackbone-run-tags to label the run a turn opens. The value is a flat JSON object of string keys and string values:

x-stackbone-run-tags: {"channel":"voice","room":"call-4f2a"}

The OpenAI and AG-UI wires stamp the object on the run, and stackbone runs list --tag channel:voice finds it again. A tag is a label only. It never changes what the turn may do.

Rule What the server keeps
Key A lowercase slug: a letter first, then letters, digits, _, . or -, up to 64 characters. Never a colon.
Value A non-empty string with no line break, other control character or unpaired surrogate (a lone \ud83d). A longer value is cut to at most 256 characters.
Count The first 16 entries that pass both rules.

A header value must stay inside Latin-1, so escape any other character as \uXXXX in the JSON. The server drops an entry that breaks a rule, and a header that is not a JSON object is dropped whole. Either way the turn runs.

BUILT WITH ❤️ FROM CANADA AND SPAIN