Content blocks
Post bodies are a block document, not a single HTML string. This keeps content portable across themes and safe to render. You send it as the blocks_json field of POST /v1/posts.
Envelope
Section titled “Envelope”{ "v": 1, "blocks": [ { "id": "p1", "type": "paragraph", "text": "…" } ]}v— schema version. Always1.blocks— an ordered array. Render order = array order.
Every block has:
| Field | Type | Required | Notes |
|---|---|---|---|
id |
string | yes | Unique, non-empty within the document (e.g. p1, h1, b_ab12). |
type |
string | yes | One of the block types below. |
text |
string | depends | Text/HTML payload (see each type). |
attrs |
object | depends | Type-specific attributes. |
Block types
Section titled “Block types”type |
Carries | Renders as |
|---|---|---|
paragraph |
text (inline HTML) |
<p> |
heading |
attrs.level (2–6), text |
<h2>…<h6> |
list |
attrs.style "unordered"/"ordered" + text (one item per line) |
<ul> / <ol> |
code |
attrs.language, text (verbatim) |
<pre><code> |
quote |
text |
<blockquote> |
image |
attrs.src (or attrs.media_id), attrs.alt |
<figure><img> |
divider |
— | <hr> |
paragraph
Section titled “paragraph”text may contain inline HTML: <a href>, <strong>, <em>, <code>, <br>. It is sanitized server-side (see below).
{ "id": "p1", "type": "paragraph", "text": "Read the <a href=\"https://docs.canverly.com\">docs</a> for <strong>more</strong>." }heading
Section titled “heading”{ "id": "h1", "type": "heading", "attrs": { "level": 2 }, "text": "Section title" }Put one item per line in text, separated by \n. Items may contain inline HTML.
{ "id": "l1", "type": "list", "attrs": { "style": "unordered" }, "text": "First item\nSecond item\nThird with a <a href=\"https://x.com\">link</a>" }text is preserved verbatim (no HTML interpretation). attrs.language drives syntax highlighting.
{ "id": "c1", "type": "code", "attrs": { "language": "python" }, "text": "print('hello')" }Reference an already-hosted image by URL, or a Canverly media id if you have one.
{ "id": "i1", "type": "image", "attrs": { "src": "https://cdn.example.com/cover.jpg", "alt": "Cover" } }HTML sanitization
Section titled “HTML sanitization”Inline HTML inside block text (and list items) is rendered as HTML, then run through an allowlist sanitizer when published through the public API. This means:
- Kept: formatting tags (
a,strong,em,code,b,i,u,s,p,br,ul/ol/li,h1–h6,blockquote,img,pre,span, tables) andhttp/https/mailtolinks. - Stripped:
<script>, event handlers (onerror,onclick, …),javascript:URLs,<iframe>, and anything else not on the allowlist.
So you can safely send rich text, but you cannot inject scripts into a site through an API key. If you need plain text, just send plain text — it is passed through unchanged.
A complete body
Section titled “A complete body”{ "v": 1, "blocks": [ { "id": "p1", "type": "paragraph", "text": "An intro paragraph with a <a href=\"https://canverly.com\">link</a>." }, { "id": "h1", "type": "heading", "attrs": { "level": 2 }, "text": "Steps" }, { "id": "l1", "type": "list", "attrs": { "style": "ordered" }, "text": "Get a key\nPOST /v1/posts\nVerify it's live" }, { "id": "c1", "type": "code", "attrs": { "language": "bash" }, "text": "curl https://api.canverly.com/v1/sites/me -H 'Authorization: Bearer ck_…'" }, { "id": "q1", "type": "quote", "text": "Publishing should be one API call." } ]}