# Canverly Public API > Public REST API for a Canverly site: publish and edit posts, read the public > archive, upload media, set SEO, change site settings, read analytics and form > leads, and move content in and out in bulk. Any tool, automation or AI agent > can use it. JSON over HTTPS. Authentication is a per-site API key > (`Authorization: Bearer ck_...`). The site is fixed by the key — a key can only > ever touch its own site. Post bodies are a block document (`blocks_json`), not > raw HTML. This file contains everything an agent needs. Base URL: https://api.canverly.com OpenAPI: https://api.canverly.com/openapi.json (served by the API) Contract: https://developers.canverly.com/specs/content-openapi.yaml (version 1.0.0, 24 operations under /v1) Docs: https://developers.canverly.com/en/ (pt-BR: https://developers.canverly.com/) Max request body: 4 MiB JSON (8 MiB for media upload and NDJSON import) ## Authentication Send on every request: Authorization: Bearer ck_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx Two key classes: - ck_ secret key. Server-side only. Can write. Required by every route except the two public read routes, which also accept pk_. - pk_ public read key. Read-only, one site, only from origins the owner registered. May be sent as ?key=pk_... (avoids a CORS preflight). Reaches ONLY GET /v1/posts and GET /v1/posts/{reference}; every other route answers 401 to a pk_ key. A ck_ key in ?key= is a 400. Keys are created in the admin (Configurações → Integrações API) and shown once. No API key can create, list or revoke a key — key management is a site-owner browser session, not an API surface. Scopes are opt-in per key. The full list: posts:write posts:read post_types:read sites:read site:read site:write seo:read seo:write media:read media:write analytics:read leads:read export import Note: sites:read (identity) and site:read (settings blob) are DIFFERENT scopes. A key may also carry per-request restrictions, all fail-closed: IP/CIDR allowlist, country allowlist (GeoIP; an indeterminate country DENIES), a time window in an IANA zone, and — for pk_ — an origin allowlist. A violation is 403 naming the dimension, e.g. "request blocked by this key's IP restriction". ## Rate limits - 60 req/min per key .......... every route except the two public read routes - 600 req/min per key ......... GET /v1/posts, GET /v1/posts/{reference} - 60 req/min per (key, IP) .... the same two routes, pk_ keys only - 10 req/min per key .......... /v1/leads, /v1/leads/{id}, export of leads (layered ON TOP of the 60/min bucket) 429 carries Retry-After (seconds). A request refused by a key restriction costs no quota. ## Endpoint: POST /v1/posts (scope: posts:write) Create a post; publish immediately with status "published". Optional header: Idempotency-Key: A replay with the same key AND body returns the original 201 (with header Idempotency-Replayed: true) and creates no duplicate. Cached 24h, scoped per api key. Errors cache nothing (safe to retry). Request body (application/json): - title string REQUIRED - blocks_json object REQUIRED { "v": 1, "blocks": Block[] } - status string optional "draft" (default) | "published" - slug string optional auto from title; send a stable slug for safe retries - excerpt string optional stored as SEO description - language string optional BCP-47, default "pt-BR" - post_type string optional "post" (default) | "page" | custom slug - category_slugs string[] optional - tag_slugs string[] optional Never send site_id / org_id / author_id — they are bound to the key and ignored. Block (each needs a unique non-empty "id"): - { "id":"p1", "type":"paragraph", "text":"inline HTML: ,,,,
" } - { "id":"h1", "type":"heading", "attrs":{"level":2}, "text":"Heading" } (level 2–6) - { "id":"l1", "type":"list", "attrs":{"style":"unordered"}, "text":"item1\nitem2" } (style: "unordered"|"ordered"; or attrs.items[]+attrs.ordered) - { "id":"c1", "type":"code", "attrs":{"language":"python"}, "text":"verbatim code" } - { "id":"q1", "type":"quote", "text":"A quote." } - { "id":"i1", "type":"image", "attrs":{"src":"https://.../x.jpg","alt":"..."} } HTML in block text is allowlist-sanitized: