List a channel's threads
Enumerates the channel's session registry — one node per thread, whether or not the caller participates in it. A thread's id is the id of the message that started it; since threads nest, that message may itself be a thread reply. Pick exactly one filter:
-
parent={threadId}— the direct children of that thread.parent=(empty) lists the root threads; omitting both filters does the same. root={threadId}— every thread in that branch, at any depth.
Nodes come back newest-spawned first (spawnSeq desc, id asc),
keyset-paged by the opaque cursor request param; the next page's
cursor comes back as nextCursor, absent on the last page. limit
defaults to 50; the valid range is 1–100.
This is enumeration, not the inbox: a caller's own participant list,
with unread counts and read positions, is the inbox's. The same endpoint
serves both callers: use the control plane with a secret key for
server-side reads, or the realtime edge with a user JWT for client
reads, which are admitted exactly like history. Browser CORS is
enabled on the realtime edge, scoped to the environment's allowed
origins.
The two hosts handle a malformed query differently — except cursor,
which both hosts refuse identically because it is validated once, at the
shared coordinator. A malformed cursor (not the exact
{spawnSeq}:{threadId} shape) is 400 validation_failed with
reason: invalid_cursor on both hosts; an absent cursor is always the
first page. For parent/root/limit, the control plane refuses a bad
query: parent and root together is 400 validation_failed with
reason: parent_and_root_exclusive, and a limit that is not an
integer in 1–100 is 400 validation_failed. The realtime edge does not
validate those: a non-empty root takes precedence over parent, and a
malformed or out-of-range limit is normalised (non-numeric or ≤ 0
falls back to 50, above 100 is capped at 100). Send a well-formed query
and both hosts behave identically.
Path Parameters
- Type: stringchannel
Id requiredThe channel id. Channels are not created explicitly — they exist by being used.
Query Parameters
- Type: stringparent
List the direct children of this thread. Empty (
parent=) lists the root threads. Exclusive withroot— the control plane refuses both together; the realtime edge lets a non-emptyrootwin. - Type: stringroot
List every thread in this branch, at any depth. Exclusive with
parent— the control plane refuses both together; the realtime edge lets a non-emptyrootwin. - Type: stringcursor
Opaque keyset cursor, from the previous page's
nextCursor. Absent = first page. A present but malformed value is400 validation_failed(reason: invalid_cursor) on both hosts. - Type: integerlimitmin:1max:100
Page size (default 50, max 100). Outside 1–100 or not an integer, the control plane answers
validation_failed; the realtime edge normalises it (see the description).
Responses
- application/json
- application/json
- application/json
- application/json
curl 'https://api.useportal.co/v1/channels/chat-general/threads?parent=m_1752912000_41&root=m_1752912000_41&limit=50' \
--header 'Authorization: Bearer YOUR_SECRET_TOKEN'
{
"threads": [
{
"id": "m_1752912100_45",
"parentThreadId": "m_1752912000_41",
"rootThreadId": "m_1752912000_41",
"depth": 2,
"spawnSeq": 45,
"spawnedBy": {
"id": "u_456"
},
"latestSeq": 52,
"threadSeq": 1,
"createdAt": 1752912099000
},
{
"id": "m_1752912000_41",
"rootThreadId": "m_1752912000_41",
"depth": 1,
"spawnSeq": 41,
"spawnedBy": {
"id": "u_123"
},
"latestSeq": 58,
"threadSeq": 3,
"createdAt": 1752911999000
}
],
"hasMore": true,
"nextCursor": "41:m_1752912000_41"
}