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

  • The channel id. Channels are not created explicitly — they exist by being used.

Query Parameters

  • List the direct children of this thread. Empty (parent=) lists the root threads. Exclusive with root — the control plane refuses both together; the realtime edge lets a non-empty root win.

  • List every thread in this branch, at any depth. Exclusive with parent — the control plane refuses both together; the realtime edge lets a non-empty root win.

  • Opaque keyset cursor, from the previous page's nextCursor. Absent = first page. A present but malformed value is 400 validation_failed (reason: invalid_cursor) on both hosts.

  • 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
Request Example for get/v1/channels/{channelId}/threads
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'
A root thread and one of its sub-threads, more available
{
  "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"
}