Writing content bodies in Markdown¶
Every content item — article, guide, video, course, event, event series, link, track, collection — has a body: the formatted text learners read on its page. You author it by sending Markdown in body_markdown, and you read it back from the body_markdown field on any response.
This is the field to use when a program is writing the content: generating an article from a template, importing documentation from a repository, or having an AI agent draft a description.
curl -X POST https://acme.plusplus.app/api/v2/articles/ \
-H "Authorization: Bearer pp_your_token_here" \
-H "Content-Type: application/json" \
-d '{
"name": "Onboarding: Your First Week",
"body_markdown": "## Before you start\n\nMake sure you have:\n\n- A laptop\n- Access to [the handbook](https://handbook.example.com)\n\n## Day one\n\nMeet your manager and set up your dev environment."
}'
The response includes the stored body, converted back to Markdown:
{
"public_id": "01HXY8C5FZ7N4S0M2D3J6Q7K9P",
"name": "Onboarding: Your First Week",
"body_markdown": "## Before you start\n\nMake sure you have:\n\n- A laptop\n- Access to [the handbook](https://handbook.example.com)\n\n## Day one\n\nMeet your manager and set up your dev environment.",
"description": "Before you start Make sure you have: A laptop Access to the handbook Day one Meet your manager and set up your dev environment."
}
description is the plain-text version, derived automatically. It powers previews and search. You never write it directly.
What Markdown is supported¶
| Supported | Not supported |
|---|---|
| Headings, paragraphs, bold, italic | Strikethrough (~~text~~) |
| Bullet and numbered lists, including nesting | Task list checkboxes (- [ ]) |
| Links and images | Inline text colors and highlights |
| Blockquotes, horizontal rules | Raw HTML of any kind |
| Fenced code blocks with a language | Footnotes, definition lists |
| Pipe tables |
Anything unsupported is not an error — it comes through as literal text. ~~gone~~ stays ~~gone~~ in the body, and - [ ] todo becomes a normal bullet reading [ ] todo.
Updating a body¶
body_markdown replaces the entire body. There is no partial or append update.
curl -X PATCH https://acme.plusplus.app/api/v2/articles/01HXY8C5FZ7N4S0M2D3J6Q7K9P/ \
-H "Authorization: Bearer pp_your_token_here" \
-H "Content-Type: application/json" \
-d '{ "body_markdown": "## Updated\n\nThe whole body is now this." }'
To clear a body, send an empty string or null. To leave it untouched, omit the field.
Read-modify-write loses formatting¶
Round-tripping is not lossless
Bodies are stored as rich text, not as your original Markdown. body_markdown is regenerated from that rich text on every read, so what you get back is an equivalent document, not the exact string you sent.
This matters if you read a body, change one sentence, and write it back. Anything the editor supports but Markdown cannot express is dropped in that cycle — including checklist items, text colors and highlights. A body authored in the PlusPlus editor and then round-tripped through this field will lose those.
Formatting that Markdown can express survives, though incidental details may shift: list indentation and table column padding get normalized, and table alignment is not preserved.
If you need to preserve the author's exact source, keep it in your own system and treat PlusPlus as the render target — write to body_markdown, don't read from it.
Content is sanitized¶
Bodies are treated as untrusted input, because they render into pages, calendar invites and emails.
- Raw HTML is stripped.
<script>,<iframe>, event handlers likeonerror, and everything else are removed before storage. Scripting elements are removed along with their contents. - Links are restricted to
http,httpsandmailto. Any other scheme (javascript:,data:,vbscript:) is replaced with#. - Images are not re-hosted.
stores that URL as-is. If the source goes away, the image breaks. To host an image in PlusPlus, upload it through the dashboard editor. - PlusPlus-hosted images come back as signed URLs. They expire after a set time (currently 1–2 days). Fetch the item again for fresh URLs — don't store them or hand them to a system that will load them later.
- Bodies are capped at 100,000 characters of Markdown source. Larger payloads are rejected with
422.
Type-specific behavior¶
Most content types just store and display the body. Three do something extra:
- Events — the body is the event description. It renders into the calendar invite and into the event-update email sent to facilitators, attendees and the wait list. Changing it re-pushes the calendar event and re-notifies attendees, the same as changing any other event field. Don't rewrite bodies in bulk on live events unless you intend to email everyone.
- Event series — the body applies to the series record only. It is not copied onto events already scheduled from the series, nor onto events created from it later.
- Guides — the body is a catalog summary, not the guide's lesson content. Lesson content comes from the uploaded ZIP package (see Building a guide). Most integrations should leave a guide's body unset.
Bulk updates¶
The bulk update endpoints (PATCH /events/bulk/, PATCH /event-series/bulk/) reject body_markdown and description with a 422. Bodies are edited one resource at a time, through that resource's own PATCH.
Errors¶
| Status | code |
Cause |
|---|---|---|
400 |
content_body_conflict |
Both body_markdown and the deprecated description were sent in one request. |
400 |
invalid_content_body |
The Markdown could not be converted into a valid document. |
422 |
validation_error |
body_markdown is longer than the 100,000-character cap. |
The deprecated description field on writes¶
description used to be accepted on update payloads. It never actually saved anything — the field was silently discarded. It now works on both create and update payloads, writing plain text (one paragraph per line) into the body.
Prefer body_markdown. Sending both in one request is rejected with 400 content_body_conflict.