Threa Developers

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.

send a message
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:

ElementSyntax
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
TablesGFM pipe tables
Blockquotes, rules> quote, ---
Links[text](https://…)
Reserved schemes A link whose target starts with one of the schemes below (for example 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.

you write
Ping @pierre and @ariadne about #releases.
you read back
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.

upload, then attach
# 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.

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