Skip to content

POST /v1/posts

Create a new post on the site that owns the API key.

  • URL: https://api.canverly.com/v1/posts
  • Method: POST
  • Auth: Bearer API key
  • Required scope: posts:write
  • Content-Type: application/json
  • Rate limit: 60 req/min per key (default)

Request

Headers

HeaderRequiredDescription
AuthorizationyesBearer ck_live_...
Content-Typeyesapplication/json
Idempotency-KeynoArbitrary string ≤ 128 chars. Replaying the same key within 24h returns the original response without re-creating the post.

Body

{
"post_type": "post",
"status": "published",
"title": "Hello from the API",
"slug": "hello-from-the-api",
"excerpt": "A short summary that shows up in lists.",
"content_html": "<p>Full HTML body.</p>",
"cover_image_url": "https://cdn.example.com/cover.jpg",
"tags": ["news", "api"],
"published_at": "2026-06-07T18:00:00Z",
"meta": {
"seo_title": "Hello from the API",
"seo_description": "How to call the Canverly API."
}
}

Fields

FieldTypeRequiredNotes
post_typestringyesSlug from GET /v1/post-types. Defaults to post.
statusenumyesdraft | published | scheduled.
titlestringyes≤ 240 chars.
slugstringnoAuto-derived from title if omitted.
excerptstringno≤ 500 chars, plain text.
content_htmlstringconditionalRequired when status is published or scheduled. Sanitized server-side.
cover_image_urlstring (URL)noHTTPS only. Canverly mirrors the asset.
tagsstring[]noAuto-created if missing.
published_atstring (ISO 8601)conditionalRequired when status is scheduled, must be in the future.
metaobjectnoFree-form SEO/extension fields.

Response

201 Created

{
"id": "01HZX9K3F7T8M2QYV5N6B4WJ8R",
"site_id": "01HZX0000000000000000000SITE",
"post_type": "post",
"status": "published",
"title": "Hello from the API",
"slug": "hello-from-the-api",
"excerpt": "A short summary that shows up in lists.",
"cover_image_url": "https://cdn.canverly.com/sites/abc/cover.jpg",
"tags": ["news", "api"],
"published_at": "2026-06-07T18:00:00Z",
"created_at": "2026-06-07T18:00:00Z",
"updated_at": "2026-06-07T18:00:00Z",
"url": "https://example-site.canverly.com/hello-from-the-api"
}

id is a ULID. url is the canonical public URL — use it for verification or to back-link from the source system.

Example: curl

Terminal window
curl -X POST https://api.canverly.com/v1/posts \
-H "Authorization: Bearer ck_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: chimpee-item-42" \
-d '{
"post_type": "post",
"status": "published",
"title": "Hello from the API",
"content_html": "<p>Hi.</p>",
"tags": ["news"]
}'

Errors

All errors are returned as application/problem+json (RFC 7807).

Statustype slugWhen
400bad-requestMalformed JSON.
401unauthenticatedMissing/invalid Authorization.
403insufficient-scopeKey lacks posts:write.
404post-type-not-foundpost_type slug does not exist on this site.
409slug-conflictA post with the given slug already exists for this post_type.
422validation-failedField-level validation errors — see errors[] in body.
429rate-limitedExceeded 60 req/min. Honour Retry-After.
500internalServer error — safe to retry with Idempotency-Key.

422 example

{
"type": "https://docs.canverly.com/errors/validation-failed",
"title": "Validation failed",
"status": 422,
"errors": [
{ "field": "title", "code": "too_long", "message": "title must be ≤ 240 chars" },
{ "field": "content_html", "code": "required", "message": "content_html is required when status is published" }
]
}

Idempotency

Sending the same Idempotency-Key twice within 24 hours returns the original 201 response and does not create a duplicate post. After 24 hours the key is forgotten and a replay creates a new post. The server stores (api_key_id, idempotency_key) → response — keys are scoped per API key, so two different consumers using the same string do not collide.

If you replay a key with a different body, the server returns 409 Conflict with type: idempotency-mismatch rather than silently overwriting.