Read one conversation
const url = 'http://localhost:8000/api/agents/threads/example/';const options = {method: 'GET', headers: {Authorization: 'Bearer <token>'}};
try { const response = await fetch(url, options); const data = await response.json(); console.log(data);} catch (error) { console.error(error);}curl --request GET \ --url http://localhost:8000/api/agents/threads/example/ \ --header 'Authorization: Bearer <token>'One stored conversation, resolved admin-scoped: a thread id belonging to another coach returns 404, not 403 — a 403 would confirm the conversation exists.
Each turn carries the agent that produced it — blank on the coach’s own turns, since a question has no persona. A conversation may mix them.
panel is the agents panel this conversation keeps (#2369): {tabs, active}, where tabs are hrefs of screens the panel hosts and active is plan, new (the + tab) or a tab’s index.
PATCH renames it (a blank title hands naming back to the auto-titler), stores its panel tabs, or both. Tabs are cleaned rather than refused: at most 12, deduped, hosted paths of this site only, a session tab without its ?t= moment; an active that no longer points at a kept tab falls back to plan. Storing tabs does not move the conversation in the history list. DELETE removes the conversation and its turns.
Authorizations
Section titled “Authorizations”Parameters
Section titled “Parameters”Path Parameters
Section titled “Path Parameters”Responses
Section titled “Responses”One stored conversation, with its turns — the read-a-thread shape.
agent here is the persona of the most recent ANSWER, not the conversation’s
identity: render it as an icon, never use it as a filter key. Per-turn attribution
lives on each message.
object
One stored turn. citations replays the grounding provenance so a
reopened conversation keeps its “Based on: …” footnotes (#759).
agent is the persona that PRODUCED this turn, and it is blank on the coach’s own
turns — a question has no persona. A conversation can mix them (switching persona
answers the next question in the same thread), so this is what a client reads to
attribute an answer; the thread-level agent is only the most recent one.
object
object
What the conversation is holding, or null.
Deliberately NOT the video URL. The client needs to know THAT there is a clip and what it is called, so it can keep the chip up; handing it a media URL again would widen the surface for no gain — the browser already has the one it uploaded, and a stale tab should not be given a fresh handle on an object it may no longer be entitled to.
object
object
Example
{ "subject_kind": "team"}object
Human-readable message, or a stable machine code for the cases a client branches on. The standard envelope for 400 (validation — a field-keyed object may appear instead), 401 (missing / invalid credentials), 403 (authenticated but not permitted), and 404 (absent — cross-team records are collapsed to 404 so the API never leaks the existence of another team’s data).
Example generated
{ "detail": "example"}The caller’s tier does not include the coaching agents.
object
One of eight canonical strings. The front-end paywall switches copy + CTA on this value (exact string equality — do not localise). quota_exhausted / seat_limit_reached / client_limit_reached / feature_not_in_plan lead to an upgrade CTA; subscription_suspended leads to a Billing Portal CTA so the buyer can update their payment method; exercise_not_in_plan leads to a contact-us CTA — the movement list is sales-managed, no tier adds to it (#2030). trial_expired / access_ended mean the team is read-only (#2901): everything opens, nothing new is created; they lead to a subscribe CTA and always carry reset_at: null.
quota_exhausted- Session quota for the period reached 0subscription_suspended- Stripe dunning in flight (past_due / unpaid)seat_limit_reached- Adding another team member would exceed plan.max_seatsclient_limit_reached- Adding another Client would exceed plan.max_client_profilesfeature_not_in_plan- The tier does not include this whole feature surfaceexercise_not_in_plan- The (activity, exercise) is not in the team’s exercise grantstrial_expired- The team is read-only: its self-serve trial endedaccess_ended- The team is read-only: its paid plan or granted access ended
ISO-8601 UTC timestamp the team’s current period ends. Null when the team has no live subscription (legacy un-migrated row). On Free + paid this is the renewal moment; quota refreshes to exactly plan.session_quota (NOT additive — leftover sessions don’t roll over).
The active plan’s role string (free, starter, professional, organization, strategic). Same value as current_role — kept as two separate fields for forward-compatibility with a future named-plan split where current_plan could carry an SKU and current_role the tier label.
The active plan’s role string. See current_plan.
Which feature surface is locked. Present ONLY on feature_not_in_plan, so one modal can name what’s gated without a detail string per feature (#773). The only value today is agents (aikynetix/agents/services.py::AGENTS_FEATURE).
Examples
Free team opens an agent conversation
{ "detail": "feature_not_in_plan", "reset_at": null, "current_plan": "free", "current_role": "free", "feature": "agents"}object
Human-readable message, or a stable machine code for the cases a client branches on. The standard envelope for 400 (validation — a field-keyed object may appear instead), 401 (missing / invalid credentials), 403 (authenticated but not permitted), and 404 (absent — cross-team records are collapsed to 404 so the API never leaks the existence of another team’s data).
Example generated
{ "detail": "example"}object
Human-readable message, or a stable machine code for the cases a client branches on. The standard envelope for 400 (validation — a field-keyed object may appear instead), 401 (missing / invalid credentials), 403 (authenticated but not permitted), and 404 (absent — cross-team records are collapsed to 404 so the API never leaks the existence of another team’s data).
Example generated
{ "detail": "example"}