Pular para o conteúdo

Referência OpenAPI: Sites e organizações (canverly-tenancy)

Esta página é gerada no build a partir de api/openapi.yaml no repositório canverly-tenancy. Não edite à mão: a fonte é a especificação.

  • Versão da especificação: 0.1.0
  • Total de operações: 17
  • Servidores declarados: https://api.canverly.com/api
  • Especificação publicada (YAML): /specs/tenancy-openapi.yaml

Create a site under an org

Autenticação: nenhuma declarada

Tipos de conteúdo: application/json (object)

Status Descrição
201 site created
400 corpo inválido ou org_id que não é ULID nem UUID
403 FORBIDDEN — o org_id do CORPO não está entre as memberships validadas na borda (X-Org-Memberships). É a defesa de mass-assignment do caminho de ESCRITA: sem ela, bastaria trocar o org_id do corpo para criar site sob a empresa de outro cliente. Chamador serviço-a-serviço (sem o cabeçalho) atravessa.
409 Conflito, com code distinguindo: DOMAIN_TAKEN (domínio já pertence a outro site), SLUG_TAKEN (slug em uso) e SITE_LIMIT_REACHED (o plano da empresa não comporta mais um site licenciado — recusa deliberada, melhor não criar do que entregar site que já nasce fora do ar).
500 falha interna (detalhe redigido, §22.8)

Resolve VÁRIOS sites por id numa requisição

Substitui o fan-out de N chamadas a /v1/sites/{id} que o painel fazia para nomear os sites do usuário — 47 requisições simultâneas para quem enxerga a frota inteira, cada uma pedindo uma conexão do pool. Mesma visibilidade de cliente do GET por id: site apagado ou arquivado NÃO volta. Autorização por INTERSEÇÃO com X-Site-Memberships — id não concedido é OMITIDO, nunca recusado com 403 (recusar confirmaria a existência). Sessão de consumer recebe lista vazia: a rota é staff-only.

Autenticação: nenhuma declarada

Nome Onde Obrigatório Descrição
ids query sim CSV de ULIDs. Máximo de 200 por chamada.
Status Descrição
200 ok
400 ids ausente/vazio, com id inválido, ou acima do teto de 200

Autenticação: nenhuma declarada

Nome Onde Obrigatório Descrição
id path sim —
Status Descrição
200 site
404 not found

Atualização parcial do site (tema, settings, domínios, idiomas)

Só os campos presentes são aplicados. settings SUBSTITUI o documento inteiro (não é merge).

IMAGEM EXTERNA → CDN — depois de validado e saneado, toda URL externa de imagem do settings (logo_url, favicon_png_url, favicon_ico_url, seo.default_og_image, logo.{light,dark}.sources[].url, favicon_kit, ads.units[].creative.image_url, <img src|srcset> do about_html) é enviada ao canverly-imagem-ingest (espera até ~8s). Pronta → a resposta e o banco já trazem a URL da CDN (e logo_media_id/favicon_*_media_id recebem o asset). Não pronta, reprovação transitória ou ingest fora → a URL externa é mantida e o PATCH responde 200 do mesmo jeito; o worker troca depois. Imagem NOVA (ausente do settings gravado) que o ingest reprova DE VEZ (definitive: true — 404/410, não é imagem, grande demais, esquema proibido, host não público, SVG hostil) → 422 IMAGEM_EXTERNA_INVALIDA e NADA é gravado. URL que já estava gravada nunca recusa (o retroativo cuida dela). SANEAMENTO NA ESCRITA — settings carrega markup de terceiro, e esta é a única porta por onde ele entra. Regra, e o porquê da assimetria: (a) logo_svg e favicon_svg são SANEADOS e aceitos: remove-se <script>, atributos on*=, URLs javascript:, <foreignObject>, <use>/<image> com href externo, <link>, elementos de animação, @import/url(http…) em <style> e o prólogo XML (DOCTYPE/<!ENTITY>). O que fica guardado é o markup JÁ limpo. Existe versão limpa de um documento SVG, e recusar por causa de um DOCTYPE que o Illustrator escreve sozinho faria um envio legítimo parecer quebrado. SVG legítimo atravessa sem alteração (a operação é idempotente). Teto de 32 KiB. (b) As URLs (logo_url, favicon_png_url, favicon_ico_url, seo.default_og_image) são RECUSADAS quando não são http(s) nem caminho absoluto do próprio site — javascript:, data:, //host e qualquer valor com espaço/aspas/< > dão 400. URL é valor atômico: não existe versão limpa de javascript:alert(1), e apagar o campo em silêncio faria o dono ver “salvo” com o ícone sumido. (c) Um campo de SVG que, DEPOIS do saneamento, já não é um SVG (ou passa do teto) também é 400 — guardar “” ali seria um sucesso mentiroso. (d) settings.logo — a logo com variante CLARA e ESCURA e formatos concorrentes. Contrato canônico: canverly_archive/logo-contract-golden.json, replicado byte a byte no tenancy, no web-admin e no web-public; o SHA-256 é cravado nas três suítes, então renomear um campo em um repositório deixa os outros dois vermelhos no CI.

logo.version 1 logo.alt texto alternativo (<=200); “” = logo decorativa logo.width inteiro 8..2048, OBRIGATÓRIO quando há logo logo.height idem — reservam a caixa no render (CLS) logo.light variante padrão, OBRIGATÓRIA quando logo existe logo.dark variante do modo escuro, OPCIONAL (cai para a clara)

Cada variante é OU svg_markup (markup inline, saneado pelo MESMO saneador de logo_svg — inclusive o SVG que chega por upload de arquivo), OU sources: [{type, url}] — nunca as duas, e as duas variantes usam a MESMA forma. type vem da allowlist image/svg+xml|image/avif|image/webp|image/png|image/jpeg, no máximo um por formato e no máximo 5 por variante; a ordem canônica é do mais capaz para o menos, porque o navegador fica com o PRIMEIRO <source> que sabe decodificar. url segue a regra APERTADA do favicon (https://host/caminho ou caminho do próprio site): a logo termina num <img src> de página https, e http:// seria bloqueado como conteúdo misto — “salvo” com a logo ausente. Os campos LEGADOS logo_svg/logo_url continuam válidos e são a reserva de quem ainda não migrou. A variante escura segue a MESMA decisão de settings.customization.theme_base que a paleta do site — não uma paralela. theme_base aceita light|auto|dark|sepia|high_contrast; auto estava faltando na allowlist e era recusado com 400 apesar de o painel oferecê-lo e o site resolvê-lo (corrigido em 2026-08-10).

Autenticação: nenhuma declarada

Nome Onde Obrigatório Descrição
id path sim —

Tipos de conteúdo: application/json (object)

Status Descrição
200 site atualizado (com o markup já saneado)
400 validação. code distingue o motivo: SETTINGS_INVALID_SVG (o campo de SVG não sobreviveu ao saneamento ou passa de 32 KiB), SETTINGS_INVALID_ASSET_URL (URL de imagem fora de http(s)/caminho absoluto), além dos códigos de customization/ads/applets. Anúncio próprio (settings.ads.units[].type: "internal"): ADS_UNIT_CREATIVE_INVALID (falta campo obrigatório do criativo — target_url, width, height, image_alt ou headline), ADS_UNIT_CREATIVE_URL_INVALID (image_url/target_url fora da regra de URL de ícone: https com caminho, ou caminho do próprio site), ADS_UNIT_UNKNOWN_KIND (creative.kind fora de image|text) e ADS_UNIT_MARKUP_FORBIDDEN (a chave html NÃO existe em unidade NENHUMA: o criativo é estrutural e o markup arbitrário tem lugar próprio em settings.code_injection). ADS_UNIT_HTML_RETIRED: o tipo de unidade "html" foi REMOVIDO em 2026-08-10 — aceitava markup arbitrário do tenant renderizado na origem do site. Migre para "internal"; para código arbitrário use settings.code_injection. Logo (settings.logo): LOGO_INVALID_SHAPE, LOGO_INVALID_VERSION, LOGO_INVALID_ALT, LOGO_INVALID_SIZE (width/height ausente, não inteiro ou fora de 8..2048), LOGO_INVALID_VARIANT, LOGO_INVALID_SOURCE ({type,url} malformado ou url vazia), LOGO_UNSUPPORTED_TYPE (formato fora da allowlist), LOGO_DUPLICATE_TYPE (dois sources do mesmo formato — o segundo seria inalcançável pelo navegador), LOGO_TOO_MANY_SOURCES, LOGO_MIXED_FORM (svg_markup e sources juntos, ou variantes com formas diferentes) e LOGO_EMPTY_LIGHT (dark sem light). A URL de sources reaproveita SETTINGS_INVALID_ASSET_URL, e o svg_markup reaproveita SETTINGS_INVALID_SVG — com o caminho completo do campo na mensagem (settings.logo.dark.svg_markup). Modais (settings.modals, contrato §2): MODALS_INVALID_SHAPE (não é um array de objetos), MODALS_TOO_MANY (mais de 10 modais), MODALS_INVALID (campo obrigatório ausente, tipo errado ou fora do limite — id 1..64 e único, title 1..120 obrigatório porque é o aria-labelledby do diálogo, content obrigatório), MODALS_UNKNOWN_FIELD (chave fora do contrato, em QUALQUER nível — modal, content, rules, trigger, pages, frequency, audience. Diferente de settings.ads, que preserva chave desconhecida por causa de dado legado: settings.modals é subárvore nova e recusa, para fechar a classe “o painel grava formId e o site lê form_id”. Campo novo exige o tenancy no ar ANTES do painel), MODALS_MARKUP_FORBIDDEN (a chave html NÃO existe em nível nenhum, e content.text é TEXTO — não aceita marcação; markup arbitrário tem lugar próprio em settings.code_injection), MODALS_CONTENT_UNKNOWN_KIND (content.kind fora de form|ad|text), MODALS_CONTENT_REF_UNKNOWN (content.unit_id não referencia nenhuma unidade definida em settings.ads.units[].id), MODALS_CONTENT_INVALID_ULID (content.form_id não é ULID de 26 caracteres Crockford base32 — UUID não serve), MODALS_TRIGGER_UNKNOWN_KIND (rules.trigger.kind fora de delay|scroll|exit_intent), MODALS_TRIGGER_INVALID (seconds fora de 1..300 — o mínimo de 1s é o piso “nunca antes do LCP” — ou percent fora de 1..100), MODALS_PAGES_INVALID (padrão de rules.pages.include/exclude inválido: até 20 por lista, cada um 1..200 chars, começando com /, sem .., \, ://, caractere de controle ou mais de 5 *), MODALS_FREQUENCY_INVALID (max_shows fora de 1..50, days fora de 1..365, ou per diferente de days — days é o único valor legal hoje; aceitar session é mudança de contrato), MODALS_AUDIENCE_INVALID (devices vazio ou fora de desktop|mobile, new_visitors_only não-booleano).
404 site inexistente OU não pertencente ao chamador (indistinguíveis)
409 domínio já pertence a outro site
422 IMAGEM_EXTERNA_INVALIDA — uma imagem NOVA do settings foi reprovada de vez pelo canverly-imagem-ingest; nada foi gravado. detail cita a primeira (“e mais N” quando há outras); invalid_images lista até 20.
500 falha interna (detalhe redigido, §22.8)

Soft-delete do site (status=‘deleted’; reversível, dado preservado)

Autenticação: nenhuma declarada

Nome Onde Obrigatório Descrição
id path sim —
Status Descrição
204 apagado
400 id que não é ULID nem UUID. Chega ao handler porque o byIDSiteGuard do grupo /v1/sites deixa passar o segmento que não parseia (é ele que permite /by-host e /batch conviverem com /{id}); a validação de formato é do handler.
404 not found
500 falha interna (detalhe redigido, §22.8)

Catálogo de aplicativos com o estado de instalação DESTE site

Devolve o catálogo INTEIRO (é uma vitrine), com enabled e installed_at por app. Atrás do byIDSiteGuard: exige membership no site, como o resto de /v1/sites/{id}.

Autenticação: nenhuma declarada

Nome Onde Obrigatório Descrição
id path sim —
Status Descrição
200 catálogo + estado
404 site não encontrado

Instala ou desinstala um aplicativo neste site

Grava settings.applets.<slug>.enabled. Para app com post_type_template, materializa (ou remove) o tipo de conteúdo no authoring ANTES de gravar a flag — se o authoring estiver fora, a resposta é 502 e NADA é gravado. Depois de gravar, o full-page cache do site é invalidado (best-effort): instalar/desinstalar muda quais rotas o site tem, e o cache não descobre isso sozinho.

DESINSTALAR NUNCA APAGA DADO. O app “forms” é o caso explícito: os formulários e os leads continuam em forms.forms/forms.leads; o que some é a ÁREA (o painel esconde, o canverly-forms recusa — inclusive a rota anônima de submissão, com 404 opaco — e o web-public pula o modal de formulário). Reinstalar devolve tudo como estava.

Autenticação: nenhuma declarada

Nome Onde Obrigatório Descrição
id path sim —
slug path sim —

Tipos de conteúdo: application/json (object)

Status Descrição
200 estado do app depois da operação
400 corpo inválido, ou SETTINGS_INVALID_APPLETS
404 slug fora do catálogo (APPLET_NOT_FOUND) ou site inexistente
502 falha ao materializar o tipo de conteúdo no authoring; nada foi gravado

Gera as páginas legais do site (app “Páginas legais”)

Monta os documentos do CASO do site (settings.legal.perfil e setor) a partir dos fatos da configuração e grava cada um como PÁGINA com fields = {secao: "legal", documento_legal, ordem} — URL canônica /legal/ (índice) e /legal/<slug>. Página nova nasce RASCUNHO. Página existente no slug: rascunho do gerador é regravado (atualizada); publicada ou agendada NUNCA é sobrescrita (existente_publicada) e rascunho escrito à mão é preservado (existente_preservada) — nos dois casos só recebe os fields que a movem para /legal/ (adotada: true), e as pendências do texto novo voltam para o dono decidir. O índice lista só as páginas que existem depois do lote. O lote nunca aborta por causa de um documento.

Autenticação: nenhuma declarada

Nome Onde Obrigatório Descrição
id path sim —
X-User-Id header sim —

Tipos de conteúdo: application/json (object)

Status Descrição
200 um resultado por documento, na ordem do catálogo
400 corpo inválido ou slug desconhecido (LEGAL_UNKNOWN_DOC)
401 sem X-User-Id
404 site não encontrado
409 app Páginas legais desligado (APPLET_DISABLED)

Documentos que a geração produziria para o site

A seleção por perfil/setor SALVOS; perfil e setor na query os sobrepõem (o formulário mostra a lista antes de salvar). Não exige o app ligado — é leitura derivada do settings.

Autenticação: nenhuma declarada

Nome Onde Obrigatório Descrição
id path sim —
perfil query não —
setor query não —
Status Descrição
200 array na ordem do índice (o índice legal primeiro)
400 perfil ou setor desconhecido (LEGAL_INVALID_CASE)
404 site não encontrado

Lista TODOS os sites (cross-tenant) para o operador de plataforma

Inclui os arquivados, com motivo (status_reason) e desde quando (status_changed_at). Exige capability de operador. Aceita busca (q, ILIKE parcial em slug e primary_domain) e os filtros exatos org_id e status; sem parâmetro nenhum a resposta é a listagem completa de sempre. Os filtros entram na MESMA cláusula do keyset, então paginar com busca junto não repete nem pula linha.

Autenticação: nenhuma declarada

Nome Onde Obrigatório Descrição
q query não Termo de busca (ILIKE parcial, sem diferenciar caixa) em slug e primary_domain. Curingas de LIKE (%, _) são tratados como literais; o termo é saneado e truncado em 128 caracteres.
org_id query não filtro exato pela empresa dona (ULID base32 ou UUID)
status query não filtro exato de estado; rótulo fora do enum devolve 400 (nunca lista tudo)
cursor query não Id da última linha da página anterior (aceita ULID base32 OU UUID — o next_cursor devolvido vem em UUID). Valor que não é id devolve 400; nunca 500.
limit query não —
X-Platform-Operator header sim —
Status Descrição
200 página de sites
400 status fora do enum
403 sem capability de operador

Detalhe de um site (cross-tenant, painel do operador)

Os mesmos campos da listagem mais o ciclo de vida (status_reason, status_changed_at, previous_status), o default_language, o updated_at e o bloco domains (primário, aliases e host canônico). Lê o site em QUALQUER estado — arquivado e deletado inclusive, que é o que o cliente não enxerga. NÃO traz contagem de posts publicados: esse dado é do canverly-authoring e buscá-lo aqui criaria dependência de runtime entre os dois serviços (§10) só para preencher um número de tela.

Autenticação: nenhuma declarada

Nome Onde Obrigatório Descrição
site_id path sim ULID base32 ou UUID
X-Platform-Operator header sim —
Status Descrição
200 detalhe do site
400 site_id não é ULID nem UUID
403 sem capability de operador
404 site inexistente

Purgar um site DE VEZ (hard-delete, painel do operador)

Apaga o cadastro do site em definitivo e LIBERA o domínio e o slug, que o soft-delete mantinha reféns. O fluxo previsto é em duas mãos: o cliente faz o soft-delete (o site some do painel dele) e o operador de plataforma confirma a purga — por isso operator_admin, e não operator_read. Invalida o cache host-keyed na mesma chamada, para o site parar de servir na hora em vez de sobreviver até o próximo ciclo do resolvedor. A ação é registrada na trilha de auditoria com QUEM a executou: é cross-tenant e irreversível. NÃO apaga o ACERVO do site nos demais serviços (posts, mídia, termos): cada serviço é dono do próprio schema, e a limpeza de conteúdo é um fluxo à parte.

Autenticação: nenhuma declarada

Nome Onde Obrigatório Descrição
site_id path sim ULID base32 ou UUID
X-Platform-Operator header sim —
Status Descrição
204 site purgado; domínio e slug liberados
400 site_id não é ULID nem UUID
403 sem capability de operador (a leitura não basta aqui)
404 site inexistente
500 falha ao apagar

Despublica o site (painel do operador)

Autenticação: nenhuma declarada

Nome Onde Obrigatório Descrição
id path sim —
X-Platform-Operator header sim —
Status Descrição
200 estado resultante
403 operator_read não escreve

Arquiva o site (painel do operador)

Autenticação: nenhuma declarada

Nome Onde Obrigatório Descrição
id path sim —
X-Platform-Operator header sim —
Status Descrição
200 estado resultante
403 operator_read não escreve

Restaura o site (painel do operador)

Autenticação: nenhuma declarada

Nome Onde Obrigatório Descrição
id path sim —
X-Platform-Operator header sim —
Status Descrição
200 estado resultante
403 operator_read não escreve

Pendências de um site (detalhe atrás do sininho)

Lê SÓ o cache. Autorização pelo mesmo byIDSiteGuard das demais rotas por id: quem não é membro do site (nem tem a permissão de org equivalente) recebe 404 OPACO, idêntico ao de um site inexistente — um 403 confirmaria a existência do site.

Autenticação: nenhuma declarada

Nome Onde Obrigatório Descrição
id path sim —
Status Descrição
200 estado de saúde do site (cached=false quando nunca verificado)
400 id não é ULID nem UUID
404 site inexistente OU não pertencente ao chamador (indistinguíveis)
503 cache de saúde não wired

O host aponta para a plataforma? (A, AAAA ou CNAME)

Compara as TRÊS formas de apontamento que a plataforma aceita, porque as três são configurações válidas que a própria tela ENSINA: registro A no apex, AAAA no apex e CNAME de subdomínio para <slug>.canverly.com. ANTES comparava SÓ o registro A — todo subdomínio corretamente apontado por CNAME e todo apex só-IPv6 eram reportados como quebrados, e o veredito CONTRADIZIA o da varredura horária de saúde (que já comparava os três) sobre o mesmo domínio. A comparação agora é a MESMA função (application.LookupDNS), não uma cópia. Autorização pelo byIDSiteGuard do grupo /v1/sites: quem não é membro do site recebe 404 opaco antes deste handler rodar.

Autenticação: nenhuma declarada

Nome Onde Obrigatório Descrição
id path sim —
host query sim —
Status Descrição
200 veredito + registros publicados
400 id inválido ou host ausente
404 site inexistente OU não pertencente ao chamador (indistinguíveis)
500 endereço da frota não configurado (falha alto em vez de rotular tudo errado)