Skip to content

OpenAPI reference: Content API (canverly-api)

This page is generated at build time from docs/openapi.yaml in the canverly-api repository. Do not edit it by hand: the source is the specification.

  • Specification version: 1.0.0
  • Total operations: 27
  • Declared servers: https://api.canverly.com
  • Published specification (YAML): /specs/content-openapi.yaml

Public REST API for programmatic publishing into Canverly sites. Any tool can integrate (no-code autoposters, custom CMS bridges, AI agents, CI jobs).

Authentication: Authorization: Bearer ck_<token>. A key is created in the admin under Configurações → Integrações API and shown once. Each key is bound to exactly one site; the site_id is derived from the key, never from the request body — a key can only ever write to its own site.

Rate limit: 60 requests/minute per key (token bucket) → 429.

Scopes: posts:write, posts:read, post_types:read, sites:read, media:read, media:write, site:read, site:write, seo:read, seo:write, analytics:read, leads:read, export, import. Scopes are opt-in per key — a key only carries what was requested when it was created. leads:read reads PII (form submissions): it is never granted by default, every read/export is audited, and it has its own stricter rate limit (10/min) on top of the general 60/min. Bulk import also requires posts:write; bulk export of posts also requires posts:write (it includes drafts), and bulk export of leads also requires leads:read.

Content is a block document (blocks_json), not raw HTML. Inline HTML inside block text is allowlist-sanitized server-side (scripts stripped).

Listar publicações do site (leitura pública)

Acervo público paginado do site da chave. Devolve só o que é público: rascunho, agendado no futuro e restrito não são filtrados na apresentação — a consulta nunca os seleciona. Paginação por keyset (cursor opaco sobre published_at + id), nunca OFFSET.

O corpo do post NÃO vem na listagem; use o detalhe.

Authentication: apiKey, publicReadKey

Name In Required Description
limit query no 1..50. Fora da faixa devolve 400 — o valor NUNCA é cortado em silêncio.
cursor query no Cursor opaco vindo do next_cursor da página anterior.
category query no Slug de categoria; separe por vírgula para casar QUALQUER.
tag query no Slug de tag; separe por vírgula para casar QUALQUER.
q query no Busca em título e resumo.
since query no RFC-3339. Só posts com updated_at >= since — é o gancho de sincronização incremental do instalável.
language query no Tag BCP-47, ex. pt-BR.
status query no Estados separados por vírgula: draft, scheduled, published, archived (deleted NÃO é listável — post apagado é soft-delete e fica fora da API pública). Mandar este parâmetro TROCA a superfície: passa a valer só chave secreta ck_ com o escopo posts:write (chave pública pk_ recebe 403, nunca conteúdo), os itens viram PostSummary (sem corpo) e a resposta deixa de ser cacheável (private, no-store, sem ETag). Valor desconhecido devolve 400. Omitir mantém exatamente o comportamento público de hoje.
post_type query no Slug de tipo de post. Só vale junto de status — a listagem pública já se restringe aos tipos públicos no SQL do authoring.
key query no Chave pública de leitura na query, para quem chama do navegador: mantém a requisição “simples” para o CORS e evita a viagem de preflight que o cabeçalho Authorization obriga. Só aceita pk_ — uma ck_ aqui devolve 400, porque vazaria para log de acesso, Referer e histórico do navegador. Ignorada quando há cabeçalho Authorization.
Status Description
200 Página do acervo público
304 Não modificado (só no modo público — a listagem autenticada não emite ETag)
400 Parâmetro inválido (ex. limit fora da faixa, cursor corrompido)
401 Chave ausente ou inválida
403 Escopo insuficiente — ou, para chave pública (pk_), a origem da requisição não está na allowlist da chave. A chave é válida; o lugar de onde ela está sendo usada é que não é. Cadastre a origem no painel, em Configurações → Integrações API.
429 Rate limit (60/min) excedido

Criar (e opcionalmente publicar) um post

Cria um post no site da chave. Com status: "published" o post é publicado e fica imediatamente visível no site. category_slugs / tag_slugs são gravados em seo_jsonb (usados pelos arquivos de categoria/tag do site público).

Authentication: apiKey

Name In Required Description
Idempotency-Key header no Opaque client-generated id. A replay with the same key AND the same body returns the original 201 (header Idempotency-Replayed: true) and does not create a duplicate. Cached 24h, scoped per api key.

Content types: application/json (CreatePost)

Status Description
201 Post criado (e publicado, se solicitado)
401 Chave ausente ou inválida
403 Escopo insuficiente — ou, para chave pública (pk_), a origem da requisição não está na allowlist da chave. A chave é válida; o lugar de onde ela está sendo usada é que não é. Cadastre a origem no painel, em Configurações → Integrações API.
422 Requisição válida mas não-processável (ex. chave antiga sem contexto de autoria)
429 Rate limit (60/min) excedido

Obter uma publicação por id ou slug (leitura pública)

Mesmo portão público da listagem. Rascunho, agendado, restrito e post de outro site respondem 404 — indistinguível de inexistente, de propósito: um 403 confirmaria que o rascunho alheio existe.

content_html é o corpo já renderizado e sanitizado na saída (allowlist). Sem <script>, <iframe> ou <form>; blocos de anúncio e de cartão de produto são omitidos; embed de vídeo vira link.

Authentication: apiKey, publicReadKey

Name In Required Description
reference path yes ULID (26), UUID (36) ou slug.
key query no Chave pública de leitura na query, para quem chama do navegador: mantém a requisição “simples” para o CORS e evita a viagem de preflight que o cabeçalho Authorization obriga. Só aceita pk_ — uma ck_ aqui devolve 400, porque vazaria para log de acesso, Referer e histórico do navegador. Ignorada quando há cabeçalho Authorization.
Status Description
200 Publicação pública com corpo
304 Não modificado
401 Chave ausente ou inválida
403 Escopo insuficiente — ou, para chave pública (pk_), a origem da requisição não está na allowlist da chave. A chave é válida; o lugar de onde ela está sendo usada é que não é. Cadastre a origem no painel, em Configurações → Integrações API.
404 Não encontrado, ou não público
429 Rate limit (60/min) excedido

Editar um post existente, por id ou slug

Patch parcial — só os campos enviados mudam. A referência funciona como no GET: id (ULID/UUID) ou slug. Diferente do GET, a busca por slug aqui também encontra post que ainda NÃO está no ar (rascunho e agendado) — é o que permite corrigir pela API um post criado sem que o id tenha sido guardado.

published_at (RFC3339) move/retroage a data de um post no ar e re-pinga o IndexNow. featured_image_id define a capa a partir de um id de asset de POST /v1/media; mande null para limpar, ou omita o campo para não mexer. A chave só edita post do próprio site.

Authentication: apiKey

Name In Required Description
reference path yes Id do post (ULID 26 / UUID 36) ou slug. O slug é resolvido dentro do site desta chave: primeiro os publicados, depois TODOS os rascunhos e agendados — sem janela e sem teto de quantidade. Post ARQUIVADO continua sendo referenciado por id. Se o mesmo slug existir em mais de um idioma, a resposta é 400 pedindo ?language= — nunca uma escolha silenciosa, que editaria o post errado.
language query no Tag BCP-47, usada SÓ para desambiguar slug — a unicidade é por (site, slug, language), então o mesmo slug pode existir em dois idiomas. Sem este parâmetro, esse caso devolve 400 pedindo desambiguação, em vez de editar o post errado em silêncio. Ignorado quando a referência já é um id.

Content types: application/json (UpdatePost)

Status Description
200 Post atualizado
400 Parâmetro inválido (ex. limit fora da faixa, cursor corrompido)
401 Chave ausente ou inválida
403 Escopo insuficiente — ou, para chave pública (pk_), a origem da requisição não está na allowlist da chave. A chave é válida; o lugar de onde ela está sendo usada é que não é. Cadastre a origem no painel, em Configurações → Integrações API.
404 Não encontrado, ou não público
429 Rate limit (60/min) excedido

Listar tipos de post (CPT) do site

Authentication: apiKey

Status Description
200 Lista de tipos de post
401 Chave ausente ou inválida
403 Escopo insuficiente — ou, para chave pública (pk_), a origem da requisição não está na allowlist da chave. A chave é válida; o lugar de onde ela está sendo usada é que não é. Cadastre a origem no painel, em Configurações → Integrações API.

Dados do site da chave

Authentication: apiKey

Status Description
200 Site resolvido a partir da chave
401 Chave ausente ou inválida

Listar os assets de mídia do site

Mais recentes primeiro. Sem cursor: paginação por limit (1..500). Filtros opcionais folder_id / tag_id.

Authentication: apiKey

Name In Required Description
limit query no 1..500. Fora da faixa devolve 400 — nunca cortado em silêncio.
folder_id query no —
tag_id query no —
Status Description
200 Página de assets do site
400 Parâmetro inválido (ex. limit fora da faixa, cursor corrompido)
401 Chave ausente ou inválida
403 Escopo insuficiente — ou, para chave pública (pk_), a origem da requisição não está na allowlist da chave. A chave é válida; o lugar de onde ela está sendo usada é que não é. Cadastre a origem no painel, em Configurações → Integrações API.
429 Rate limit (60/min) excedido

Subir um asset de mídia (multipart)

Upload multipart com um campo file. O asset é gravado no site da chave; as variantes são geradas de forma assíncrona (resposta com status: processing). Teto de corpo: 8 MiB.

Authentication: apiKey

Content types: multipart/form-data (object)

Status Description
202 Aceito — asset gravado, variantes processando
400 Parâmetro inválido (ex. limit fora da faixa, cursor corrompido)
401 Chave ausente ou inválida
403 Escopo insuficiente — ou, para chave pública (pk_), a origem da requisição não está na allowlist da chave. A chave é válida; o lugar de onde ela está sendo usada é que não é. Cadastre a origem no painel, em Configurações → Integrações API.
413 Corpo do upload acima de 8 MiB
415 Tipo de mídia não suportado
429 Rate limit (60/min) excedido

Obter um asset de mídia

Um id de outro site responde 404 — a existência não é revelada.

Authentication: apiKey

Name In Required Description
id path yes —
Status Description
200 Asset de mídia
401 Chave ausente ou inválida
403 Escopo insuficiente — ou, para chave pública (pk_), a origem da requisição não está na allowlist da chave. A chave é válida; o lugar de onde ela está sendo usada é que não é. Cadastre a origem no painel, em Configurações → Integrações API.
404 Não encontrado, ou não público
429 Rate limit (60/min) excedido

Apagar um asset de mídia

Soft-delete do asset. Um id de outro site responde 404.

Authentication: apiKey

Name In Required Description
id path yes —
Status Description
204 Apagado
401 Chave ausente ou inválida
403 Escopo insuficiente — ou, para chave pública (pk_), a origem da requisição não está na allowlist da chave. A chave é válida; o lugar de onde ela está sendo usada é que não é. Cadastre a origem no painel, em Configurações → Integrações API.
404 Não encontrado, ou não público
429 Rate limit (60/min) excedido

Ler as configurações editáveis do site

Authentication: apiKey

Status Description
200 Configurações do site
401 Chave ausente ou inválida
403 Escopo insuficiente — ou, para chave pública (pk_), a origem da requisição não está na allowlist da chave. A chave é válida; o lugar de onde ela está sendo usada é que não é. Cadastre a origem no painel, em Configurações → Integrações API.
429 Rate limit (60/min) excedido

Atualizar as configurações do site

Só campos permitidos são aplicados; settings é MESCLADO sobre o blob gravado (chaves: social, footer_columns, legal_links, search, customization, advanced, subscriptions_enabled, language_url_mode). org_id/status/site_id/slug do corpo são ignorados. Corpo sem nada editável é 400.

Authentication: apiKey

Content types: application/json (SiteSettingsPatch)

Status Description
200 Configurações atualizadas
400 Parâmetro inválido (ex. limit fora da faixa, cursor corrompido)
401 Chave ausente ou inválida
403 Escopo insuficiente — ou, para chave pública (pk_), a origem da requisição não está na allowlist da chave. A chave é válida; o lugar de onde ela está sendo usada é que não é. Cadastre a origem no painel, em Configurações → Integrações API.
429 Rate limit (60/min) excedido

Ler o SEO no nível do site

Authentication: apiKey

Status Description
200 SEO do site
401 Chave ausente ou inválida
403 Escopo insuficiente — ou, para chave pública (pk_), a origem da requisição não está na allowlist da chave. A chave é válida; o lugar de onde ela está sendo usada é que não é. Cadastre a origem no painel, em Configurações → Integrações API.
429 Rate limit (60/min) excedido

Atualizar o SEO no nível do site

Chaves de SEO permitidas, mescladas em settings.seo. Corpo sem nada editável é 400.

Authentication: apiKey

Content types: application/json (SiteSeo)

Status Description
200 SEO atualizado
400 Parâmetro inválido (ex. limit fora da faixa, cursor corrompido)
401 Chave ausente ou inválida
403 Escopo insuficiente — ou, para chave pública (pk_), a origem da requisição não está na allowlist da chave. A chave é válida; o lugar de onde ela está sendo usada é que não é. Cadastre a origem no painel, em Configurações → Integrações API.
429 Rate limit (60/min) excedido

Tirar do ar um post publicado

Move o post de published para archived, emite o evento de despublicação e PURGA a página em cache. A referência é o id (ULID) ou o slug.

NÃO é remoção: o post sai do site e continua no acervo. Post que não está publicado responde 409 — conflito de estado, não falha de servidor.

Authentication: apiKey

Name In Required Description
reference path yes —
language query no Desambigua um slug que existe em mais de um idioma.
Status Description
200 Post despublicado
401 Chave ausente ou inválida
403 Escopo insuficiente — ou, para chave pública (pk_), a origem da requisição não está na allowlist da chave. A chave é válida; o lugar de onde ela está sendo usada é que não é. Cadastre a origem no painel, em Configurações → Integrações API.
404 Não encontrado, ou não público
409 O post não está publicado (conflito de estado)
429 Rate limit (60/min) excedido

Ler o SEO de um post

A referência é o id do post (ULID) ou o slug, com a mesma resolução do GET /v1/posts/{reference}. Post de outro site responde 404.

Authentication: apiKey

Name In Required Description
reference path yes —
Status Description
200 SEO do post
401 Chave ausente ou inválida
403 Escopo insuficiente — ou, para chave pública (pk_), a origem da requisição não está na allowlist da chave. A chave é válida; o lugar de onde ela está sendo usada é que não é. Cadastre a origem no painel, em Configurações → Integrações API.
404 Não encontrado, ou não público
429 Rate limit (60/min) excedido

Atualizar o SEO de um post

Só chaves de SEO permitidas; mescladas no seo_json do post. Corpo sem nada editável é 400.

Authentication: apiKey

Name In Required Description
reference path yes —

Content types: application/json (PostSeo)

Status Description
200 SEO atualizado
400 Parâmetro inválido (ex. limit fora da faixa, cursor corrompido)
401 Chave ausente ou inválida
403 Escopo insuficiente — ou, para chave pública (pk_), a origem da requisição não está na allowlist da chave. A chave é válida; o lugar de onde ela está sendo usada é que não é. Cadastre a origem no painel, em Configurações → Integrações API.
404 Não encontrado, ou não público
429 Rate limit (60/min) excedido

Ler a fonte editável de um post (documento de blocos)

O post como o dono o edita: metadados, taxonomia (lida do SEO do post) e blocks_json EXATAMENTE como armazenado — sem renderizar e sem sanitizar. Leia, modifique e devolva pelo PATCH /v1/posts/{reference}.

Existe porque o GET /v1/posts/{reference} só devolve content_html renderizado (blocos de anúncio/cartão de produto/story saem vazios), e reconverter aquele HTML num PATCH apagaria conteúdo.

A referência é resolvida como no PATCH: id ou slug, inclusive rascunho e agendado. Campo ausente sai null; taxonomia ausente sai []. Post de outro site responde 404. A resposta sai com Cache-Control: private, no-store (pode carregar rascunho).

Authentication: apiKey

Name In Required Description
reference path yes Id do post (ULID 26 / UUID 36) ou slug.
language query no Tag BCP-47, usada SÓ para desambiguar slug. Ignorado quando a referência já é um id.
Status Description
200 Fonte editável do post
400 Parâmetro inválido (ex. limit fora da faixa, cursor corrompido)
401 Chave ausente ou inválida
403 Escopo insuficiente — ou, para chave pública (pk_), a origem da requisição não está na allowlist da chave. A chave é válida; o lugar de onde ela está sendo usada é que não é. Cadastre a origem no painel, em Configurações → Integrações API.
404 Não encontrado, ou não público
429 Rate limit (60/min) excedido
502 Falha do serviço de conteúdo, ou corpo armazenado em formato não reconhecido (nunca é devolvido como corpo vazio).

Resumo de views/visitantes do site

Acessos do site na janela pedida (humanos, sem bots). from/to (YYYY-MM-DD, UTC) têm precedência; senão o preset days (1..365, default 30). Encaminha ao canverly-analytics com o site_id da chave.

Authentication: apiKey

Name In Required Description
from query no Início da janela (YYYY-MM-DD, UTC).
to query no Fim da janela (YYYY-MM-DD, UTC, inclusivo).
days query no Preset de janela; ignorado se from/to vierem.
Status Description
200 Resumo de acessos
401 Chave ausente ou inválida
403 Escopo insuficiente — ou, para chave pública (pk_), a origem da requisição não está na allowlist da chave. A chave é válida; o lugar de onde ela está sendo usada é que não é. Cadastre a origem no painel, em Configurações → Integrações API.
429 Rate limit (60/min) excedido

Views por post/página do site (paginado)

Páginas/posts mais vistos na janela, paginado por limit/offset. Derivado do top_paths do resumo.

Authentication: apiKey

Name In Required Description
from query no —
to query no —
days query no —
limit query no 1..200. Fora da faixa é 400 — nunca cortado em silêncio.
offset query no —
Status Description
200 Página de posts mais vistos
400 Parâmetro inválido (ex. limit fora da faixa, cursor corrompido)
401 Chave ausente ou inválida
403 Escopo insuficiente — ou, para chave pública (pk_), a origem da requisição não está na allowlist da chave. A chave é válida; o lugar de onde ela está sendo usada é que não é. Cadastre a origem no painel, em Configurações → Integrações API.
429 Rate limit (60/min) excedido

Listar os leads (submissões de formulário) do site

LEADS SÃO PII. Exige o escopo opt-in leads:read; toda leitura é auditada e passa por um limite de taxa próprio, mais estrito (10/min). Recorte por form_id/status/de/ate; paginação por cursor. Um site sem o app de formulários (ou uma chave sem membership) recebe 404. Para um lead único, ver GET /v1/leads/{id}.

Authentication: apiKey

Name In Required Description
form_id query no Restringe a um formulário (id ULID/UUID).
status query no —
de query no Início do período (YYYY-MM-DD ou RFC3339).
ate query no Fim do período (inclusivo quando data).
cursor query no —
limit query no —
ordem query no —
Status Description
200 Página de leads do site
401 Chave ausente ou inválida
403 Escopo insuficiente — ou, para chave pública (pk_), a origem da requisição não está na allowlist da chave. A chave é válida; o lugar de onde ela está sendo usada é que não é. Cadastre a origem no painel, em Configurações → Integrações API.
404 Não encontrado, ou não público
422 Requisição válida mas não-processável (ex. chave antiga sem contexto de autoria)
429 Rate limit (60/min) excedido

Ler um lead único (submissão de formulário) do site

LEAD É PII. Exige o escopo opt-in leads:read; a leitura é auditada e passa pelo limite estrito (10/min). O lead é escopado ao site da chave: um id de outro inquilino (ou inexistente, ou site sem o app) recebe o MESMO 404 opaco — sem oráculo de existência. Retorna o lead na mesma forma de um item da lista.

Authentication: apiKey

Name In Required Description
id path yes Id do lead (ULID/UUID).
Status Description
200 O lead do site
401 Chave ausente ou inválida
403 Escopo insuficiente — ou, para chave pública (pk_), a origem da requisição não está na allowlist da chave. A chave é válida; o lugar de onde ela está sendo usada é que não é. Cadastre a origem no painel, em Configurações → Integrações API.
404 Não encontrado, ou não público
429 Rate limit (60/min) excedido

Exportar um recurso em massa (NDJSON ou CSV, streaming)

Transmite resource do site em format. NDJSON = um objeto por linha; CSV = colunas planas.

posts só em NDJSON (CSV é 400 — blocos aninhados) e exige TAMBÉM posts:write, com chave ck_: o export de posts é o acervo INTEIRO do dono — rascunho, agendado, publicado e arquivado (apagado não), cada linha com o corpo em blocks_json. Ver o que ainda não está no ar é a mesma permissão do GET /v1/posts?status=, e uma chave pk_ (que viaja no view-source de toda página com o widget) recebe 403.

A linha do export volta a entrar por POST /v1/import: os campos derivados (id, created_at, canonical_url, …) são descartados lá, as taxonomias do seo_json sobem para o topo, e um post com access restrito FALHA a linha — post pago não vira público por descuido de importação.

leads exige TAMBÉM leads:read (auditado + limite estrito); o CSV de leads é o do canverly-forms (colunas por formulário). analytics exporta a série temporal.

Authentication: apiKey

Name In Required Description
resource query yes —
format query no —
form_id query no leads: restringe a um formulário.
status query no leads: clean | spam | all.
de query no leads: início do período.
ate query no leads: fim do período.
from query no analytics: início da janela.
to query no analytics: fim da janela.
days query no analytics: preset de janela.
Status Description
200 Stream do recurso. Content-Type application/x-ndjson ou text/csv; Content-Disposition attachment.
400 Parâmetro inválido (ex. limit fora da faixa, cursor corrompido)
401 Chave ausente ou inválida
403 Escopo insuficiente — ou, para chave pública (pk_), a origem da requisição não está na allowlist da chave. A chave é válida; o lugar de onde ela está sendo usada é que não é. Cadastre a origem no painel, em Configurações → Integrações API.
404 Não encontrado, ou não público
429 Rate limit (60/min) excedido

Importar posts em massa a partir de NDJSON

Corpo NDJSON, uma linha = um post (mesmo schema de POST /v1/posts). Exige import E posts:write. site_id/org_id/author_id NUNCA vêm do corpo — vêm da chave (um site_id numa linha é descartado). Idempotência por conteúdo de linha (repetir não duplica). Teto 8 MiB / 1000 linhas. Só posts nesta versão (mídia por URL não é suportada).

Authentication: apiKey

Name In Required Description
resource query no —

Content types: application/x-ndjson (string)

Status Description
200 Resumo por linha
400 Parâmetro inválido (ex. limit fora da faixa, cursor corrompido)
401 Chave ausente ou inválida
403 Escopo insuficiente — ou, para chave pública (pk_), a origem da requisição não está na allowlist da chave. A chave é válida; o lugar de onde ela está sendo usada é que não é. Cadastre a origem no painel, em Configurações → Integrações API.
413 Corpo acima de 8 MiB ou mais de 1000 linhas
429 Rate limit (60/min) excedido

Liveness probe

Authentication: none declared

Status Description
200 Process is alive

Readiness probe

Authentication: none declared

Status Description
200 Ready to serve
503 Not ready (Redis/JWKS/OpenAPI not loaded)

Prometheus metrics

Authentication: none declared

Status Description
200 text/plain prometheus exposition format