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

POST
/api/open/v1/articles

Create a standalone article to attach at creation time

GET
/api/open/v1/articles/{id}

Get an article with its ordered elements

GET
/api/open/v1/articles/{id}/markdown

The article flattened to one markdown document

PATCH
/api/open/v1/articles/{id}

Update the article title/subtitle

PUT
/api/open/v1/articles/{id}/elements

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

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

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

WhereShape
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 id update 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…"
  }
}