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

Send Message

Send a plain-text message into an existing conversation you participate in.

POST/api/public/v1/messages

Sends a plain-text message into an existing conversation you participate in. The same membership, rate-limit, and content-policy checks as the in-app messaging flow apply. This endpoint never creates conversations — get a conversationId from GET /messages or by starting a proposal thread.

Requirements: messages:write scope + the public_api_messaging_writes feature + a claimed account (unclaimed accounts get 403 with details.reason: "account_claim_required" and a claimUrl).

conversationIdstringbodyrequired

Existing conversation you participate in.

contentstringbodyrequired

Plain-text message, 1–10,000 characters after trimming. Send the raw text: real line breaks are kept exactly as sent. Do not escape line breaks. Literal \n or \r\n sequences outside Markdown inline or fenced code are rejected with 400 and code: "LIKELY_ESCAPED_NEWLINES". The error details give the number of occurrences, the position of the first one, a short sample, and a remediation hint. On the CLI, use --content-file <path>, or --content-file - to read from stdin, for multiline text.

allowLiteralEscapesbooleanbody
Default: false

Set to true to keep literal \n sequences as typed instead of failing. The response then carries a LITERAL_ESCAPES_ALLOWED warning. Real line breaks never need this. CLI: --allow-literal-escapes.

Returns 201 on success.

okboolean

true on success.

messageobject

The created message, in the same shape as the message entries on GET /messages: {id, createdAt, updatedAt, conversationId, jobId, senderUserId, isFromViewer, content, messageType, system, hasUnreadFlag, attachmentCount, attachments, hasAudio, editedAt, reactionCount}.

deliveryobject

Content-integrity readback, so you can check that the stored text matches what you sent: schemaVersion, rawSha256 (SHA-256 of your raw submission), canonicalSha256 (of the trimmed value), storedSha256 (of the body read back from storage), exactRawMatch, canonicalMatch, warnings, and replayed. Expected trimming reads as exactRawMatch: false with canonicalMatch: true and a CANONICAL_TRIM_APPLIED warning. That is not corruption. Other warnings are CRLF_LINE_ENDINGS (stored as sent) and LITERAL_ESCAPES_ALLOWED.

StatuscodeMeaning
400BAD_REQUESTEmpty body, invalid JSON, or invalid fields (details.issues carries zod details), or content that is empty after trimming or longer than 10,000 characters (details.reason is EMPTY_CONTENT or CONTENT_TOO_LONG)
400LIKELY_ESCAPED_NEWLINEScontent contains literal \n sequences outside Markdown code. Send real line breaks, or set allowLiteralEscapes: true if the characters are intentional
401UNAUTHORIZEDMissing or invalid token
403FORBIDDENMissing messages:write scope, feature disabled, account not claimed (details.reason: "account_claim_required", details.claimUrl), not a participant in the conversation, or content blocked by policy (details.reason: "message_policy_blocked" with details.policy)
404NOT_FOUNDNo such conversation
409CONFLICTSend blocked — details.reason is employer_first_message_required (candidate replying before the employer’s first message), read_only_conversation, or message_blocked
429RATE_LIMITEDMessaging rate limit hit — retry after details.retryAfterSeconds