POST /v1/posts
Create a post on the site that owns the API key. With status: "published" it is published in the same call.
- URL:
https://api.canverly.com/v1/posts - Method:
POST - Auth: Bearer API key — scope
posts:write - Content-Type:
application/json - Max body: 4 MiB
- Rate limit: 60 req/min per key
For worked, multi-language examples see the publishing guide.
Request
Section titled “Request”Headers
Section titled “Headers”| Header | Required | Value |
|---|---|---|
Authorization |
yes | Bearer ck_… |
Content-Type |
yes | application/json |
Idempotency-Key |
no | Opaque id (≤ 255 chars). A replay with the same key + body returns the original 201 (no duplicate). See Idempotency. |
| Field | Type | Required | Notes |
|---|---|---|---|
title |
string | yes | Post title. Must be non-empty. |
blocks_json |
object | yes | Block document: { "v": 1, "blocks": [...] }. See Content blocks. |
status |
string | no | "draft" (default) or "published". |
slug |
string | no | Auto-derived from title if omitted. |
excerpt |
string | no | Stored as the SEO description. |
language |
string | no | BCP-47; default "pt-BR". |
post_type |
string | no | "post" (default), "page", or a custom slug from /v1/post-types. |
category_slugs |
string[] | no | Category slugs (drive category archives). |
tag_slugs |
string[] | no | Tag slugs. |
{ "title": "Hello from the API", "status": "published", "slug": "hello-from-the-api", "excerpt": "A short summary.", "language": "en", "category_slugs": ["news"], "tag_slugs": ["api"], "blocks_json": { "v": 1, "blocks": [ { "id": "p1", "type": "paragraph", "text": "Body text." } ] }}Response
Section titled “Response”201 Created
Section titled “201 Created”{ "id": "01K8Z9K3F7T8M2QYV5N6B4WJ8R", "slug": "hello-from-the-api", "status": "published", "public_url": null}| Field | Type | Notes |
|---|---|---|
id |
string | Post ULID (Crockford base32). |
slug |
string | Final slug (may be normalized from your input). |
status |
string | "published" or "draft". |
public_url |
string | null | Currently null; build the URL as https://<primary_domain>/<slug> (see /v1/sites/me). |
Errors
Section titled “Errors”Envelope: { "error": { "code", "message" } }. Full table in Errors & rate limits.
| Status | error.code |
When |
|---|---|---|
400 |
bad_request |
Malformed JSON, or title empty. |
401 |
unauthorized |
Missing/invalid/revoked key. |
403 |
forbidden |
Key lacks posts:write. |
413 |
— | Body over 4 MiB. |
415 |
— | Missing/incorrect Content-Type. |
422 |
unprocessable |
Missing blocks_json, wrong field type, or a key created before publishing was enabled. |
429 |
rate_limited |
Over 60 req/min — honor Retry-After. |
5xx |
upstream |
Transient; retry with backoff (reuse the same slug). |
Idempotency
Section titled “Idempotency”Send an Idempotency-Key header to make retries safe. The first request runs
normally; its 201 response is cached for 24 hours. Any replay with the same
key and the same body returns that original response with an extra
Idempotency-Replayed: true header — the post is created at most once.
curl -X POST https://api.canverly.com/v1/posts \ -H "Authorization: Bearer ck_live_xxx" \ -H "Idempotency-Key: source-item-42" \ -H "Content-Type: application/json" \ -d '{ "title": "…", "blocks_json": { "v": 1, "blocks": [] } }'- The key is scoped per api key + request body, so it can’t collide across sites or with a different payload.
- A request that errors caches nothing — retrying re-runs it.
- Sending the same key with a different body is treated as a new request (different post).
See also
Section titled “See also”PATCH /v1/posts/{id}— edit or re-date an existing post.GET /v1/posts— read the published archive, with filters, keyset pagination andsincefor incremental sync.POST /v1/import— create many posts in one NDJSON request instead of a loop.- Post SEO — title, description, canonical URL,
no_indexandschema_typeare set on their own route, not in the create body.
There is no public DELETE for posts: deleting content is an admin action.