Markdown
The message content format.
Message bodies are markdown. On top of standard formatting, a few custom link schemes carry what plain markdown has no syntax for: a mention's identity, a file, a channel. They survive a round trip unchanged.
The content field
You send a message body in the content field as a markdown
string. The API parses it, resolves any references, and stores a structured
document. Reads return the canonical markdown in the same content
field.
curl -X POST /api/v1/workspaces//streams/STREAM_ID/messages \
-H "Authorization: Bearer " \
-H "Content-Type: application/json" \
-d '{ "content": "Deploy is green. Thanks @pierre. Details in #releases." }'
What you write and what you read back are not always byte-identical. You can
write a mention as a bare @pierre; the API resolves it and stores
the actor it points at, so the read-back form is the resolved
[@pierre](user:usr_…). The text a person sees is the same either
way.
Standard markdown
Common GitHub-flavored markdown renders as you'd expect. Supported:
| Element | Syntax |
|---|---|
| Bold, italic, strikethrough | **bold**, *italic*, ~~struck~~ |
| Inline code, code blocks | `code`, triple-backtick fences with a language |
| Headings | # H1 through ###### H6 |
| Lists | - item, 1. item |
| Tables | GFM pipe tables |
| Blockquotes, rules | > quote, --- |
| Links | [text](https://…) |
user: or attachment:) is a typed reference, not a
plain link; Threa renders it as a chip or card. Use https://,
mailto:, or a relative path for ordinary links.
Mentions and channels
Write a mention as a bare @slug and a channel as
#slug. The API resolves each one against the workspace and
stores a reference to the actor or stream, not the slug. Slugs can change;
the stored reference doesn't.
Ping @pierre and @ariadne about #releases. Ping [@pierre](user:usr_8x2…) and [@ariadne](persona:persona_a1…) about [#releases](channel:stream_4f…).
The label inside the brackets is a display name; the link target is the
stable id. An actor is a workspace member (user:), an agent like
Ariadne (persona:), or a bot (bot:).
@here and @channel are broadcasts and resolve to
broadcast:here / broadcast:channel.
If you already know the id, you can write the resolved form directly; it round-trips unchanged. A bare slug that resolves to nothing is left as typed.
Attachments
Upload a file first, then reference the returned id in a message. The scheme
is attachment:<id>; the link label is the filename shown in
the message.
# 1. upload, returns { "id": "attach_…" }
curl -X POST /api/v1/workspaces//attachments \
-H "Authorization: Bearer " \
-F file=@./report.pdf
# 2. reference that id in a message
curl -X POST /api/v1/workspaces//streams/STREAM_ID/messages \
-H "Authorization: Bearer " \
-H "Content-Type: application/json" \
-d '{ "content": "Q3 numbers: [report.pdf](attachment:attach_…)" }' Emoji
Write emoji as :shortcode: (for example :tada:).
Known shortcodes resolve to the character; an unknown one stays as text. Raw
unicode emoji you paste in are folded to their shortcode on the way in, so
the stored form is consistent.
Link scheme reference
Every typed reference rides on standard [label](scheme:payload)
link syntax, so a client that doesn't understand a scheme still shows the
label as readable text. The ones you'll write are the first group; the rest
you'll mostly encounter on reads.
| Scheme | Reference | Example |
|---|---|---|
user: | workspace member mention | [@pierre](user:usr_…) |
persona: | agent mention (e.g. Ariadne) | [@ariadne](persona:persona_…) |
bot: | bot mention | [@deploybot](bot:bot_…) |
broadcast: | @here / @channel | [@here](broadcast:here) |
channel: | channel link | [#releases](channel:stream_…) |
attachment: | uploaded file | [report.pdf](attachment:attach_…) |
memo: | memory card | [Auth rewrite](memo:memo_…) |
quote: | quoted reply (read) | [Alice](quote:stream_…/msg_…/usr_…/user) |
shared-message: | cross-stream share (read) | [Alice](shared-message:stream_…/msg_…) |
Why the schemes exist
Threa stores each message as a structured document, not as a string. The
markdown you send and read is a projection of that document. Plain markdown
has no way to say "this is a mention of member usr_8x2," only
"this is the text @pierre." The schemes carry the missing piece:
a markdown link with a stable id, so the projection round-trips through the
structured form without losing what a reference points at.
For an integration: send plain, readable markdown, and read the resolved form
back. If you only need the text, strip the link syntax to its label; a
mention reduces to @pierre, a file to its filename.