Pular para o conteúdo

Referência OpenAPI: Formulários (canverly-forms)

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

  • Versão da especificação: 0.1.0
  • Total de operações: 19
  • Servidores declarados: http://127.0.0.1:8096, https://api.canverly.com/api/v1
  • Especificação publicada (YAML): /specs/forms-openapi.yaml

Formulários, leads (PII) e webhooks de saída.

Duas superfícies, dois modelos de confiança. Ler os dois antes de integrar evita a maior parte dos enganos possíveis com este serviço.

  • /v1/public/* — anônima. Nunca é exposta em api.canverly.com: o gateway exige JWT em tudo, e submissão anônima com JWT não existe. Ela é chamada serviço a serviço pelo canverly-web-public, que resolve o site a partir do Host e carimba X-Canverly-Site-Id — o mesmo desenho do /px do analytics. O serviço escuta só no loopback.
  • /v1/{forms,leads,form-webhooks} — administrativa, atrás do gateway. Exige ?site_id= presente no X-Site-Memberships que o gateway carimba a partir do JWT validado. Divergência responde 404, nunca 403: um 403 confirmaria que o recurso existe e viraria oráculo de enumeração.

Formulários é um APLICATIVO da loja, não recurso do núcleo. O site precisa ter settings.applets.forms.enabled = true (catálogo do canverly-tenancy; ligado pelo dono em PUT /v1/sites/{id}/applets/forms). O padrão é DESINSTALADO. Sem o app:

  • a superfície administrativa inteira responde 404, como se a área não existisse para aquele site;
  • a superfície pública — inclusive POST .../submit — responde o mesmo 404 opaco de formulário inexistente. Nunca um código próprio: a diferença viraria oráculo de “este site já teve formulários”.

Desinstalar não apaga nada: formulários e leads continuam no banco e voltam inteiros quando o app é reinstalado.

Quando o tenancy não responde e não há valor utilizável em cache, as duas superfícies respondem 503 — igual para todo site, então a resposta não diz nada sobre este. O lead NÃO é gravado e NÃO é dado como recebido.

Contrato de nomes de campo: docs/CONTRATO-CAMPOS.md (extensão do canverly_archive/2026-08-10-contrato-formularios-modais-anuncios.md §3).

o processo está de pé

Responde 200 mesmo com o Postgres fora — é deliberado (piso §10: a queda de outra peça não derruba este serviço). Quem pergunta “consegue trabalhar?” é /ready.

Autenticação: nenhuma declarada

Status Descrição
200 alive

o serviço consegue trabalhar

503 enquanto o banco não responde. É esta que o smoke observa.

Autenticação: nenhuma declarada

Status Descrição
200 ready
503 banco indisponível

exposição Prometheus

Autenticação: nenhuma declarada

Status Descrição
200 texto Prometheus

definição pública do formulário + token de render

Devolve só o que o render precisa. O render_token carrega o instante da emissão ASSINADO por nós — é com ele que o tempo mínimo de preenchimento é medido no servidor. Um elapsed_ms mandado pelo cliente mediria a honestidade do cliente.

Só formulário active é descrito. draft e disabled caem no 404 opaco, igual ao inexistente. Até 2026-08-10 esta rota respondia 200 para os dois — vazava a definição de um rascunho e, pior, distinguia “desabilitado” de “inexistente”, reintroduzindo o oráculo de enumeração que o resto do serviço não tem. O contrato aqui já dizia o certo; era o código que divergia (Form::visivel_publicamente).

Autenticação: nenhuma declarada

Nome Onde Obrigatório Descrição
form_id path sim ULID. Não enumerável — todo erro responde o mesmo 404 opaco.
X-Canverly-Site-Id header sim Site resolvido do Host pelo chamador interno (web-public). É a prova de origem do contrato §3.4. Ausente = 404 opaco.
Status Descrição
200 definição
404 Não encontrado. A mesma resposta para: não existe, está desabilitado, é de outro site, o id é ilegível, ou o site não tem o aplicativo Formulários instalado.
503 Não foi possível confirmar no canverly-tenancy se este site tem o aplicativo Formulários instalado (tenancy fora e sem valor utilizável em cache). Não é “desinstalado”: é “não sei”, e por isso não vira 404 — um 404 diria ao dono que os formulários dele sumiram.

recebe uma submissão anônima

A resposta nunca ecoa o payload. Um erro de campo devolve a CHAVE e um motivo de vocabulário fechado, nunca o valor recebido.

Honeypot e tempo mínimo respondem SUCESSO — e agora o lead é GRAVADO, marcado (contrato v2 §8). Dizer ao bot que ele foi pego é ensiná-lo a ajustar, então a resposta é idêntica à do lead legítimo. O que muda é do lado de dentro: o lead entra com status: "spam", não conta como lead em métrica nem notificação, não dispara webhook, tem retenção própria mais curta e pode ser devolvido ao fluxo pelo dono (POST /v1/leads/{id}/nao-e-spam).

Campo fora da definição é rejeitado, não ignorado — inclusive site_id, que nunca vem do cliente (deriva do formulário, server-side).

Autenticação: nenhuma declarada

Nome Onde Obrigatório Descrição
form_id path sim ULID. Não enumerável — todo erro responde o mesmo 404 opaco.
X-Canverly-Site-Id header sim Site resolvido do Host pelo chamador interno (web-public). É a prova de origem do contrato §3.4. Ausente = 404 opaco.

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

Status Descrição
200 recebido (ou descartado como automação — indistinguível de fora)
400 corpo inválido ou sessão do formulário expirada
404 Não encontrado. A mesma resposta para: não existe, está desabilitado, é de outro site, o id é ilegível, ou o site não tem o aplicativo Formulários instalado.
413 corpo acima do teto (64 KiB)
422 campo inválido
429 Estourou um dos limites do contrato v2 §9 — por token de render (defesa primária), por IP (secundária e não confiável), por formulário, ou por SITE em janela de hora/dia (o teto da campanha). A resposta é idêntica para todos, byte a byte. Nunca se revela qual limite bateu: saber a dimensão diria ao atacante quanto esperar e por onde distribuir.
503 O lead NÃO foi gravado. Duas causas: falha nossa, ou não deu para confirmar no tenancy se o site tem o aplicativo Formulários instalado. A resposta é a mesma nos dois casos — e igual para todo site, então não revela nada sobre este.

lista os formulários do site

Autenticação: nenhuma declarada

Nome Onde Obrigatório Descrição
site_id query sim ULID ou UUID. Conferido contra X-Site-Memberships.
Status Descrição
200 lista
404 Não encontrado. A mesma resposta para: não existe, está desabilitado, é de outro site, o id é ilegível, ou o site não tem o aplicativo Formulários instalado.
503 Não foi possível confirmar no canverly-tenancy se este site tem o aplicativo Formulários instalado (tenancy fora e sem valor utilizável em cache). Não é “desinstalado”: é “não sei”, e por isso não vira 404 — um 404 diria ao dono que os formulários dele sumiram.

cria um formulário

site_id vem da QUERY conferida contra os memberships — o corpo não participa da decisão de tenant.

Autenticação: nenhuma declarada

Nome Onde Obrigatório Descrição
site_id query sim ULID ou UUID. Conferido contra X-Site-Memberships.

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

Status Descrição
201 criado
404 Não encontrado. A mesma resposta para: não existe, está desabilitado, é de outro site, o id é ilegível, ou o site não tem o aplicativo Formulários instalado.
409 já existe formulário com este slug no site
422 definição inválida
503 Não foi possível confirmar no canverly-tenancy se este site tem o aplicativo Formulários instalado (tenancy fora e sem valor utilizável em cache). Não é “desinstalado”: é “não sei”, e por isso não vira 404 — um 404 diria ao dono que os formulários dele sumiram.

um formulário

Autenticação: nenhuma declarada

Nome Onde Obrigatório Descrição
id path sim —
site_id query sim ULID ou UUID. Conferido contra X-Site-Memberships.
Status Descrição
200 ok
404 Não encontrado. A mesma resposta para: não existe, está desabilitado, é de outro site, o id é ilegível, ou o site não tem o aplicativo Formulários instalado.
503 Não foi possível confirmar no canverly-tenancy se este site tem o aplicativo Formulários instalado (tenancy fora e sem valor utilizável em cache). Não é “desinstalado”: é “não sei”, e por isso não vira 404 — um 404 diria ao dono que os formulários dele sumiram.

atualiza (o slug não muda — ele pode estar em HTML publicado)

Autenticação: nenhuma declarada

Nome Onde Obrigatório Descrição
id path sim —
site_id query sim ULID ou UUID. Conferido contra X-Site-Memberships.

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

Status Descrição
200 atualizado
404 Não encontrado. A mesma resposta para: não existe, está desabilitado, é de outro site, o id é ilegível, ou o site não tem o aplicativo Formulários instalado.
503 Não foi possível confirmar no canverly-tenancy se este site tem o aplicativo Formulários instalado (tenancy fora e sem valor utilizável em cache). Não é “desinstalado”: é “não sei”, e por isso não vira 404 — um 404 diria ao dono que os formulários dele sumiram.

apaga o formulário, seus leads e suas entregas (cascata)

Autenticação: nenhuma declarada

Nome Onde Obrigatório Descrição
id path sim —
site_id query sim ULID ou UUID. Conferido contra X-Site-Memberships.
Status Descrição
204 apagado
404 Não encontrado. A mesma resposta para: não existe, está desabilitado, é de outro site, o id é ilegível, ou o site não tem o aplicativo Formulários instalado.
503 Não foi possível confirmar no canverly-tenancy se este site tem o aplicativo Formulários instalado (tenancy fora e sem valor utilizável em cache). Não é “desinstalado”: é “não sei”, e por isso não vira 404 — um 404 diria ao dono que os formulários dele sumiram.

página de leads (keyset)

Gera registro de auditoria em forms.lead_access_log, gravado ANTES da resposta — se o registro falhar, a leitura falha. O registro carrega o RECORTE aplicado (formulário, período, estado, ordem): auditoria que só diz “listou” não responde à pergunta que ela existe para responder.

Parâmetro desconhecido é 422, nunca ignorado: um status=lixo aceito em silêncio faria o dono concluir que não há spam.

Autenticação: nenhuma declarada

Nome Onde Obrigatório Descrição
site_id query sim ULID ou UUID. Conferido contra X-Site-Memberships.
form_id query não vazio = TODOS os formulários do site (tela de Submissões, §5)
status query não O padrão é clean e é decisão de produto: com o anti-spam gravando em vez de descartar (§8), um padrão “tudo” faria a caixa de entrada do dono virar o depósito de spam na primeira campanha. Valor desconhecido é 422.
de query não início inclusivo: AAAA-MM-DD ou instante RFC 3339. Ilegível = 422.
ate query não Fim do período. Vindo como DATA, o dia entra INTEIRO (o serviço soma 24 h e compara com <) — senão o lead das 9h do próprio dia sumiria do relatório daquele dia. Vindo como instante RFC 3339, o corte é exato.
ordem query não Sentido da ordenação por created_at. O keyset acompanha o sentido; a ordenação é do SERVIDOR porque a lista é paginada — ordenar no cliente ordenaria só a página visível.
cursor query não <nanos>|<ulid>
limit query não —
Status Descrição
200 página
404 Não encontrado. A mesma resposta para: não existe, está desabilitado, é de outro site, o id é ilegível, ou o site não tem o aplicativo Formulários instalado.
503 Não foi possível confirmar no canverly-tenancy se este site tem o aplicativo Formulários instalado (tenancy fora e sem valor utilizável em cache). Não é “desinstalado”: é “não sei”, e por isso não vira 404 — um 404 diria ao dono que os formulários dele sumiram.

exporta em CSV o recorte pedido (§5)

A exportação obedece ao MESMO filtro da listagem — os dois usam a mesma função de recorte e a mesma consulta. Exportar tudo quando a tela mostra um recorte é a diferença entre relatório e surpresa.

form_id é OPCIONAL desde o contrato v2 §5: sem ele, exporta os leads de todos os formulários do site. As colunas são a UNIÃO das definições envolvidas, e cada coluna de campo sai como <key> (<id>) — os dois identificadores numa linha só, para que um campo renomeado depois da coleta continue casando pelo id.

Colunas vêm da DEFINIÇÃO, nunca das chaves encontradas nos dados — senão a planilha mudaria de forma a cada exportação. Célula (inclusive do CABEÇALHO) que começa com =, +, -, @, tab ou CR sai prefixada por ': o Excel/Sheets do dono EXECUTA fórmula ao abrir, e o campo veio de um anônimo.

Leitura por keyset em páginas de 500, até o teto de 10 000 linhas. Auditado ANTES da resposta, com o recorte aplicado.

Autenticação: nenhuma declarada

Nome Onde Obrigatório Descrição
site_id query sim ULID ou UUID. Conferido contra X-Site-Memberships.
form_id query não vazio = todos os formulários do site
status query não O padrão é clean e é decisão de produto: com o anti-spam gravando em vez de descartar (§8), um padrão “tudo” faria a caixa de entrada do dono virar o depósito de spam na primeira campanha. Valor desconhecido é 422.
de query não início inclusivo: AAAA-MM-DD ou instante RFC 3339. Ilegível = 422.
ate query não Fim do período. Vindo como DATA, o dia entra INTEIRO (o serviço soma 24 h e compara com <) — senão o lead das 9h do próprio dia sumiria do relatório daquele dia. Vindo como instante RFC 3339, o corte é exato.
ordem query não Sentido da ordenação por created_at. O keyset acompanha o sentido; a ordenação é do SERVIDOR porque a lista é paginada — ordenar no cliente ordenaria só a página visível.
Status Descrição
200 CSV (UTF-8 com BOM, anexo)
404 Não encontrado. A mesma resposta para: não existe, está desabilitado, é de outro site, o id é ilegível, ou o site não tem o aplicativo Formulários instalado.
422 parâmetro de recorte ilegível (estado
503 Não foi possível confirmar no canverly-tenancy se este site tem o aplicativo Formulários instalado (tenancy fora e sem valor utilizável em cache). Não é “desinstalado”: é “não sei”, e por isso não vira 404 — um 404 diria ao dono que os formulários dele sumiram.

apaga um lead (direito ao esquecimento a pedido)

Autenticação: nenhuma declarada

Nome Onde Obrigatório Descrição
id path sim —
site_id query sim ULID ou UUID. Conferido contra X-Site-Memberships.
Status Descrição
204 apagado
404 Não encontrado. A mesma resposta para: não existe, está desabilitado, é de outro site, o id é ilegível, ou o site não tem o aplicativo Formulários instalado.
503 Não foi possível confirmar no canverly-tenancy se este site tem o aplicativo Formulários instalado (tenancy fora e sem valor utilizável em cache). Não é “desinstalado”: é “não sei”, e por isso não vira 404 — um 404 diria ao dono que os formulários dele sumiram.

devolve um lead marcado ao fluxo normal (“não é spam”)

Contrato v2 §8: falso positivo é reversível. Muda status para clean e zera spam_score.

O que ela NÃO faz, e a decisão está escrita: não dispara o webhook retroativamente. O destino é um CRM de terceiro que já pode ter sido reconciliado, e um lead.created chegando dias depois do fato é pior que a ausência dele. Reenvio, se houver, é ato explícito por entrega.

spam_reasons é preservado — é o histórico que explica a marcação e o que permite medir qual regra produz falso positivo.

Sem corpo, de propósito: um PATCH {"status":"…"} criaria de graça o caminho inverso (MARCAR como spam), que não existe. Marcar é decisão nossa; desmarcar é do dono.

Grava trilha de acesso a PII (unmark_spam) — se a trilha falhar, a operação falha.

Autenticação: nenhuma declarada

Nome Onde Obrigatório Descrição
id path sim —
site_id query sim ULID ou UUID. Conferido contra X-Site-Memberships.
Status Descrição
204 devolvido ao fluxo
404 Não existe, é de outro site, ou já estava limpo. Os três são o mesmo 404: a operação é idempotente e um duplo clique não pode contar dois falsos positivos na métrica.
503 Não foi possível confirmar no canverly-tenancy se este site tem o aplicativo Formulários instalado (tenancy fora e sem valor utilizável em cache). Não é “desinstalado”: é “não sei”, e por isso não vira 404 — um 404 diria ao dono que os formulários dele sumiram.

lista os webhooks do site (SEM o segredo)

Autenticação: nenhuma declarada

Nome Onde Obrigatório Descrição
site_id query sim ULID ou UUID. Conferido contra X-Site-Memberships.
Status Descrição
200 lista
404 Não encontrado. A mesma resposta para: não existe, está desabilitado, é de outro site, o id é ilegível, ou o site não tem o aplicativo Formulários instalado.
503 Não foi possível confirmar no canverly-tenancy se este site tem o aplicativo Formulários instalado (tenancy fora e sem valor utilizável em cache). Não é “desinstalado”: é “não sei”, e por isso não vira 404 — um 404 diria ao dono que os formulários dele sumiram.

cria um webhook e devolve o segredo UMA única vez

A URL é validada AQUI (https, sem userinfo, sem porta de serviço) e de novo, com resolução e aprovação de endereço, a cada entrega. O secret é gerado por CSPRNG do sistema e nunca aceito do cliente.

Autenticação: nenhuma declarada

Nome Onde Obrigatório Descrição
site_id query sim ULID ou UUID. Conferido contra X-Site-Memberships.

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

Status Descrição
201 criado
404 Não encontrado. A mesma resposta para: não existe, está desabilitado, é de outro site, o id é ilegível, ou o site não tem o aplicativo Formulários instalado.
422 URL recusada pelo guard de saída
503 Não foi possível confirmar no canverly-tenancy se este site tem o aplicativo Formulários instalado (tenancy fora e sem valor utilizável em cache). Não é “desinstalado”: é “não sei”, e por isso não vira 404 — um 404 diria ao dono que os formulários dele sumiram.

pré-visualiza o JSON exato que sairia (§6)

Aplica um mapeamento sobre o lead mais recente do formulário (ou sobre um lead de exemplo derivado da definição, quando ainda não houver nenhum) e devolve o corpo que o destino receberia.

É a MESMA função da entrega (domain::mapping_apply::aplicar): uma simulação com outro caminho de código pré-visualizaria outra coisa, e o dono descobriria a diferença no CRM do cliente dele.

O mapeamento é validado ANTES de aplicar, com a mesma regra do salvar — pré-visualizar algo que o salvar recusaria seria oferecer um caminho que não existe.

Nenhum segredo aparece aqui, e não por omissão: o contexto de substituição não tem onde guardá-los, e a resposta lista os cabeçalhos só por NOME. Usar um lead real gera registro de auditoria.

preview é segmento estático ao lado do dinâmico {id}; os ids do serviço são ULID e nenhum deles é a palavra preview.

Autenticação: nenhuma declarada

Nome Onde Obrigatório Descrição
site_id query sim ULID ou UUID. Conferido contra X-Site-Memberships.

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

Status Descrição
200 o corpo que sairia
404 Não encontrado. A mesma resposta para: não existe, está desabilitado, é de outro site, o id é ilegível, ou o site não tem o aplicativo Formulários instalado.
422 form_id ausente
503 Não foi possível confirmar no canverly-tenancy se este site tem o aplicativo Formulários instalado (tenancy fora e sem valor utilizável em cache). Não é “desinstalado”: é “não sei”, e por isso não vira 404 — um 404 diria ao dono que os formulários dele sumiram.

atualiza URL, escopo, estado, mapeamento e cabeçalho de autenticação

O corpo é COMPLETO, não parcial: mandar só enabled apagaria a URL e o mapeamento. O auth_header_value é a exceção — omiti-lo PRESERVA a credencial gravada (ver o corpo de criação). O secret de assinatura não muda aqui: trocá-lo é apagar e criar.

Autenticação: nenhuma declarada

Nome Onde Obrigatório Descrição
id path sim —
site_id query sim ULID ou UUID. Conferido contra X-Site-Memberships.

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

Status Descrição
200 atualizado
404 Não encontrado. A mesma resposta para: não existe, está desabilitado, é de outro site, o id é ilegível, ou o site não tem o aplicativo Formulários instalado.
503 Não foi possível confirmar no canverly-tenancy se este site tem o aplicativo Formulários instalado (tenancy fora e sem valor utilizável em cache). Não é “desinstalado”: é “não sei”, e por isso não vira 404 — um 404 diria ao dono que os formulários dele sumiram.

apaga o webhook e as entregas dele

Autenticação: nenhuma declarada

Nome Onde Obrigatório Descrição
id path sim —
site_id query sim ULID ou UUID. Conferido contra X-Site-Memberships.
Status Descrição
204 apagado
404 Não encontrado. A mesma resposta para: não existe, está desabilitado, é de outro site, o id é ilegível, ou o site não tem o aplicativo Formulários instalado.
503 Não foi possível confirmar no canverly-tenancy se este site tem o aplicativo Formulários instalado (tenancy fora e sem valor utilizável em cache). Não é “desinstalado”: é “não sei”, e por isso não vira 404 — um 404 diria ao dono que os formulários dele sumiram.