Articles
An article is the rich content body used across Monotree. Handbook and course chapters own one (their article_id), and announcements, events, calendar entries, certificates and staff cards expose an article_id too. An article has a title, a subtitle, and an ordered list of typed elements — the content blocks.
Articles are addressed by their own id, so these endpoints work identically for every content type. The body convenience field on announcements, events, calendar entries and staff cards only works while the article is simple (empty or a single paragraph) — once it carries rich content, body writes return 422 to protect it.
Required scope: read:articles, write:articles (plus write:media when attaching media).
Endpoints
/api/open/v1/articlesCreate a standalone article to attach at creation time
/api/open/v1/articles/{id}Get an article with its ordered elements
/api/open/v1/articles/{id}/markdownThe article flattened to one markdown document
/api/open/v1/articles/{id}Update the article title/subtitle
/api/open/v1/articles/{id}/elementsReplace the article's elements (bulk save)
Build first, attach at create
Announcements and calendar entries publish by default the moment they are created (events default to drafts) — enriching published content afterwards briefly exposes half-built pages. Either create as a draft (is_published: false), enrich, then publish — or: POST /articles { "title?", "subtitle?", "translations?" } creates a standalone article, fill it via PUT /articles/{id}/elements, then pass article_id when creating the announcement/event/calendar entry. The content is complete the instant it goes live.
article_id and body are mutually exclusive (422), and the article must not already belong to other content (422).
Element types
Writable types and their structure:
| Type | Structure | Notes |
|---|---|---|
paragraph | { "type": "paragraph", "body": "markdown" } | Body is markdown in and out (bold, italic, links, lists; other markdown flattens to text). |
header | { "type": "header", "body": "plain text" } | A section heading. |
image_gallery | { "type": "image_gallery", "title": "optional", "media_ids": [1, 2] } | 1–10 image media. |
video_gallery | { "type": "video_gallery", "title": "optional", "media_ids": [3] } | 1–10 video media. |
document | { "type": "document", "name": "optional", "media_ids": [4] } | 1–10 document media (pdf, docx, …); text is extracted for search. |
Read-only types — returned on GET, never created or modified through the API:
| Type | Shape |
|---|---|
reference | { "type": "reference", "referenced_type": "Handbook", "referenced_id": 3 } — an in-app link to other content. |
gif | { "type": "gif", "url": "https://…" } |
Media in elements
Upload media first via POST /media (the token also needs the write:media scope), then reference it by id in media_ids. Media must be uploaded by the same token, and the media type must match the element type (422 otherwise).
Translations
Text fields read and write the customer's default content locale; a field-keyed translations object carries the other locales and is returned on every read:
| Where | Shape |
|---|---|
| article | "translations": { "title": { "da": "…" }, "subtitle": { "da": "…" } } |
paragraph / header | "translations": { "body": { "da": "…" } } — paragraph values are markdown, like body |
document | "translations": { "name": { "da": "…" } } |
| handbook / course | "translations": { "title": { "da": "…" }, "subtitle": { "da": "…" } } — the content's own title/subtitle, on POST/PATCH /handbooks and /courses |
Merge semantics: locales you omit keep their value. Locale keys must be activated languages (422 otherwise), and writing a translation automatically activates that locale on the owning content (handbook, course, …). Galleries have no translatable text fields.
Reads always return the full translations map on the fields above — list endpoints included. Two places read the default content locale only: chapter title/subtitle (in chapter responses and the chapter summaries on the handbook/course) — fetch the chapter's article for the translations — and the body convenience field. For one language at a time, use the markdown export with ?locale=.
Bulk save
PUT /articles/{id}/elements takes the complete desired state of the article, in order:
- items with an element
idupdate that element in place - items without an id create a new element at that position
- writable elements missing from the array are deleted; the final order is the array order
- read-only elements are never deleted — include them by id to position them, or omit them and they keep their place after your items
curl -X PUT https://customer.monotree.com/api/open/v1/articles/91/elements \
-H "Authorization: Bearer mono_your_token_here" \
-H "Content-Type: application/json" \
-d '{
"elements": [
{ "id": 501, "type": "header", "body": "First day" },
{ "type": "paragraph", "body": "New **markdown** paragraph", "translations": { "body": { "da": "Nyt **markdown** afsnit" } } },
{ "id": 503, "type": "image_gallery", "title": "Photos", "media_ids": [123] }
]
}'Response
{
"data": {
"id": 91,
"title": "Getting Started",
"subtitle": "First steps",
"translations": { "title": { "en": "Getting Started", "da": "Kom godt i gang" }, "subtitle": { "en": "First steps" } },
"elements": [
{ "id": 501, "type": "header", "body": "First day", "translations": { "body": { "en": "First day" } } },
{ "id": 502, "type": "paragraph", "body": "Hello **world**", "translations": { "body": { "en": "Hello **world**", "da": "Hej **verden**" } } },
{ "id": 503, "type": "image_gallery", "title": "Photos",
"media": [ { "id": 123, "type": "image", "url": "https://…", "mime_type": "image/jpeg",
"filename": "team.jpg", "size": 12345, "width": 1200, "height": 800, "created_at": "…" } ] }
],
"created_at": "2026-07-01T09:00:00+00:00",
"updated_at": "2026-07-01T09:00:00+00:00"
}
}Markdown export
GET /articles/{id}/markdown flattens any article into one markdown document — convenient for search indexes or LLMs. Pass ?locale= to read a specific activated language (see locales); defaults to the customer's default content locale. Unknown or inactive locales return 422.
{
"data": {
"id": 91,
"title": "Getting Started",
"locale": "da",
"markdown": "# Getting Started\n\nFirst steps\n\n### Welcome aboard\n\n…"
}
}