DEVELOPER DOCUMENTATION
Read a Message Thread
Read one thread in full: the parent message and every reply, oldest first, with who replied last.
/api/public/v1/messages/threadReads one message thread: the parent message and its replies, oldest first, with complete bodies. Get a threadId from thread.threadId on a message in GET /messages/audit, or from threadId on a reply.
This endpoint is read-only. Attachments appear as opaque metadata only; to download one, pass its attachments[].attachmentId to GET /messages/{messageId}/attachments/{attachmentId}, and read privacy and sensitive files first.
Requirements: messages:read scope, and either membership in the conversation or employer access to its job. If you were removed from the conversation, or the conversation is hidden from you, the read is refused; employer job access does not override that.
Paging and freshness
Section titled “Paging and freshness”- Follow
nextCursoruntil it isnullto read every reply. - To check for new replies later, save the ID of the last reply you read and pass it as
cursor. This returns later replies only; it does not show edits to replies you already read. freshness.versionchanges when anything in the thread changes, including edits and deletions outside the page you read. Before you reply, read the thread again from the first page and compareversion. It is an observation, not a lock: someone can still post between your read and your reply.
Request
Section titled “Request”threadIdstringqueryrequiredThe thread to read.
limitnumberquery100Replies per page, 1–200.
cursorstringquerynextCursor from the previous page, or the ID of the last reply you read. It must be a reply in this thread.
Response
Section titled “Response”asOfstringISO time of the read. The page and freshness come from the same database snapshot.
threadIdstringThe thread.
conversationIdstringThe conversation the thread belongs to.
contextTypestringproposal_dm, job_dm, or channel.
jobIdstring | nullLinked job.
proposalIdstring | nullLinked proposal, for proposal conversations.
needsEmployerReplybooleantrue when the latest person-written message in the whole thread, parent included, came from someone outside your organization. System events, deleted messages and moderated messages are skipped, so they never open or answer it. A reply from any teammate answers it. See How needsEmployerReply works.
parentobject | nullThe message that started the thread, as a message record (the same shape as messages[] in GET /messages/audit).
repliesobject[]This page of replies, oldest first, as message records. Each record has state (visible, deleted, or moderated), content (null when deleted or moderated), sender ({userId, displayName, role, employerOrganizationMember, isFromViewer}), and opaque attachments[].
replyCountnumberTotal replies in the thread.
nextCursorstring | nullPass back as cursor for the next page; null at the end.
latestWorkerReplyobject | null{messageId, senderUserId, createdAt} of the latest reply from a freelancer in the whole thread.
latestEmployerReplyobject | null{messageId, senderUserId, createdAt} of the latest reply from your organization in the whole thread.
freshnessobject{latestReplyId, latestReplyAt, version}. See Paging and freshness.
Errors
Section titled “Errors”| Status | code | Meaning |
|---|---|---|
400 | BAD_REQUEST | Invalid query parameters, or a cursor that is not a reply in this thread (details.reason: "UNKNOWN_CURSOR") |
401 | UNAUTHORIZED | Missing or invalid token |
403 | FORBIDDEN | Missing messages:read scope, or no access to the conversation. details.reason is not_a_participant, membership_removed, or conversation_hidden |
404 | NOT_FOUND | No such thread (details.reason: "thread_missing") |
503 | SERVICE_UNAVAILABLE | A database dependency was briefly unavailable. Retry the same request after Retry-After |
CLI, MCP, and SDK
Section titled “CLI, MCP, and SDK”- CLI:
opentrain messages thread get --thread-id <THREAD_ID>with--cursorand--limit. - MCP:
opentrain_get_message_thread, withthreadId,cursor, andlimit. - TypeScript SDK:
opentrain.getMessageThread({ threadId, cursor, limit }).