The card's conversation, or what changed since a cursor
const url = 'http://localhost:8000/api/portal/2489E9AD-2EE2-8E00-8EC9-32D5F69181C0/thread/';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/portal/2489E9AD-2EE2-8E00-8EC9-32D5F69181C0/thread/ \ --header 'Authorization: Bearer <token>'Without since: the conversation — participants, the newest page of messages (oldest first), the reader’s own marker and settings, and a cursor. With since=<cursor>: what changed since — messages created, edited or deleted, oldest first — with reset: true and the newest page instead when the cursor is stale or a card changed (put it in place of what you hold). Poll it every 8 s with If-None-Match; an unchanged thread is 304 with no body. 400 invalid_cursor.
On this side a card the portal does not show (a result not shared, withdrawn or moved; a metric the team hides) reads {kind, placeholder: true}, recomputed on every read.
The signed-in person’s grant on the card in the path — View is enough. 404 while client access is switched off; 404 card_not_found for a card the account never held; 403 access_ended (reason: revoked · team_plan · minor) and consent_required as on every portal route; a partner API key gets 403. 429 past the account’s rate (chat_read 240/min for reads, chat_post 30/min for writes).
Authorizations
Section titled “Authorizations”Parameters
Section titled “Parameters”Path Parameters
Section titled “Path Parameters”Query Parameters
Section titled “Query Parameters”The cards’ language: en · ru · es.
The cursor of the last answer: answers what changed since (the delta). Send the last ETag as If-None-Match: 304 when nothing did.
metric (default) · imperial.
Responses
Section titled “Responses”GET …/thread/ — the conversation (ClientThread).
object
Opaque. Send it back as ?since= to poll for what changed.
ThreadParticipant.
object
coach- Coachathlete- Athleteguardian- Parent or guardian
False once this person can no longer read the thread.
The newest page, oldest first.
One message (ThreadMessage).
object
The author’s participant id; null once their account is gone.
coach- Coachathlete- Athleteguardian- Parent or guardian
Coach side: a name, else an address. Client side: a name only, never an address — empty when the person gave none.
Empty once deleted, and for a shared result nobody reviewed.
kind: session — a whole session.
object
True: kind alone — the card cannot be shown to this reader (or its subject is gone).
The discriminator.
session- session
kind: session_moment — a moment of a session’s clip.
object
True: kind alone — the card cannot be shown to this reader (or its subject is gone).
The discriminator.
session_moment- session_moment
Where in the clip, in milliseconds.
kind: metric — one metric of a session.
object
True: kind alone — the card cannot be shown to this reader (or its subject is gone).
The discriminator.
metric- metric
The value as the session reads it, with its unit.
kind: report — a Solutions report.
object
True: kind alone — the card cannot be shown to this reader (or its subject is gone).
The discriminator.
report- report
kind: plan_step — a step of an agent’s plan. The coach side reads
its title, the agent and the agent thread; the client side its place in
the plan and whether it is done (the title is “Plan step N” — AI text
stays coach-only).
object
True: kind alone — the card cannot be shown to this reader (or its subject is gone).
The discriminator.
plan_step- plan_step
Client side: its place in the plan.
Client side: marked done.
Coach side only.
Coach side only.
kind: document — a document on the athlete a coach shared (#2693
S5), carried only by the document_shared notice. Never its file name,
the problem it is filed under or who uploaded it.
object
True: kind alone — the card cannot be shown to this reader (or its subject is gone).
The discriminator.
document- document
medical, imaging, clearance, consent_form, insurance or other.
The coach’s title; may be empty — name it by its kind.
The date on the paper.
result_shared- Result sharedclient_upload- Client sent a clipupload_refused- Client’s clip refused: team out of analysesdocument_shared- Document shared
Null for a message someone wrote.
result_shared- Result sharedclient_upload- Client sent a clipupload_refused- Client’s clip refused: team out of analysesdocument_shared- Document shared
Soft delete: the row stays and reads “message deleted”.
Coach side: false while nobody on the client’s side holds access, on a plan without client access, and for staff. Client side: true.
staff- staffplan- planconsent_pending- consent_pendingminor- minorinvite_pending- invite_pendingno_access- no_access
Null when can_post; else why not, as the server would refuse a post — coach side, in this order: staff (not a member of the team), plan (a read-only team, or a plan without client access: the 402), then, with nobody on the card’s side who can read it now, consent_pending (someone accepted and has not consented yet), minor (the athlete’s own access ended: they are a minor), invite_pending (nobody joined yet; an invite is out and still usable) or no_access (nobody accepted an invite). Client side: always null.
staff- staffplan- planconsent_pending- consent_pendingminor- minorinvite_pending- invite_pendingno_access- no_access
ThreadSettings — the signed-in person’s own, for this conversation.
object
GET …/thread/?since=<cursor> — what changed since the cursor.
object
True: messages is the newest page, to put in place of what the client holds (the cursor was stale, or a card changed).
Messages created, edited or deleted since the cursor, oldest first.
One message (ThreadMessage).
object
The author’s participant id; null once their account is gone.
coach- Coachathlete- Athleteguardian- Parent or guardian
Coach side: a name, else an address. Client side: a name only, never an address — empty when the person gave none.
Empty once deleted, and for a shared result nobody reviewed.
kind: session — a whole session.
object
True: kind alone — the card cannot be shown to this reader (or its subject is gone).
The discriminator.
session- session
kind: session_moment — a moment of a session’s clip.
object
True: kind alone — the card cannot be shown to this reader (or its subject is gone).
The discriminator.
session_moment- session_moment
Where in the clip, in milliseconds.
kind: metric — one metric of a session.
object
True: kind alone — the card cannot be shown to this reader (or its subject is gone).
The discriminator.
metric- metric
The value as the session reads it, with its unit.
kind: report — a Solutions report.
object
True: kind alone — the card cannot be shown to this reader (or its subject is gone).
The discriminator.
report- report
kind: plan_step — a step of an agent’s plan. The coach side reads
its title, the agent and the agent thread; the client side its place in
the plan and whether it is done (the title is “Plan step N” — AI text
stays coach-only).
object
True: kind alone — the card cannot be shown to this reader (or its subject is gone).
The discriminator.
plan_step- plan_step
Client side: its place in the plan.
Client side: marked done.
Coach side only.
Coach side only.
kind: document — a document on the athlete a coach shared (#2693
S5), carried only by the document_shared notice. Never its file name,
the problem it is filed under or who uploaded it.
object
True: kind alone — the card cannot be shown to this reader (or its subject is gone).
The discriminator.
document- document
medical, imaging, clearance, consent_form, insurance or other.
The coach’s title; may be empty — name it by its kind.
The date on the paper.
result_shared- Result sharedclient_upload- Client sent a clipupload_refused- Client’s clip refused: team out of analysesdocument_shared- Document shared
Null for a message someone wrote.
result_shared- Result sharedclient_upload- Client sent a clipupload_refused- Client’s clip refused: team out of analysesdocument_shared- Document shared
Soft delete: the row stays and reads “message deleted”.
Set with reset only.
staff- staffplan- planconsent_pending- consent_pendingminor- minorinvite_pending- invite_pendingno_access- no_access
Null when can_post; else why not, as the server would refuse a post — coach side, in this order: staff (not a member of the team), plan (a read-only team, or a plan without client access: the 402), then, with nobody on the card’s side who can read it now, consent_pending (someone accepted and has not consented yet), minor (the athlete’s own access ended: they are a minor), invite_pending (nobody joined yet; an invite is out and still usable) or no_access (nobody accepted an invite). Client side: always null.
staff- staffplan- planconsent_pending- consent_pendingminor- minorinvite_pending- invite_pendingno_access- no_access
ThreadSettings — the signed-in person’s own, for this conversation.
object
Who is in the conversation now, sent when it changed since the cursor — a grant accepted or ended, a coach joining or leaving, the card becoming a minor’s: on the portal side with reset (the fingerprint moved), on the coach side beside the usual messages. Absent: keep what you hold.
ThreadParticipant.
object
coach- Coachathlete- Athleteguardian- Parent or guardian
False once this person can no longer read the thread.
With participants: the athlete reads as a minor.
Examples
The conversation
{ "client_id": "a1f1c0a2-0000-4000-8000-0000000000aa", "cursor": "41.9b2e4c70d1aa", "participants": [ { "id": "c0ac4000-0000-4000-8000-0000000000c1", "display_name": "Kim Larsen", "avatar_url": "", "role": "coach", "active": true }, { "id": "9a7e0000-0000-4000-8000-0000000000b2", "display_name": "Olga Lebedeva", "avatar_url": "", "role": "guardian", "active": true } ], "messages": [ { "id": "3f2a1b0c-0000-4000-8000-0000000000d3", "author_id": "c0ac4000-0000-4000-8000-0000000000c1", "author_role": "coach", "author_name": "Kim Larsen", "text": "", "reference": { "kind": "session", "placeholder": false, "session_id": "5e2b9c41-7d3a-4f80-9b6e-1c0d2e3f4a51", "activity": "running", "title": "Treadmill check", "recorded_at": "2026-11-02T09:40:00Z", "thumbnail_url": null }, "event": "result_shared", "created_at": "2026-11-02T10:16:00Z", "edited_at": null, "deleted": false }, { "id": "3f2a1b0c-0000-4000-8000-0000000000d3", "author_id": "c0ac4000-0000-4000-8000-0000000000c1", "author_role": "coach", "author_name": "Kim Larsen", "text": "Nice cadence on this one — let's keep the stride short next week.", "reference": { "kind": "session", "placeholder": false, "session_id": "5e2b9c41-7d3a-4f80-9b6e-1c0d2e3f4a51", "activity": "running", "title": "Treadmill check", "recorded_at": "2026-11-02T09:40:00Z", "thumbnail_url": null }, "event": null, "created_at": "2026-11-02T10:16:00Z", "edited_at": null, "deleted": false } ], "has_older": false, "me_id": "c0ac4000-0000-4000-8000-0000000000c1", "last_read_message_id": "3f2a1b0c-0000-4000-8000-0000000000d3", "can_post": true, "post_blocked_reason": null, "minor": true, "settings": { "muted": false, "pinned": false, "archived": false }}What changed
{ "cursor": "42.9b2e4c70d1aa", "reset": false, "messages": [ { "id": "3f2a1b0c-0000-4000-8000-0000000000d3", "author_id": "c0ac4000-0000-4000-8000-0000000000c1", "author_role": "coach", "author_name": "Kim Larsen", "text": "Edited: keep the stride short.", "reference": { "kind": "session", "placeholder": false, "session_id": "5e2b9c41-7d3a-4f80-9b6e-1c0d2e3f4a51", "activity": "running", "title": "Treadmill check", "recorded_at": "2026-11-02T09:40:00Z", "thumbnail_url": null }, "event": null, "created_at": "2026-11-02T10:16:00Z", "edited_at": "2026-11-02T10:20:00Z", "deleted": false } ], "has_older": null, "last_read_message_id": "3f2a1b0c-0000-4000-8000-0000000000d3", "can_post": true, "post_blocked_reason": null, "settings": { "muted": false, "pinned": false, "archived": false }}A card the portal does not show
{ "cursor": "42.5d1c09a7be33", "reset": false, "messages": [ { "id": "3f2a1b0c-0000-4000-8000-0000000000d3", "author_id": "c0ac4000-0000-4000-8000-0000000000c1", "author_role": "coach", "author_name": "Kim Larsen", "text": "", "reference": { "kind": "session", "placeholder": true }, "event": "result_shared", "created_at": "2026-11-02T10:16:00Z", "edited_at": null, "deleted": false } ], "has_older": null, "last_read_message_id": "3f2a1b0c-0000-4000-8000-0000000000d3", "can_post": true, "post_blocked_reason": null, "settings": { "muted": false, "pinned": false, "archived": false }}Nothing changed since the If-None-Match tag: no body; keep what you hold.
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).
Examples
400 — a cursor this route never gave
{ "detail": "invalid_cursor"}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
access_ended, consent_required, or DRF’s message for a partner API key.
With access_ended: why the card no longer opens — revoked, team_plan or minor.
With consent_required: the version to accept.
With consent_required: the texts to show.
Examples
403 — access ended
{ "detail": "access_ended", "reason": "revoked", "client_name": "Sofia Lebedeva", "team_name": "North Track Academy"}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).
Examples
404 — not a card of this account
{ "detail": "card_not_found"}object
429 — throttled: this account’s (or address’s) rate for the route is used up; try again after wait seconds, which Retry-After repeats. A route with a limit of its own answers its own code instead, documented there.
With throttled: whole seconds until the next request can pass.
Example generated
{ "detail": "example", "wait": 1}