---
source: https://threa.io/developers/markdown
notes: Markdown mirror generated from the page above. YOUR_WORKSPACE_ID is the ws_… id in the app URL after /w/; YOUR_API_KEY is a key from Settings > API keys.
---

# 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:*

```bash
curl -X POST https://app.threa.io/api/v1/workspaces/YOUR_WORKSPACE_ID/streams/STREAM_ID/messages \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -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://…)` |

> **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:*

```text
Ping @pierre and @ariadne about #releases.
```

*you read back:*

```text
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:*

```bash
# 1. upload, returns { "id": "attach_…" }
curl -X POST https://app.threa.io/api/v1/workspaces/YOUR_WORKSPACE_ID/attachments \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -F file=@./report.pdf

# 2. reference that id in a message
curl -X POST https://app.threa.io/api/v1/workspaces/YOUR_WORKSPACE_ID/streams/STREAM_ID/messages \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -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.
