Skip to content
OpenTrain AIOpenTrain AIOpenTrain AIDocs

Ask OpenTrain

Answers from the documentation, with sources.

What would you like to do with OpenTrain?

AI answers can be mistaken. Check the linked sources. Don’t include private account information.

Open app

DEVELOPER DOCUMENTATION

Read a Message Thread

Read one thread in full: the parent message and every reply, oldest first, with who replied last.

GET/api/public/v1/messages/thread

Reads 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.

  • Follow nextCursor until it is null to 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.version changes 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 compare version. It is an observation, not a lock: someone can still post between your read and your reply.
threadIdstringqueryrequired

The thread to read.

limitnumberquery
Default: 100

Replies per page, 1–200.

cursorstringquery

nextCursor from the previous page, or the ID of the last reply you read. It must be a reply in this thread.

asOfstring

ISO time of the read. The page and freshness come from the same database snapshot.

threadIdstring

The thread.

conversationIdstring

The conversation the thread belongs to.

contextTypestring

proposal_dm, job_dm, or channel.

jobIdstring | null

Linked job.

proposalIdstring | null

Linked proposal, for proposal conversations.

needsEmployerReplyboolean

true 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 | null

The 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[].

replyCountnumber

Total replies in the thread.

nextCursorstring | null

Pass 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.

StatuscodeMeaning
400BAD_REQUESTInvalid query parameters, or a cursor that is not a reply in this thread (details.reason: "UNKNOWN_CURSOR")
401UNAUTHORIZEDMissing or invalid token
403FORBIDDENMissing messages:read scope, or no access to the conversation. details.reason is not_a_participant, membership_removed, or conversation_hidden
404NOT_FOUNDNo such thread (details.reason: "thread_missing")
503SERVICE_UNAVAILABLEA database dependency was briefly unavailable. Retry the same request after Retry-After
  • CLI: opentrain messages thread get --thread-id <THREAD_ID> with --cursor and --limit.
  • MCP: opentrain_get_message_thread, with threadId, cursor, and limit.
  • TypeScript SDK: opentrain.getMessageThread({ threadId, cursor, limit }).