Publishing posts
POST /v1/posts is the one call that creates content. With status: "published" it also publishes, so a single request takes a post from nothing to live.
- URL:
POST https://api.canverly.com/v1/posts - Scope:
posts:write - Content-Type:
application/json - Max body size: 4 MiB (larger →
413)
Request body
Section titled “Request body”| Field | Type | Required | Notes |
|---|---|---|---|
title |
string | yes | The post title. |
blocks_json |
object | yes | The body as a block document: { "v": 1, "blocks": [...] }. |
status |
string | no | "draft" (default) or "published". "publish"/"public"/"live" are accepted aliases for published. |
slug |
string | no | URL slug. Auto-derived from title when omitted. |
excerpt |
string | no | Short summary; stored as the SEO description. |
language |
string | no | BCP-47 tag. Defaults to "pt-BR". |
post_type |
string | no | "post" (default), "page", or a custom post-type slug from GET /v1/post-types. |
category_slugs |
string[] | no | Category slugs. Drives the site’s category archives. |
tag_slugs |
string[] | no | Tag slugs. |
Draft vs published
Section titled “Draft vs published”status: "draft"(default) — the post is created but not visible on the public site. Use this to stage content; publish it later from the admin.status: "published"— the post is created, categorized, and published in the same call. It is live immediately athttps://<site-domain>/<slug>.
Categories and tags
Section titled “Categories and tags”category_slugs / tag_slugs are stored on the post and power the site’s category and tag archives. Use the slugs configured for the site (e.g. news, tutorials). Unknown slugs are stored as-is; create the matching category/tag in the admin so the archive page exists.
A complete example
Section titled “A complete example”This publishes a fully-formed article — title, slug, excerpt, category, and a mixed block body (paragraph, heading, list, code):
curl -X POST https://api.canverly.com/v1/posts \ -H "Authorization: Bearer ck_live_xxx" \ -H "Content-Type: application/json" \ -d '{ "title": "Shipping faster with the Canverly API", "slug": "shipping-faster-with-the-canverly-api", "status": "published", "language": "en", "excerpt": "How to automate publishing end to end.", "category_slugs": ["engineering"], "tag_slugs": ["api", "automation"], "blocks_json": { "v": 1, "blocks": [ { "id": "p1", "type": "paragraph", "text": "You can publish from any tool with one authenticated call." }, { "id": "h1", "type": "heading", "attrs": { "level": 2 }, "text": "Why blocks" }, { "id": "l1", "type": "list", "attrs": { "style": "unordered" }, "text": "Portable across themes\nSafe (sanitized)\nEasy to generate" }, { "id": "c1", "type": "code", "attrs": { "language": "bash" }, "text": "curl -X POST https://api.canverly.com/v1/posts" } ] } }'import requests
payload = { "title": "Shipping faster with the Canverly API", "slug": "shipping-faster-with-the-canverly-api", "status": "published", "language": "en", "excerpt": "How to automate publishing end to end.", "category_slugs": ["engineering"], "tag_slugs": ["api", "automation"], "blocks_json": { "v": 1, "blocks": [ {"id": "p1", "type": "paragraph", "text": "You can publish from any tool with one authenticated call."}, {"id": "h1", "type": "heading", "attrs": {"level": 2}, "text": "Why blocks"}, {"id": "l1", "type": "list", "attrs": {"style": "unordered"}, "text": "Portable across themes\nSafe (sanitized)\nEasy to generate"}, {"id": "c1", "type": "code", "attrs": {"language": "bash"}, "text": "curl -X POST https://api.canverly.com/v1/posts"}, ], },}r = requests.post( "https://api.canverly.com/v1/posts", headers={"Authorization": "Bearer ck_live_xxx"}, json=payload, timeout=20,)r.raise_for_status()post = r.json()print(post["id"], post["slug"], post["status"])const payload = { title: "Shipping faster with the Canverly API", slug: "shipping-faster-with-the-canverly-api", status: "published", language: "en", excerpt: "How to automate publishing end to end.", category_slugs: ["engineering"], tag_slugs: ["api", "automation"], blocks_json: { v: 1, blocks: [ { id: "p1", type: "paragraph", text: "You can publish from any tool with one authenticated call." }, { id: "h1", type: "heading", attrs: { level: 2 }, text: "Why blocks" }, { id: "l1", type: "list", attrs: { style: "unordered" }, text: "Portable across themes\nSafe (sanitized)\nEasy to generate" }, { id: "c1", type: "code", attrs: { language: "bash" }, text: "curl -X POST https://api.canverly.com/v1/posts" }, ], },};
const res = await fetch("https://api.canverly.com/v1/posts", { method: "POST", headers: { Authorization: "Bearer ck_live_xxx", "Content-Type": "application/json", }, body: JSON.stringify(payload),});if (!res.ok) throw new Error(`${res.status}: ${await res.text()}`);const post = await res.json();console.log(post.id, post.slug, post.status);<?php$payload = [ "title" => "Shipping faster with the Canverly API", "slug" => "shipping-faster-with-the-canverly-api", "status" => "published", "language" => "en", "excerpt" => "How to automate publishing end to end.", "category_slugs" => ["engineering"], "tag_slugs" => ["api", "automation"], "blocks_json" => [ "v" => 1, "blocks" => [ ["id" => "p1", "type" => "paragraph", "text" => "You can publish from any tool with one authenticated call."], ["id" => "h1", "type" => "heading", "attrs" => ["level" => 2], "text" => "Why blocks"], ["id" => "l1", "type" => "list", "attrs" => ["style" => "unordered"], "text" => "Portable across themes\nSafe (sanitized)\nEasy to generate"], ["id" => "c1", "type" => "code", "attrs" => ["language" => "bash"], "text" => "curl -X POST https://api.canverly.com/v1/posts"], ], ],];$ch = curl_init("https://api.canverly.com/v1/posts");curl_setopt_array($ch, [ CURLOPT_POST => true, CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => ["Authorization: Bearer ck_live_xxx", "Content-Type: application/json"], CURLOPT_POSTFIELDS => json_encode($payload),]);$res = curl_exec($ch);$code = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);if ($code >= 400) { throw new Exception("HTTP $code: $res"); }echo $res, "\n";require "net/http"require "json"
payload = { title: "Shipping faster with the Canverly API", slug: "shipping-faster-with-the-canverly-api", status: "published", language: "en", excerpt: "How to automate publishing end to end.", category_slugs: ["engineering"], tag_slugs: ["api", "automation"], blocks_json: { v: 1, blocks: [ { id: "p1", type: "paragraph", text: "You can publish from any tool with one authenticated call." }, { id: "h1", type: "heading", attrs: { level: 2 }, text: "Why blocks" }, { id: "l1", type: "list", attrs: { style: "unordered" }, text: "Portable across themes\nSafe (sanitized)\nEasy to generate" }, { id: "c1", type: "code", attrs: { language: "bash" }, text: "curl -X POST https://api.canverly.com/v1/posts" }, ], },}
uri = URI("https://api.canverly.com/v1/posts")http = Net::HTTP.new(uri.host, uri.port)http.use_ssl = truereq = Net::HTTP::Post.new(uri)req["Authorization"] = "Bearer ck_live_xxx"req["Content-Type"] = "application/json"req.body = payload.to_jsonres = http.request(req)raise "HTTP #{res.code}: #{res.body}" if res.code.to_i >= 400puts res.bodypackage main
import ( "bytes" "encoding/json" "fmt" "io" "net/http")
func main() { payload := map[string]any{ "title": "Shipping faster with the Canverly API", "slug": "shipping-faster-with-the-canverly-api", "status": "published", "language": "en", "excerpt": "How to automate publishing end to end.", "category_slugs": []string{"engineering"}, "tag_slugs": []string{"api", "automation"}, "blocks_json": map[string]any{ "v": 1, "blocks": []map[string]any{ {"id": "p1", "type": "paragraph", "text": "You can publish from any tool with one authenticated call."}, {"id": "h1", "type": "heading", "attrs": map[string]any{"level": 2}, "text": "Why blocks"}, {"id": "l1", "type": "list", "attrs": map[string]any{"style": "unordered"}, "text": "Portable across themes\nSafe (sanitized)\nEasy to generate"}, {"id": "c1", "type": "code", "attrs": map[string]any{"language": "bash"}, "text": "curl -X POST https://api.canverly.com/v1/posts"}, }, }, } b, _ := json.Marshal(payload) req, _ := http.NewRequest("POST", "https://api.canverly.com/v1/posts", bytes.NewReader(b)) req.Header.Set("Authorization", "Bearer ck_live_xxx") req.Header.Set("Content-Type", "application/json") res, err := http.DefaultClient.Do(req) if err != nil { panic(err) } defer res.Body.Close() out, _ := io.ReadAll(res.Body) if res.StatusCode >= 400 { panic(fmt.Sprintf("HTTP %d: %s", res.StatusCode, out)) } fmt.Println(string(out))}Response
Section titled “Response”201 Created:
{ "id": "01K8Z9K3F7T8M2QYV5N6B4WJ8R", "slug": "shipping-faster-with-the-canverly-api", "status": "published", "public_url": null}id— the post’s ULID (Crockford base32). Keep it to correlate with your source system.slug— the final slug (may differ from your input if it was normalized).status—"published"or"draft".
Verify it’s live
Section titled “Verify it’s live”A published post is reachable at https://<site-domain>/<slug>. Resolve <site-domain> once via GET /v1/sites/me (primary_domain), then:
curl -s -o /dev/null -w "%{http_code}\n" https://blog.example.com/shipping-faster-with-the-canverly-api# 200Custom post types
Section titled “Custom post types”To publish into a custom post type (CPT), set post_type to its slug. Discover valid slugs first with GET /v1/post-types. post and page always exist.
Avoiding duplicates (Idempotency-Key)
Section titled “Avoiding duplicates (Idempotency-Key)”Autoposters retry on timeouts. To make retries safe, send an Idempotency-Key header — an opaque id derived from your source item (e.g. the source id):
curl -X POST https://api.canverly.com/v1/posts \ -H "Authorization: Bearer ck_live_xxx" \ -H "Idempotency-Key: feed-item-2026-0042" \ -H "Content-Type: application/json" \ -d '{ "title": "…", "status": "published", "blocks_json": { "v": 1, "blocks": [] } }'The first call creates the post; any replay with the same key and body returns the original 201 (with Idempotency-Replayed: true) instead of creating a duplicate. The response is cached for 24 hours, scoped per api key.