Páginas

https://api.pages.asender.net · API key / sessão · 156 operações

Mais guias desta API: Páginas: o JSON do documento v2

Pages API

A API da ferramenta Páginas do Asender: constrói a landing page, publica, e mede o que acontece nela.

Vai criar ou editar o CONTEÚDO de páginas (você ou um agente de IA)? Leia Páginas: o JSON do documento v2: modelo mental, todos os campos de todos os itens, erros de validação, mídia, fluxos seguros (rascunho/versão/publicação), receitas completas e um prompt pronto. É uma ferramenta servida por dois processos — a

autoria (o construtor: páginas, formulários, mídia, domínios, links curtos, marca, geração por IA, testes A/B) e o runtime (o que serve a página publicada ao visitante e a mede: captura de lead, analytics, pixels, session replay). Para quem integra, é uma API só, num endereço só.

Por que dois endereços, e onde passa a fronteira. A fronteira interna autoria/runtime não é sua — você não precisa saber por onde ela passa. A regra prática é simples: tudo o que você administra (páginas, formulários, pixels, relatórios) fala com api.pages.asender.net; tudo o que o visitante da página publicada dispara (o formulário embutido, o pixel, o link curto) acontece em sites.asender.net, e você quase nunca chama isso à mão — é a página servida que chama. sites.asender.net não é uma API: é onde a página publicada é servida. Se o runtime cair, o que quebra é a analytics de Páginas — não o CRM, não o e-mail, não a plataforma (ADR-0037).

Autenticação

Páginas tem três públicos, e cada um usa uma credencial diferente. A spec diz qual vale em cada rota.

Sessão (OIDC) — a autoria e a leitura de métricas

O construtor e as telas de analytics/pixels/replay são operados por um usuário logado. A credencial é a sessão OIDC (OAuth 2.1 + PKCE, emitida por https://auth.asender.net). O token vai em Authorization: Bearer ou no cookie asender_session (o caminho dos frontends da casa):

Authorization: Bearer <token-de-sessão>
X-Asender-Tenant: <slug-da-conta-ativa>

O X-Asender-Tenant é uma preferência — diz em qual das suas contas você está operando. Ele não autoriza nada: o acesso é decidido no servidor pelo PEP do produto pages, e o tenant efetivo é sempre resolvido do token. Sessão ausente ou inválida responde 401; a sessão de um usuário sem o produto pages na conta responde 403.

API key — integração server-to-server (formulários, leads e páginas)

Um sistema seu — ou um agente de IA — pode administrar formulários, enviar leads e criar e manter páginas sem sessão de usuário, com uma API key da conta (sk_live_…/sk_test_…). A chave vai em Authorization: Bearer e é validada a cada chamada — chave revogada para de valer na hora (sem cache). A conta é resolvida da chave; um tenant_id no corpo é ignorado (piso 3). Em Páginas a chave vale só nas rotas abaixo, e cada uma exige um escopo (deny-default: rota fora da tabela, ou chave sem o escopo, responde 403):

EscopoRotas
forms:readGET /v1/formularios (aceita ?external_id=), GET /v1/formularios/{id}, GET /v1/formularios/externo/{external_id}
pages:readGET /v1/paginas, GET /v1/paginas/{id}, GET /v1/paginas/{id}/rascunho, GET /v1/paginas/{id}/seo, GET /v1/midias, GET /v1/dominios (lista os domínios da conta; o padrão vem primeiro, com padrao: true)
pages:writecriar e editar sem publicar: POST /v1/paginas, POST /v1/paginas/importar, POST /v1/paginas/{id}/versoes, PUT/DELETE /v1/paginas/{id}/rascunho, PUT /v1/paginas/{id}/seo, POST /v1/paginas/{id}/midias/copiar, PUT /v1/paginas/{id}/dominio (move a página de domínio e/ou caminho), POST /v1/paginas/lote (duplicar, mudar_dominio), POST /v1/midias, POST /v1/midias/de-url
pages:publishpublicar e despublicar — o ato visível ao público: POST /v1/paginas/{id}/publicar (com versao obrigatória) e POST /v1/paginas/lote (publicar, despublicar)
forms:writePOST /v1/formularios, PUT /v1/formularios/{id}, PUT /v1/formularios/externo/{external_id}, DELETE /v1/formularios/{id}, e o envio de lead: POST /v1/leads, POST /v1/forms/{formID}/submissions, POST /v1/forms/externo/{externalID}/submissions

Os escopos não se implicam (pages:write não lê nem publica). Todo o resto (editar slug/título, A/B, roteamento, destinos, demais rotas de domínios, apagar mídia, SEO do site, analytics…) é só sessão: com chave responde 401/403. Contra sobrescrever trabalho de pessoas, o PUT …/rascunho por chave exige ?base= (428 sem ele; 409 com os dois lados preservados) e a escrita é limitada por chave (60/min; publicar 20/min). O guia completo para agentes, com o contrato do documento, está em Documento de página v2. Passo a passo em Integrar um formulário externo.

Como obter uma chave. As API keys são emitidas pela Plataforma (POST https://api.asender.net/api/tenants/{id}/api-keys, ver a Platform API) ou pela tela de API keys do painel, informando os scopes (forms:read, forms:write, pages:read, pages:write, pages:publish). Use sk_test_… fora de produção.

Sem credencial — o visitante da página publicada

O snippet de formulário embutido em site de terceiro (POST /f/{formID}), a coleta de pixel (/pixel.js, POST /collect), a gravação de session replay (POST /replay) e o link curto (GET /r/{slug}) são anônimos: quem chama é o navegador de um visitante, não um integrador. Quando há alguma credencial, ela é a do pixel — um token assinado que a página servida já carrega, enviado no corpo — nunca uma chave sua. Esses endpoints defendem-se por origem (a allowlist do formulário ou do pixel) e por rate-limit, não por sessão.

Erros

O envelope de erro difere por processo, e a spec diz a verdade sobre cada um.

Autoria (api.pages.asender.net, control plane) responde com a chave erro:

{ "erro": "já existe uma página no endereço \"promo\"" }

Runtime (captura, envio de lead por chave, analytics/pixels/replay) responde no formato RFC 7807-compatível da casa, com error.message:

{ "error": { "message": "credencial ausente ou inválida" } }

Os códigos e quando cada um aparece:

HTTPquando
400corpo inválido, parâmetro de query malformado, submissão recusada pela validação do formulário
401sessão/credencial ausente ou inválida (chave revogada, conta inativa)
403produto pages ausente na conta (autoria); chave sem o escopo da rota (a chave não tem o escopo forms:write); origem não autorizada (snippet/coleta)
404recurso inexistente ou fora do escopo da credencial (não revela existência em outra conta)
409conflito de unicidade (slug/nome/external_id já em uso, experimento já rodando)
413upload de mídia grande demais
422regra de negócio (título ausente, domínio não verificado, peso fora de 0–100); Idempotency-Key já usada com outro corpo
429excesso nas rotas públicas de coleta/replay (rate-limit por credencial × IP) e no envio de lead por chave (300/min por conta) — traz Retry-After
502a captura ou a geração por IA não pôde completar agora
503dependência indisponível ou recurso desligado neste ambiente (IA, importação por URL, validação da chave) — tente de novo com backoff

Paginação e janelas

Páginas não usa cursor opaco (diferente da Mail API). As coleções de autoria (páginas, formulários, redirects, pixels) devolvem a lista sob uma chave nomeada ({"paginas":[…]}, {"formularios":[…]}, …); onde há muito volume, um ?limite=N recorta. Os relatórios de analytics trabalham por janela de tempo: ?de=…&ate=… em RFC3339; ausente usa os últimos 7 dias, e o teto é de 92 dias.


Autoria — o ciclo de vida de uma página

A base de tudo nesta seção é https://api.pages.asender.net, com sessão.

Criar, salvar versão, publicar

Uma página nasce com um título e (opcionalmente) um slug e um dominio próprio. Salvar o trabalho do editor cria uma versão; publicar promove uma versão ao ar.

# 1) criar a página
curl -X POST 'https://api.pages.asender.net/v1/paginas' \
  -H 'Authorization: Bearer <token-de-sessão>' \
  -H 'X-Asender-Tenant: <slug-da-conta>' \
  -H 'Content-Type: application/json' \
  -d '{ "titulo": "Lançamento", "slug": "promo" }'
# → 201 { "id": "…", "slug": "promo", "titulo": "Lançamento", … }

# 2) salvar uma versão (auto-save do editor)
curl -X POST 'https://api.pages.asender.net/v1/paginas/<id>/versoes' \
  -H 'Authorization: Bearer <token-de-sessão>' \
  -H 'X-Asender-Tenant: <slug-da-conta>' \
  -H 'Content-Type: application/json' \
  -d '{ "template": { "versao": 2, "secoes": [ { "id": "s1", "colunas": [ { "id": "s1c1", "itens": [
        { "id": "t1", "tipo": "titulo", "props": { "texto": "Olá", "nivel": 1 } } ] } ] } ] } }'
# → 201 { "versao": 3 }

# 3) publicar (sem corpo, publica a última salva)
curl -X POST 'https://api.pages.asender.net/v1/paginas/<id>/publicar' \
  -H 'Authorization: Bearer <token-de-sessão>' \
  -H 'X-Asender-Tenant: <slug-da-conta>' \
  -H 'Content-Type: application/json' \
  -d '{ "versao": 3 }'
# → 200 { "versao_publicada": 3 }

Regras que valem antes de integrar:

Domínios próprios

Adicionar um domínio devolve o registro TXT de posse a publicar no DNS; depois você pede a verificação.

curl -X POST 'https://api.pages.asender.net/v1/dominios' \
  -H 'Authorization: Bearer <token-de-sessão>' \
  -H 'X-Asender-Tenant: <slug-da-conta>' \
  -H 'Content-Type: application/json' \
  -d '{ "host": "lp.empresa.com.br" }'
# → 201 { "id":"…", "host":"lp.empresa.com.br", "registro_txt":"asender-verify=…", "verificado":false }

curl -X POST 'https://api.pages.asender.net/v1/dominios/<id>/verificar' \
  -H 'Authorization: Bearer <token-de-sessão>' \
  -H 'X-Asender-Tenant: <slug-da-conta>'
# → 200 { …, "verificado": true }   (422 se o TXT ainda não propagou; 503 se o DNS não respondeu)

Domínio padrão e mover uma página de endereço

Toda conta ganha, automaticamente, um domínio padrão em apages.xyz. GET /v1/dominios (sessão ou chave pages:read) lista os domínios com o padrão primeiro e "padrao": true nele.

PUT /v1/paginas/{id}/dominio (sessão ou chave pages:write) move a página:

curl -X PUT 'https://api.pages.asender.net/v1/paginas/<id>/dominio' \
  -H 'Authorization: Bearer sk_live_…' \
  -H 'Content-Type: application/json' \
  -d '{ "dominio_id": "dom_01J9XYZ", "caminho": "meu-slug" }'
# → 200 { "pagina_id":"…", "dominio_id":"dom_01J9XYZ", "dominio":"minhaconta.apages.xyz",
#         "caminho":"meu-slug", "url_publica":"https://minhaconta.apages.xyz/meu-slug",
#         "alterado":true, "publicada":true,
#         "redirecionamento": { "criado":true, "de":"https://sites.asender.net/slug-antigo" } }
StatusQuando
400corpo inválido
401 / 403sem credencial / chave sem pages:write
404página ou domínio inexistente ou de outra conta (não revela existência)
409motivo: caminho_em_uso (endereço de outra página) ou pagina_inicial (a página é a home do domínio de origem)
422motivo: dominio_nao_verificado ou caminho_invalido

Ações em massa (publicar, despublicar, duplicar, mudar domínio)

POST /v1/paginas/lote (sessão ou chave: publicar/despublicar exigem pages:publish; duplicar/mudar_dominio, pages:write) aplica uma ação a até 100 páginas. Cada página é processada de forma independente: não há tudo-ou-nada, e a resposta diz o desfecho de cada uma.

curl -X POST 'https://api.pages.asender.net/v1/paginas/lote' \
  -H 'Authorization: Bearer <token-de-sessão>' \
  -H 'X-Asender-Tenant: <slug-da-conta>' \
  -H 'Content-Type: application/json' \
  -d '{ "acao": "publicar", "ids": ["pag_01","pag_02"], "rascunho": "pular" }'
# → 200 { "lote_id":"lot_…", "acao":"publicar", "total":2, "ok":1, "puladas":1, "erros":0,
#         "resultados":[ {"id":"pag_01","status":"ok","versao":3},
#                        {"id":"pag_02","status":"pulada","motivo":"rascunho_mais_novo"} ] }

Formulários — o contrato de campos

Um formulário declara seus campos; a captura recusa submissão de campo não declarado (é assim que a base do CRM não fica suja com telefone/Telefone/ tel). O origens[] é a allowlist do modo snippet (CORS por formulário).

curl -X POST 'https://api.pages.asender.net/v1/formularios' \
  -H 'Authorization: Bearer <token-de-sessão>' \
  -H 'X-Asender-Tenant: <slug-da-conta>' \
  -H 'Content-Type: application/json' \
  -d '{
    "nome": "Contato",
    "campos": [
      { "nome": "email", "rotulo": "E-mail", "tipo": "email", "obrigatorio": true },
      { "nome": "nome",  "rotulo": "Nome",   "tipo": "texto", "obrigatorio": false }
    ],
    "origens": ["https://empresa.com.br"],
    "mensagem_pos_envio": "Recebemos, obrigado."
  }'
# → 201 { "id": "…", "nome": "Contato", "campos": [ … ] }

Tipos de campo: texto, texto_longo, email, telefone, numero, booleano, opcao (com opcoes[]), data, url, oculto e outros (ver o schema Campo). Um sistema externo também administra formulários por chave, amarrando-os ao seu próprio id (external_id) — veja Integrar um formulário externo. GET /v1/formularios/{id} devolve também o snippet de embed.

Fontes da página

O documento v2 aceita, no estilo da página, duas famílias de fonte: estilo.fonte_titulos (títulos h1–h6) e estilo.fonte_texto (texto corrido). Vazio ou ausente herda do kit de marca; senão o valor tem de ser um destes ids, exatamente assim (minúsculas, sem espaços):

comfortaa, urbanist, montserrat, inter, poppins, open-sans, nunito, playfair-display, merriweather, dm-sans, manrope, raleway.

As fontes são auto-hospedadas pelo runtime (/_f/…, cache de um ano): a página publicada não chama nenhum serviço de terceiros. Qualquer outro valor ("Inter", "arial", um nome com espaço, texto com CSS) é recusado na gravação com 422 e código ErrEstiloInvalido, com estilo.fonte_titulos ou estilo.fonte_texto na mensagem de erro. A lista também é o enum desses campos no schema DocV2_estiloPagina da especificação.

{ "versao": 2, "estilo": { "fonte_titulos": "playfair-display", "fonte_texto": "inter" }, "secoes": [ … ] }

Testes A/B e roteamento

Sob /v1/paginas/{id} moram duas mecânicas de desvio, exclusivas entre si (ADR-0014):


Captura de lead — os modos de submissão

Um formulário recebe submissão de três formas, e todas caem no mesmo pipeline (validação do contrato de campos → antispam → grava → outbox para o CRM).

Modo API (server-to-server, com API key)

Para o seu backend enviar um lead. Base: https://api.pages.asender.net (sites.asender.net serve igual). Exige chave com forms:write. O sucesso é 201 (novo) ou 200 (deduplicado); o corpo traz duplicada.

curl -X POST 'https://api.pages.asender.net/v1/forms/<form_id>/submissions' \
  -H 'Authorization: Bearer sk_live_xxxxxxxxxxxxxxxxxxxx' \
  -H 'Content-Type: application/json' \
  -d '{
    "campos": { "email": "cliente@exemplo.test", "nome": "Fulana" },
    "origem": { "utm_source": "google", "utm_medium": "cpc" }
  }'
# → 201 { "ok": true, "lead_id": "…", "duplicada": false }

POST /v1/leads é o mesmo, com o form_id no corpo em vez de no path, e POST /v1/forms/externo/{externalID}/submissions acha o formulário pelo seu external_id. O corpo completo (id_externo, consentimento, ocorrido_em, origem.url/utm_*) está no passo 3 da integração externa. Os campos são explícitos (campos é um mapa nome→valor) — nada de bind do corpo inteiro (anti mass assignment, §37).

Modo snippet (embutido, anônimo)

O snippet gerado pelo formulário posta em POST /f/{formID} sem credencial: a conta sai do próprio formulário, e a origem tem de estar na allowlist dele (senão 403). Um <form> nativo recebe a tela de agradecimento; um XHR recebe JSON. Sempre responde 200 — inclusive quando a submissão é classificada como spam: aceitar e descartar são indistinguíveis, de propósito (devolver 403 ensinaria o bot a ajustar até passar).

Modo página nativa

Quando o formulário faz parte de uma página servida pelo runtime, o POST da própria página (sites.asender.net/…) alimenta o mesmo pipeline. Nada a integrar — é a página publicada funcionando.

Não-produção nunca entrega efeito externo real (piso 18). O lead capturado segue para o CRM por outbox; use @exemplo.test (ou os TLDs reservados .test/.invalid/.example) em fixtures e testes, nunca um e-mail de pessoa real ou o domínio de um cliente.

Integrar um formulário externo (ex.: blog)

Caso: o seu blog (ou outro sistema) tem um formulário próprio e você quer que cada envio vire lead no Asender — contato no CRM, mensagem na caixa de atendimento, e marketing quando houver opt-in. Tudo server-to-server, em https://api.pages.asender.net, com uma API key. O fluxo é: chave → upsert do formulário → enviar leads.

Não-produção nunca entrega efeito externo real (piso 18). Fora de prd use chave sk_test_… e e-mails sintéticos @exemplo.test (ou os TLDs reservados .test/.invalid/.example) — nunca e-mail de pessoa real nem o domínio de um cliente. Os exemplos abaixo já seguem isso.

1. Crie a chave com os escopos certos

No painel (API keys) ou pela Plataforma:

curl -X POST 'https://api.asender.net/api/tenants/<tenant_id>/api-keys' \
  -H 'Authorization: Bearer <token-de-sessão>' \
  -H 'Content-Type: application/json' \
  -d '{ "name": "blog-formularios", "scopes": ["forms:write", "forms:read"] }'
# → o segredo sk_… aparece UMA vez

2. Faça o upsert do formulário por external_id

PUT /v1/formularios/externo/{external_id} cria o formulário se não existe (201) e o atualiza se já existe (200). É idempotente: rodar de novo com o mesmo corpo não muda nada — pode ir no deploy do blog. Você não precisa guardar o id do Asender; o seu external_id (1–128 caracteres, A-Z a-z 0-9 . _ : -, único na conta) basta.

curl -X PUT 'https://api.pages.asender.net/v1/formularios/externo/blog-contato' \
  -H "Authorization: Bearer $ASENDER_API_KEY" \
  -H 'Content-Type: application/json' \
  -d '{
    "nome": "Contato do blog",
    "exige_consentimento": true,
    "texto_consentimento": "Concordo em receber comunicações",
    "base_legal": "consentimento",
    "campos": [
      { "nome": "email",    "rotulo": "E-mail",   "tipo": "email",  "obrigatorio": true },
      { "nome": "nome",     "rotulo": "Nome",     "tipo": "texto",  "obrigatorio": true },
      { "nome": "empresa",  "chave": "empresa",    "rotulo": "Empresa", "tipo": "texto" },
      { "nome": "interesse","rotulo": "Interesse", "tipo": "opcao",  "opcoes": ["produto", "parceria"] }
    ]
  }'
# → 201 { "id": "…", "external_id": "blog-contato", "nome": "Contato do blog", "campos": [ … ] }
# → (rodando de novo) 200 { … o mesmo formulário … }

O PUT substitui a definição inteira: o que não vier no corpo deixa de existir. Mapeamento dos campos do formulário do blog para o tipo do Asender:

Campo do formulário do blogtipo no AsenderObservação
texto (input text)texto
área de texto (textarea)texto_longo
e-mailemailalimenta o e-mail do contato
telefone (tel)telefonealimenta o telefone do contato
númeronumero
caixa de seleção única (checkbox)booleanovalor_marcado define o valor enviado quando marcada
select / radioopcaoliste os valores em opcoes[]
datadata
hiddenoculto
URLurl

Outras propriedades do campo:

Só os campos declarados são aceitos no envio: campo desconhecido é recusado, e é assim que a base do CRM não fica suja.

Para criar sem upsert, POST /v1/formularios aceita o mesmo corpo com external_id e, opcionalmente, o header Idempotency-Key (1–255 caracteres): repetir a mesma chave com o mesmo corpo devolve 201 com o mesmo formulário e Idempotent-Replayed: true; a mesma chave com corpo diferente dá 422. Para conferir: GET /v1/formularios/externo/blog-contato ou GET /v1/formularios?external_id=blog-contato. DELETE /v1/formularios/{id} apaga (lógico) e libera o external_id.

3. Envie os leads, server-to-server

A cada envio no blog, o seu servidor chama (nunca o navegador do visitante):

curl -X POST 'https://api.pages.asender.net/v1/forms/externo/blog-contato/submissions' \
  -H "Authorization: Bearer $ASENDER_API_KEY" \
  -H 'Content-Type: application/json' \
  -d '{
    "campos": { "email": "fulana@exemplo.test", "nome": "Fulana de Tal", "empresa": "Exemplo Ltda", "interesse": "parceria" },
    "id_externo": "blog-sub-48213",
    "consentimento": true,
    "ocorrido_em": "2026-10-08T14:03:00Z",
    "origem": {
      "url": "https://blog.exemplo.test/post/lancamento",
      "utm_source": "newsletter", "utm_medium": "email", "utm_campaign": "lancamento"
    }
  }'
# → 201 { "ok": true, "lead_id": "…", "duplicada": false, "id_externo": "blog-sub-48213" }

(Se preferir o id do Asender: POST /v1/forms/{formID}/submissions ou POST /v1/leads com form_id no corpo — mesmo corpo.)

CampoO que é
camposmapa nome→valor dos campos declarados no passo 2
id_externoid desta submissão no seu sistema; é a chave de deduplicação (por formulário). Até 255 caracteres. O header Idempotency-Key serve no lugar
consentimentotrue equivale a campos.consentimento = "on"; obrigatório se o formulário exige_consentimento
ocorrido_emquando o visitante enviou (RFC 3339). Fora da janela — mais de 5 min no futuro ou mais de 365 dias no passado — é recusado (400)
origemurl (a página do blog, http/https, até 2048), utm_source, utm_medium, utm_campaign, utm_term, utm_content, referrer

Retry seguro. Reenvie com o mesmo id_externo quantas vezes precisar: o primeiro vira 201; os seguintes dão 200 com "duplicada": true e o mesmo lead_id — nada é gravado duas vezes. Trate 200 e 201 como sucesso.

Backoff. Em 429 espere Retry-After segundos; em 502/503 (core ou registro indisponível) espere com backoff exponencial (ex.: 1 s, 2 s, 4 s… com jitter) e reenvie com o mesmo id_externo. Não retente 400/401/403/404 — são determinísticos (corpo, chave, escopo, formulário). O lead já aceito é durável: o CRM e a caixa de atendimento recebem de forma assíncrona (outbox), com retry próprio.

4. Onde os leads aparecem

5. Erros, escopos, limites e LGPD

Envelopes (a spec diz a verdade por processo):

Autoria (/v1/formularios…):

{ "erro": "a chave não tem o escopo forms:write" }

Envio de lead (/v1/leads, /v1/forms/…):

{ "error": { "message": "o formulário exige consentimento" } }
HTTPSignificado nesta integração
400corpo inválido; campo não declarado/inválido; consentimento exigido ausente; ocorrido_em fora da janela
401chave ausente, inválida, revogada ou conta inativa
403a chave não tem o escopo da rota (forms:read/forms:write)
404formulário inexistente ou de outra conta (não revela existência)
409external_id ou nome já em uso
422definição de formulário inválida; Idempotency-Key já usada com outro corpo
429limite excedido — respeite Retry-After
503não deu para validar a chave agora — tente de novo com backoff

Quickstart — integração externa (SDK JS)

Você tem um formulário numa página sua (ex.: um site Next próprio) e quer que as submissões cheguem ao Asender — no CRM e no marketing. Não precisa falar HTTP na mão: o SDK @asender/pages-sdk faz a captura pelos dois caminhos, cada um com a defesa certa.

npm install @asender/pages-sdk
modoquandoendpointcredencial
server-to-serverseu backend/route-handler envia o leadPOST /v1/leadsAPI key sk_*, lida de env no servidor
cliente anônimoo browser do visitante posta diretoPOST /f/{formID}nenhuma — a defesa é a allowlist de origem do formulário
### A API key sk_* é SEGREDO — nunca vai para o browser A chave é validada a cada chamada e resolve a conta sozinha (um tenant_id no corpo é ignorado). Ela mora só no servidor — variável de ambiente, secret manager — e jamais em bundle de front, NEXT_PUBLIC_* ou localStorage. No SDK, o modo de browser está num entry separado (@asender/pages-sdk/client) que não importa o caminho da chave: por construção, o bundle do navegador não alcança o segredo. Vazou uma chave? Revogue no painel — ela para de valer na hora.

Gerar a API key

As chaves são emitidas pela Plataforma — pela tela de API keys do painel ou por POST https://api.asender.net/api/tenants/{id}/api-keys (ver a Platform API). Use sk_test_… fora de produção e sk_live_… só em produção.

Server-side — route handler do Next (o caso "Bioma")

O formulário da sua página posta no seu endpoint; o seu endpoint (no servidor) chama o Asender com a chave. O visitante nunca vê a chave.

// app/api/lead/route.ts — RODA NO SERVIDOR
import { createServerClient, AsenderHttpError } from "@asender/pages-sdk";

const pages = createServerClient({ apiKey: process.env.ASENDER_API_KEY! });

export async function POST(req: Request) {
  const { email, nome, empresa } = await req.json();
  try {
    const r = await pages.captureLead({
      formId: "form_da_bioma",
      campos: { email, nome, empresa },           // "empresa" não-mapeado vira custom field
      origem: { utm_source: "bioma", utm_medium: "site" },
    });
    return Response.json({ ok: true, lead_id: r.lead_id, duplicada: r.duplicada });
  } catch (e) {
    if (e instanceof AsenderHttpError) return Response.json({ erro: e.message }, { status: e.status });
    throw e; // rede/validação — não engole
  }
}

O corpo enviado ao runtime é { form_id, campos, origem } — campos é um mapa nome→valor, explícito (anti mass assignment). Sucesso é 201 (novo) ou 200 (deduplicado); r.criado traduz isso e r.duplicada vem do corpo.

Client-side — snippet anônimo (sem chave)

Quando o formulário pode postar direto do browser, use o entry /client. Não há API key: autorize a origem da sua página na allowlist do formulário (no painel de Páginas) — o runtime recusa origem fora da lista com 403.

import { createBrowserClient } from "@asender/pages-sdk/client";

const pages = createBrowserClient();

form.addEventListener("submit", async (ev) => {
  ev.preventDefault();
  const campos = Object.fromEntries(new FormData(form) as any) as Record<string, string>;
  await pages.submitForm({ formId: "form_da_bioma", campos });
  // /f/{formID} SEMPRE responde 200 (aceito ou spam, indistinguível, por desenho)
});

Como os campos viram destino

O que você manda em campos é preservado inteiro. Para cada campo, o formulário aplica seu mapa de destino: um campo mapeado para um alvo canônico (email | telefone | nome) alimenta o campo estruturado do CRM/marketing; um campo não-mapeado vira custom field com o nome do campo — nunca é descartado. Toda submissão é gravada no histórico de coleta para auditoria, antes de seguir aos destinos marcados (CRM, marketing, webhook).

Erros e retry

O SDK não engole erro: AsenderValidationError (entrada inválida, antes da rede), AsenderHttpError (.status, mensagem já extraída do envelope) e AsenderNetworkError (.cause). Ele re-tenta com backoff só em falha de rede e em 429/502/503; nunca em 4xx determinístico (400/401/403/404).


Medir o publicado

Base: https://api.pages.asender.net, com sessão (é o painel quem lê).

Analytics

Relatórios por janela (?de=&ate= RFC3339, padrão 7 dias, teto 92):

curl 'https://api.pages.asender.net/v1/analytics/overview?pagina=<id>&de=2026-08-01T00:00:00Z&ate=2026-08-29T00:00:00Z' \
  -H 'Authorization: Bearer <token-de-sessão>' \
  -H 'X-Asender-Tenant: <slug-da-conta>'

Pixels

O control plane dos pontos de coleta: GET/POST /v1/pixels (a criação devolve o snippet pronto com a credencial assinada), …/domains (as origens autorizadas a coletar) e …/conversions (as conversões rastreadas).

curl -X POST 'https://api.pages.asender.net/v1/pixels' \
  -H 'Authorization: Bearer <token-de-sessão>' \
  -H 'X-Asender-Tenant: <slug-da-conta>' \
  -H 'Content-Type: application/json' \
  -d '{ "nome": "Landing principal", "ativo": true }'
# → 201 { "id":"…", "nome":"Landing principal", "ativo":true, "snippet":"<script …></script>" }

Coleta e session replay (público)

GET /pixel.js serve o script; POST /collect ingere um lote de eventos de comportamento; POST /replay grava uma jornada. Os três rodam no runtime (sites.asender.net), são anônimos e trazem a credencial do pixel no corpo (o token assinado que a página carrega). São o que o pixel.js chama — você não os invoca à mão. A leitura das jornadas é autenticada: GET /v1/replay/sessions e …/sessions/{id}/events, na base de autoria.


A referência completa de cada rota — parâmetros, corpo, respostas e um curl pronto — está logo abaixo, gerada do código e da spec.

Superfície interna (não pública). O runtime e a autoria conversam por um canal interno de control plane, reservado aos serviços da casa: a definição do formulário, a página publicada para render, o sitemap, a autorização de TLS de domínio próprio e a descarga de contagens. Ele não é documentado aqui porque não é uma superfície de integração: use a sessão (para administrar) ou a API key (para enviar leads).

Referência

Toda operação registrada no código, com o que faz, o que recebe e o que devolve.

_f

GET /_f/{arquivo} sem schema na spec

304 quando o `If-None-Match` bate; senão o bundle, com cache de uma hora e `X-Content-Type-Options: nosniff`.

Onde é usadarota /chat.js do runtime.
Entradasw; r.
Efeitosnenhum — é leitura de bytes já em memória. Não há CORS aqui, e é de propósito. Um <script src> não precisa de CORS para executar, e liberar Access-Control-Allow-Origin: * neste arquivo só abriria a leitura do conteúdo por fetch de qualquer origem sem ganho nenhum. Quem precisa de CORS é a API do chat, e lá a origem é conferida contra a chave de site — não liberada em *. ifNoneMatchCasa (comparação FRACA, RFC 9110 §13.1.2), não ==: a borda comprime este script on-the-fly e rebaixa o ETag forte para W/"…" na resposta — o navegador devolve esse W/ na revisita, e == contra o forte nunca batia (achado em embed.js, forms-rapidos 2026-10-01; mesma causa aqui, mesmo remédio — ver etag_match.go).

Exemplo

curl -X GET 'https://api.pages.asender.net/_f/arquivo_AQUI' \
  -H 'Authorization: Bearer SEU_TOKEN'

{slug}

POST /{slug} sem schema na spec

resolve a página pela mesma rota que a serviu, monta a submissão com `Modo = pagina` e chama o MESMO pipeline dos outros dois modos.

Onde é usadaPOST no caminho da própria página.
Efeitosos do pipeline (banco, outbox). O tenant e o form vêm da PÁGINA resolvida no servidor, nunca do corpo: o form da página é HTML público, e qualquer um pode reenviá-lo com outro form_id. Resolver pelo host+caminho é o que impede postar no formulário de outro tenant.

Exemplo

curl -X POST 'https://api.pages.asender.net/slug_AQUI' \
  -H 'Authorization: Bearer SEU_TOKEN' \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: <uuid-v4>' \
  -d '{ }'

Analytics

GET /v1/analytics/ab

Resultado do teste A/B por variante

Sem experimento: sessões e conversões por variante na janela (legado). Com experimento (ADR-0093): o relatório estatístico de UM teste — por variante, visitas, conversões, taxa com intervalo de Wilson 95%, chance de ser a melhor (Beta-Binomial), peso configurado e efetivo; o estado (coletando / sem_dados_suficientes / inconclusivo / vencedor_provavel) e a série diária. Conversão = sessão com form_submit; robôs fora.

Onde é usadatela do experimento.
Efeitosuma leitura.

Parâmetros

nomeemobrigatóriodescrição
paginaquery nãorecorta o relatório por página; ausente agrega todas.
dequery nãoinício da janela (RFC3339). Ausente usa os últimos 7 dias; teto de 92 dias.
atequery nãofim da janela (RFC3339).
experimentoquery não
variantesquery nãoid:peso separados por vírgula (até 10)
desdequery nãoinício do teste
minimoquery não
probquery não
pisoquery não
autobalancearquery não

Respostas

códigosignificado
200ok
400parâmetros do experimento inválidos
401Sem sessão / credencial ausente ou inválida.

Exemplo

curl -X GET 'https://api.pages.asender.net/v1/analytics/ab' \
  -H 'Authorization: Bearer SEU_TOKEN'
GET /v1/analytics/breakdown

Quebra por dimensão

Onde é usadatela de origens.
Efeitosuma leitura.

Parâmetros

nomeemobrigatóriodescrição
dimensaoquery nãodimensão da quebra; dimensão desconhecida devolve 400 COM a lista aceita
paginaquery nãorecorta o relatório por página; ausente agrega todas.
dequery nãoinício da janela (RFC3339). Ausente usa os últimos 7 dias; teto de 92 dias.
atequery nãofim da janela (RFC3339).
limitequery não

Respostas

códigosignificado
200ok
400dimensão/janela inválidas
401Sem sessão / credencial ausente ou inválida.

Exemplo

curl -X GET 'https://api.pages.asender.net/v1/analytics/breakdown' \
  -H 'Authorization: Bearer SEU_TOKEN'
GET /v1/analytics/events

Eventos brutos

Onde é usadatela de depuração da instrumentação.
Efeitosuma leitura.

Parâmetros

nomeemobrigatóriodescrição
paginaquery nãorecorta o relatório por página; ausente agrega todas.
limitequery não
dequery nãoinício da janela (RFC3339). Ausente usa os últimos 7 dias; teto de 92 dias.
atequery nãofim da janela (RFC3339).

Respostas

códigosignificado
200ok
401Sem sessão / credencial ausente ou inválida.

Exemplo

curl -X GET 'https://api.pages.asender.net/v1/analytics/events' \
  -H 'Authorization: Bearer SEU_TOKEN'
GET /v1/analytics/heatmap

Mapa de calor

Onde é usadatela de mapa de calor.
Efeitosuma leitura. # As posições são PORCENTAGEM, e não pixel Pixel absoluto é da tela de quem clicou: o mesmo botão sai em x=320 no celular e x=980 no monitor. A célula em porcentagem compara telas diferentes — que é a única pergunta que um mapa de calor responde.

Parâmetros

nomeemobrigatóriodescrição
paginaquery nãorecorta o relatório por página; ausente agrega todas.
ladoquery nãoresolução da grade
dequery nãoinício da janela (RFC3339). Ausente usa os últimos 7 dias; teto de 92 dias.
atequery nãofim da janela (RFC3339).

Respostas

códigosignificado
200ok
400lado inválido
401Sem sessão / credencial ausente ou inválida.

Exemplo

curl -X GET 'https://api.pages.asender.net/v1/analytics/heatmap' \
  -H 'Authorization: Bearer SEU_TOKEN'
GET /v1/analytics/live

Instantâneo em tempo real

Onde é usadacabeçalho da tela ao vivo, no primeiro carregamento.
Efeitosuma leitura. Existe além do stream porque quem abre a tela precisa ver ALGO antes do primeiro evento chegar. Sem isso, uma conta com movimento baixo mostraria tela vazia por minutos e pareceria quebrada.

Respostas

códigosignificado
200ok
401Sem sessão / credencial ausente ou inválida.

Exemplo

curl -X GET 'https://api.pages.asender.net/v1/analytics/live' \
  -H 'Authorization: Bearer SEU_TOKEN'
GET /v1/analytics/live/stream

Stream em tempo real (SSE)

Server-Sent Events do movimento ao vivo.

Onde é usadatela ao vivo.
Efeitosmantém uma conexão aberta e uma inscrição no hub. # SSE, e não WebSocket O fluxo é de mão única (servidor → tela) e o cliente é um navegador. SSE atravessa proxy comum, reconecta sozinho e cabe em quatro linhas de JS; WebSocket traria um protocolo inteiro para carregar dado que só desce.

Respostas

códigosignificado
200fluxo de eventos
401Sem sessão / credencial ausente ou inválida.

Exemplo

curl -X GET 'https://api.pages.asender.net/v1/analytics/live/stream' \
  -H 'Authorization: Bearer SEU_TOKEN'
GET /v1/analytics/overview

Visão geral do publicado

Onde é usadatela inicial de analytics.
Efeitosduas leituras. Funil e atribuição juntos porque a tela mostra os dois lado a lado, e duas requisições poderiam ler janelas diferentes — o total da atribuição não fecharia com o topo do funil, e o número "errado" seria o certo.

Parâmetros

nomeemobrigatóriodescrição
paginaquery nãorecorta o relatório por página; ausente agrega todas.
dequery nãoinício da janela (RFC3339). Ausente usa os últimos 7 dias; teto de 92 dias.
atequery nãofim da janela (RFC3339).

Respostas

códigosignificado
200ok
400janela inválida (RFC3339, teto 92 dias)
401Sem sessão / credencial ausente ou inválida.

Exemplo

curl -X GET 'https://api.pages.asender.net/v1/analytics/overview' \
  -H 'Authorization: Bearer SEU_TOKEN'
GET /v1/analytics/relatorio

Relatório filtrado (totais, série diária e dimensão)

Atribuição por SESSÃO (primeiro valor preenchido da visita): a conversão que acontece depois da página de entrada herda a origem dela. A comparação com o período anterior é esta mesma rota com a janela anterior.

Onde é usadaaba "Analytics" do formulário no painel.
Efeitosnove leituras sob o tenant. Um formulário de outra conta casa zero linha na RLS: o relatório sai zerado, que é o desfecho correto (a existência do form não vaza nem por 403).

Parâmetros

nomeemobrigatóriodescrição
dimensaoquery nãodimensão das linhas; desconhecida devolve 400 com a lista
dequery nãoinício da janela (RFC3339). Ausente usa os últimos 7 dias; teto de 92 dias.
atequery nãofim da janela (RFC3339).
paginaquery não
utm_sourcequery nãoorigem; `(vazio)` = só visitas sem origem
utm_mediumquery não
utm_campaignquery não
paisquery nãoISO-2 da borda
dispositivoquery não
limitequery não

Respostas

códigosignificado
200ok
400dimensão/filtro/janela inválidos (RFC3339, teto 92 dias)
401Sem sessão / credencial ausente ou inválida.
501relatório indisponível neste ambiente

Exemplo

curl -X GET 'https://api.pages.asender.net/v1/analytics/relatorio' \
  -H 'Authorization: Bearer SEU_TOKEN'
GET /v1/analytics/session/{id}

Eventos de uma sessão

Onde é usadatela de sessão — o que aquela visita fez, em ordem.
Efeitosuma leitura. Sessão de outra conta responde lista VAZIA, e não 404: o id de sessão é opaco e gerado no browser, e distinguir "não existe" de "não é sua" transformaria a rota num oráculo de existência de sessão alheia.

Parâmetros

nomeemobrigatóriodescrição
idpath sim

Respostas

códigosignificado
200ok
400sessão inválida
401Sem sessão / credencial ausente ou inválida.

Exemplo

curl -X GET 'https://api.pages.asender.net/v1/analytics/session/id_AQUI' \
  -H 'Authorization: Bearer SEU_TOKEN'
GET /v1/analytics/traffic-quality

Qualidade do tráfego

Onde é usadatela de qualidade — "esse tráfego pago é gente?".
Efeitosuma leitura.

Parâmetros

nomeemobrigatóriodescrição
dequery nãoinício da janela (RFC3339). Ausente usa os últimos 7 dias; teto de 92 dias.
atequery nãofim da janela (RFC3339).

Respostas

códigosignificado
200ok
401Sem sessão / credencial ausente ou inválida.

Exemplo

curl -X GET 'https://api.pages.asender.net/v1/analytics/traffic-quality' \
  -H 'Authorization: Bearer SEU_TOKEN'
GET /v1/formularios/{formID}/analytics

Funil e conversão de um formulário

Visualizações e inícios contam SESSÕES únicas; envios vem das submissões gravadas (autoritativo). taxa_conversao = envios / visualizacoes; abandono = (inicios - sessões com envio_ok) / inicios. Série diária no fuso pedido, com zeros nos dias vazios.

Onde é usadaaba "Analytics" do formulário no painel.
Efeitosnove leituras sob o tenant. Um formulário de outra conta casa zero linha na RLS: o relatório sai zerado, que é o desfecho correto (a existência do form não vaza nem por 403).

Parâmetros

nomeemobrigatóriodescrição
formIDpath sim
dequery nãoRFC3339 ou AAAA-MM-DD (padrão: 30 dias atrás)
atequery nãoRFC3339 ou AAAA-MM-DD (padrão: agora; máx. 400 dias)
fusoquery não

Respostas

códigosignificado
200relatório do funil
400parâmetro inválido
401sem sessão/conta

Exemplo

curl -X GET 'https://api.pages.asender.net/v1/formularios/formID_AQUI/analytics' \
  -H 'Authorization: Bearer SEU_TOKEN'
GET /v1/relatorios/visoes

Listar as visões salvas da conta

Onde é usadatela de domínios.
Efeitosuma leitura.

Respostas

códigosignificado
200ok
401Sem sessão / sem tenant na sessão.

Exemplo

curl -X GET 'https://api.pages.asender.net/v1/relatorios/visoes' \
  -H 'Authorization: Bearer SEU_TOKEN'
POST /v1/relatorios/visoes

Salvar o recorte atual como visão

Onde é usadatela de domínios.
Efeitosuma escrita. # O domínio nasce NÃO verificado E é isso que impede o pior desfecho do produto: sem a prova de DNS, qualquer conta declararia banco.com.br e passaria a receber certificado TLS para esse nome — servindo uma página de captura no domínio de outra pessoa, com cadeado verde.

Respostas

códigosignificado
201criada
400corpo inválido (inclui campo desconhecido, ex. tenant_id)
401Sem sessão / sem tenant na sessão.
409já existe visão com esse nome (sem diferenciar maiúscula)
422recorte inválido, ou a conta já tem 50 visões

Exemplo

curl -X POST 'https://api.pages.asender.net/v1/relatorios/visoes' \
  -H 'Authorization: Bearer SEU_TOKEN' \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: <uuid-v4>' \
  -d '{ }'
DELETE /v1/relatorios/visoes/{id}

Apagar uma visão

Onde é usadatela de domínios.
Efeitosuma escrita. Apagar TIRA a autorização de TLS, e é o único caminho para isso — de propósito: um domínio verificado que pudesse ser "desverificado" por outro caminho sairia do ar sem ninguém entender por quê.

Parâmetros

nomeemobrigatóriodescrição
idpath sim

Respostas

códigosignificado
204apagada
401Sem sessão / sem tenant na sessão.
404não existe — ou é de outra conta (mesma resposta)

Exemplo

curl -X DELETE 'https://api.pages.asender.net/v1/relatorios/visoes/id_AQUI' \
  -H 'Authorization: Bearer SEU_TOKEN'
PUT /v1/relatorios/visoes/{id}

Atualizar uma visão

Onde é usadaedição.
Efeitosescreve a linha.

Parâmetros

nomeemobrigatóriodescrição
idpath sim

Respostas

códigosignificado
200atualizada
400corpo inválido
401Sem sessão / sem tenant na sessão.
404não existe — ou é de outra conta (mesma resposta)
409nome repetido
422recorte inválido

Exemplo

curl -X PUT 'https://api.pages.asender.net/v1/relatorios/visoes/id_AQUI' \
  -H 'Authorization: Bearer SEU_TOKEN' \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: <uuid-v4>' \
  -d '{ }'

asender-sw.js

GET /asender-sw.js sem schema na spec

304 quando o `If-None-Match` bate; senão o bundle, com cache de uma hora e `X-Content-Type-Options: nosniff`.

Onde é usadarota /chat.js do runtime.
Entradasw; r.
Efeitosnenhum — é leitura de bytes já em memória. Não há CORS aqui, e é de propósito. Um <script src> não precisa de CORS para executar, e liberar Access-Control-Allow-Origin: * neste arquivo só abriria a leitura do conteúdo por fetch de qualquer origem sem ganho nenhum. Quem precisa de CORS é a API do chat, e lá a origem é conferida contra a chave de site — não liberada em *. ifNoneMatchCasa (comparação FRACA, RFC 9110 §13.1.2), não ==: a borda comprime este script on-the-fly e rebaixa o ETag forte para W/"…" na resposta — o navegador devolve esse W/ na revisita, e == contra o forte nunca batia (achado em embed.js, forms-rapidos 2026-10-01; mesma causa aqui, mesmo remédio — ver etag_match.go).

Exemplo

curl -X GET 'https://api.pages.asender.net/asender-sw.js' \
  -H 'Authorization: Bearer SEU_TOKEN'

campos-nativos

GET /v1/campos-nativos sem schema na spec

GET /v1/campos-nativos → `{campos, grupos, paises}`.

Onde é usadaconstrutor de formulário (paleta de campos nativos, seletor de país) e o diálogo de destinos (alvos do mapa). Sessão do painel exigida pela rota.
Efeitosnenhum — lê o catálogo embutido. Sem tenant na pergunta: o catálogo é o mesmo para toda conta. O 401 sem conta na sessão é só coerência com as rotas vizinhas.

Exemplo

curl -X GET 'https://api.pages.asender.net/v1/campos-nativos' \
  -H 'Authorization: Bearer SEU_TOKEN'

Captura de leads

POST /f/{formID}

Enviar uma submissão pelo snippet (anônimo)

Modo embutido em site de terceiro. Sem credencial: a conta sai do próprio formulário, e a origem tem de estar na allowlist DELE (senão 403). Sempre responde 200 — inclusive quando a submissão é classificada como spam (aceitar e descartar são indistinguíveis, por desenho). Um <form> nativo (Accept: text/html) recebe a tela de agradecimento; um XHR recebe o JSON. Servido no runtime, com CORS por formulário. O OPTIONS é o preflight.

Onde é usadarota pública, chamada de outro domínio pelo navegador.
Efeitosos do pipeline. Aqui não há API key: quem chama é o navegador do visitante, e uma key no snippet seria segredo no cliente (§13.4). A defesa é a allowlist de origem, verificada NO SERVIDOR — o header Origin é posto pelo navegador e não pode ser forjado por script da própria página.

Parâmetros

nomeemobrigatóriodescrição
formIDpath sim

Respostas

códigosignificado
200aceito (ou spam, indistinguível)
400corpo malformado
403origem não autorizada para este formulário
404formulário não encontrado
503não consegui resolver a conta do formulário agora

Exemplo

curl -X POST 'https://api.pages.asender.net/f/formID_AQUI' \
  -H 'Authorization: Bearer SEU_TOKEN' \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: <uuid-v4>' \
  -d '{ }'
GET /f/{formID}/carimbo

Obter o carimbo de tempo anti-spam do snippet (anônimo)

Chamado pelo script do snippet ao carregar. Devolve um carimbo assinado (HMAC) do instante do pedido, que o snippet reenvia no campo oculto _asender_carimbo do POST /f/{formID}; o runtime mede o tempo de preenchimento com o PRÓPRIO relógio. Mesma allowlist de origem do POST (senão 403, sem CORS) e Cache-Control: no-store. Os campos _asender_website (honeypot), _asender_carimbo e _asender_interacao são reservados: lidos pelo anti-spam e nunca gravados como dado do lead. Sem carimbo, o tempo é tratado como não medido (sem penalidade).

Onde é usadachamado pelo formulario_antispam.js ao carregar o snippet (cross-origin, com Origin) e pela página nativa (same-origin, sem Origin).
Efeitosnenhum além da resposta — nada é gravado. # Por que conferir a origem, se um bot forja o cabeçalho Fora do navegador o Origin é forjável, e a allowlist não detém bot aqui (nem no POST). Ela mantém a rota com a MESMA política do envio: um site que não pode postar no formulário também não lê o carimbo dele. no-store porque um carimbo servido de cache mediria o tempo desde o primeiro visitante.

Parâmetros

nomeemobrigatóriodescrição
formIDpath sim

Respostas

códigosignificado
200carimbo emitido
403origem não autorizada para este formulário
404formulário não encontrado ou carimbo desligado
503não consegui resolver a conta do formulário agora

Exemplo

curl -X GET 'https://api.pages.asender.net/f/formID_AQUI/carimbo' \
  -H 'Authorization: Bearer SEU_TOKEN'
GET /f/{formID}/endereco

Consultar o endereço de um CEP para o widget de endereço (anônimo)

Chamado pelo script do snippet quando o visitante completa o CEP. O runtime confere o formulário (tem endereço? aceita o país? o CEP tem o formato?) e consulta o asender_plataforma_api com a credencial de serviço — o core nunca fica exposto. Mesma política de origem do carimbo; teto de 0.5/s (rajada 10) por formulário × IP e 1/s (rajada 30) por IP. Nenhuma falha trava o formulário: o widget fica manual.

Onde é usadaformulario_endereco.js do snippet e da página.
Efeitosuma ida ao core no miss do cache. # Respostas 200 endereço; 404 sem consulta ligada, formulário sem endereço ou CEP não encontrado; 403 origem recusada; 422 país/CEP inválidos; 429 teto; 503 core fora. Nenhuma delas trava o formulário: o widget fica manual.

Parâmetros

nomeemobrigatóriodescrição
formIDpath sim
paisquery sim
cepquery sim

Respostas

códigosignificado
200endereço encontrado (sem número e complemento)
403origem não autorizada para este formulário
404CEP não encontrado, formulário sem endereço ou consulta desligada
422país fora dos aceitos ou CEP fora do formato do país
429consultas demais (Retry-After)
503consulta indisponível agora (o endereço fica manual)

Exemplo

curl -X GET 'https://api.pages.asender.net/f/formID_AQUI/endereco' \
  -H 'Authorization: Bearer SEU_TOKEN'
POST /f/{formID}/eventos

Registrar eventos do funil do formulário embutido (anônimo)

Chamado pelo script do snippet via navigator.sendBeacon (corpo text/plain com JSON, para não disparar preflight). Tipos: impressao, visualizacao, inicio, campo, envio_tentativa, envio_ok, envio_erro. Mesma política de origem do carimbo; limite por formulário × IP. O IP não é gravado (o país vem do GeoIP). Alimenta GET /v1/formularios/{formID}/analytics.

Onde é usadatela de depuração da instrumentação.
Efeitosuma leitura.

Parâmetros

nomeemobrigatóriodescrição
formIDpath sim

Respostas

códigosignificado
204eventos registrados
400lote inválido
403origem não autorizada para este formulário
404formulário não encontrado ou funil desligado
429limite de eventos excedido
503banco indisponível

Exemplo

curl -X POST 'https://api.pages.asender.net/f/formID_AQUI/eventos' \
  -H 'Authorization: Bearer SEU_TOKEN' \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: <uuid-v4>' \
  -d '{ }'
POST /v1/forms/{formID}/submissions

Enviar uma submissão de um formulário (modo API)

Igual a POST /v1/leads, com o formID (id do Asender) no path. Exige API key com forms:write.

Onde é usadarota autenticada do runtime.
Efeitosos do pipeline (banco, outbox). O tenant vem do CONTEXTO, posto pelo middleware que validou a API key — nunca do corpo (§37: confiar em tenant_id do cliente). É isso que faz a key de um tenant não alcançar o form de outro.

Parâmetros

nomeemobrigatóriodescrição
formIDpath sim
Idempotency-Keyheader nãoAlternativa ao `id_externo` do corpo como chave de deduplicação (por formulário).

Respostas

códigosignificado
200duplicada — o `id_externo` (ou `Idempotency-Key`) já foi recebido neste formulário; nada de novo gravado e o `lead_id` é o do envio original
201lead criado
400recusado: corpo inválido, campo não declarado ou inválido, consentimento exigido ausente, `ocorrido_em` fora da janela
401Sem sessão / credencial ausente ou inválida.
403A chave não tem o escopo `forms:write`.
404formulário não encontrado (ou de outra conta)
429limite por conta excedido (300 por minuto por padrão). Aguarde `Retry-After` segundos.
502não consegui registrar agora; tente de novo com o mesmo `id_externo`
503não consegui validar a chave agora; tente de novo

Exemplo

curl -X POST 'https://api.pages.asender.net/v1/forms/formID_AQUI/submissions' \
  -H 'Authorization: Bearer SEU_TOKEN' \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: <uuid-v4>' \
  -d '{ }'
POST /v1/forms/externo/{externalID}/submissions

Enviar uma submissão pelo id externo do formulário (modo API)

Igual a POST /v1/forms/{formID}/submissions, mas o formulário é achado pelo external_id que você definiu na autoria — você não precisa guardar o id do Asender. Exige API key com forms:write; formulário inexistente (ou de outra conta) responde 404.

Onde é usadarota do modo API, para integradores que guardam o id deles, não o nosso.
Efeitosuma chamada ao pages_api para resolver; depois os do pipeline. 404 quando o pages_api não conhece o id (para a conta da chave); 503 quando não dá para perguntar — recarregar resolve, e o lead não é aceito às cegas.

Parâmetros

nomeemobrigatóriodescrição
externalIDpath sim`external_id` do formulário
Idempotency-Keyheader nãoAlternativa ao `id_externo` do corpo como chave de deduplicação (por formulário).

Respostas

códigosignificado
200duplicada — o `id_externo` (ou `Idempotency-Key`) já foi recebido neste formulário; nada de novo gravado e o `lead_id` é o do envio original
201lead criado
400recusado: corpo inválido, campo não declarado ou inválido, consentimento exigido ausente, `ocorrido_em` fora da janela
401Sem sessão / credencial ausente ou inválida.
403A chave não tem o escopo `forms:write`.
404formulário não encontrado (ou de outra conta)
429limite por conta excedido (300 por minuto por padrão). Aguarde `Retry-After` segundos.
502não consegui registrar agora; tente de novo com o mesmo `id_externo`
503não consegui validar a chave agora; tente de novo

Exemplo

curl -X POST 'https://api.pages.asender.net/v1/forms/externo/externalID_AQUI/submissions' \
  -H 'Authorization: Bearer SEU_TOKEN' \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: <uuid-v4>' \
  -d '{ }'
POST /v1/leads

Enviar um lead (modo API, com API key)

Modo server-to-server. Exige API key com forms:write; a conta é resolvida da chave e o form_id vem no corpo. Servido no runtime, mas alcançável em https://api.pages.asender.net (e em https://sites.asender.net, igual). Sucesso é 201 (novo) ou 200 (duplicado). Reenviar com o mesmo id_externo é seguro (retry sem duplicar). Limite: 300 requisições por minuto por conta e 64 KB por corpo. Fora de produção use sk_test_… e e-mails @exemplo.test.

Onde é usadarota autenticada do runtime.
Efeitosos do pipeline (banco, outbox). O tenant vem do CONTEXTO, posto pelo middleware que validou a API key — nunca do corpo (§37: confiar em tenant_id do cliente). É isso que faz a key de um tenant não alcançar o form de outro.

Parâmetros

nomeemobrigatóriodescrição
Idempotency-Keyheader nãoAlternativa ao `id_externo` do corpo como chave de deduplicação (por formulário).

Respostas

códigosignificado
200duplicada — o `id_externo` (ou `Idempotency-Key`) já foi recebido neste formulário; nada de novo gravado e o `lead_id` é o do envio original
201lead criado
400recusado: corpo inválido, campo não declarado ou inválido, consentimento exigido ausente, `ocorrido_em` fora da janela
401Sem sessão / credencial ausente ou inválida.
403A chave não tem o escopo `forms:write`.
404formulário não encontrado (ou de outra conta)
429limite por conta excedido (300 por minuto por padrão). Aguarde `Retry-After` segundos.
502não consegui registrar agora; tente de novo com o mesmo `id_externo`
503não consegui validar a chave agora; tente de novo

Exemplo

curl -X POST 'https://api.pages.asender.net/v1/leads' \
  -H 'Authorization: Bearer SEU_TOKEN' \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: <uuid-v4>' \
  -d '{ }'

chat.js

GET /chat.js sem schema na spec

304 quando o `If-None-Match` bate; senão o bundle, com cache de uma hora e `X-Content-Type-Options: nosniff`.

Onde é usadarota /chat.js do runtime.
Entradasw; r.
Efeitosnenhum — é leitura de bytes já em memória. Não há CORS aqui, e é de propósito. Um <script src> não precisa de CORS para executar, e liberar Access-Control-Allow-Origin: * neste arquivo só abriria a leitura do conteúdo por fetch de qualquer origem sem ganho nenhum. Quem precisa de CORS é a API do chat, e lá a origem é conferida contra a chave de site — não liberada em *. ifNoneMatchCasa (comparação FRACA, RFC 9110 §13.1.2), não ==: a borda comprime este script on-the-fly e rebaixa o ETag forte para W/"…" na resposta — o navegador devolve esse W/ na revisita, e == contra o forte nunca batia (achado em embed.js, forms-rapidos 2026-10-01; mesma causa aqui, mesmo remédio — ver etag_match.go).

Exemplo

curl -X GET 'https://api.pages.asender.net/chat.js' \
  -H 'Authorization: Bearer SEU_TOKEN'

Coleta

POST /collect

Ingerir um lote de eventos de comportamento (anônimo, credencial de pixel)

Chamado pelo pixel.js no navegador do visitante. A credencial (token assinado do pixel) vai no corpo. Responde 204; 403 para credencial/origem inválida, 429 para excesso. Servido no runtime.

Onde é usadarota pública, a de maior volume do sistema depois da própria página.
Efeitospublica no broker; NUNCA escreve no banco. # Responde 204 quase sempre, e isso é deliberado Credencial inválida é 403 (o cliente precisa saber que aquele HTML está velho). Fora isso — evento inválido, tipo desconhecido, lote parcialmente ruim — a resposta é 204 e o que dava para aproveitar foi publicado. O pixel roda no browser de terceiros: transformar um evento malformado em erro visível não conserta nada e enche o console de quem só quer ver a página.

Respostas

códigosignificado
204aceito
400corpo inválido
403credencial ou origem inválida
429excesso (rate-limit por credencial × IP)
503coleta indisponível agora

Exemplo

curl -X POST 'https://api.pages.asender.net/collect' \
  -H 'Authorization: Bearer SEU_TOKEN' \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: <uuid-v4>' \
  -d '{ }'
GET /pixel.js

O script do pixel (anônimo)

Devolve o pixel.js a ser carregado pela página publicada. Servido no runtime, com ETag.

Onde é usadaHTML publicado antes do endereço versionado (ScriptVersionado) e o snippet do pixel de terceiros, que não pode trocar de endereço a cada deploy.
Efeitosescreve o script. Cache de 1 hora com ETag: o script muda com o deploy, e revalidar de hora em hora custa um 304. immutable seria errado — o conteúdo muda no mesmo caminho. ifNoneMatchCasa (comparação FRACA, RFC 9110 §13.1.2), não ==: a borda comprime este script on-the-fly e rebaixa o ETag forte para W/"…" na resposta — o navegador devolve esse W/ na revisita, e == contra o forte nunca batia (achado em embed.js, forms-rapidos 2026-10-01; mesma causa aqui, mesmo remédio — ver etag_match.go).

Respostas

códigosignificado
200o script
304não modificado (ETag)

Exemplo

curl -X GET 'https://api.pages.asender.net/pixel.js' \
  -H 'Authorization: Bearer SEU_TOKEN'
GET /r/{slug}

Resolver um link curto (anônimo)

Redireciona ao destino do link e conta o clique. Servido no runtime.

Onde é usadarota pública do runtime, nos hosts da conta (substitui o Redirect.Servir da 012 no server.go).

Parâmetros

nomeemobrigatóriodescrição
slugpath sim

Respostas

códigosignificado
302redireciona ao destino
404link inexistente ou inativo

Exemplo

curl -X GET 'https://api.pages.asender.net/r/slug_AQUI' \
  -H 'Authorization: Bearer SEU_TOKEN'
GET /r/{slug}

Resolver um link curto (anônimo)

Redireciona ao destino do link e conta o clique. Servido no runtime.

Onde é usadarota /chat.js do runtime.
Entradasw; r.
Efeitosnenhum — é leitura de bytes já em memória. Não há CORS aqui, e é de propósito. Um <script src> não precisa de CORS para executar, e liberar Access-Control-Allow-Origin: * neste arquivo só abriria a leitura do conteúdo por fetch de qualquer origem sem ganho nenhum. Quem precisa de CORS é a API do chat, e lá a origem é conferida contra a chave de site — não liberada em *. ifNoneMatchCasa (comparação FRACA, RFC 9110 §13.1.2), não ==: a borda comprime este script on-the-fly e rebaixa o ETag forte para W/"…" na resposta — o navegador devolve esse W/ na revisita, e == contra o forte nunca batia (achado em embed.js, forms-rapidos 2026-10-01; mesma causa aqui, mesmo remédio — ver etag_match.go).

Parâmetros

nomeemobrigatóriodescrição
slugpath sim

Respostas

códigosignificado
302redireciona ao destino
404link inexistente ou inativo

Exemplo

curl -X GET 'https://api.pages.asender.net/r/slug_AQUI' \
  -H 'Authorization: Bearer SEU_TOKEN'
GET /r/{slug}

Resolver um link curto (anônimo)

Redireciona ao destino do link e conta o clique. Servido no runtime.

Onde é usadarota /chat.js do runtime.
Entradasw; r.
Efeitosnenhum — é leitura de bytes já em memória. Não há CORS aqui, e é de propósito. Um <script src> não precisa de CORS para executar, e liberar Access-Control-Allow-Origin: * neste arquivo só abriria a leitura do conteúdo por fetch de qualquer origem sem ganho nenhum. Quem precisa de CORS é a API do chat, e lá a origem é conferida contra a chave de site — não liberada em *. ifNoneMatchCasa (comparação FRACA, RFC 9110 §13.1.2), não ==: a borda comprime este script on-the-fly e rebaixa o ETag forte para W/"…" na resposta — o navegador devolve esse W/ na revisita, e == contra o forte nunca batia (achado em embed.js, forms-rapidos 2026-10-01; mesma causa aqui, mesmo remédio — ver etag_match.go).

Parâmetros

nomeemobrigatóriodescrição
slugpath sim

Respostas

códigosignificado
302redireciona ao destino
404link inexistente ou inativo

Exemplo

curl -X GET 'https://api.pages.asender.net/r/slug_AQUI' \
  -H 'Authorization: Bearer SEU_TOKEN'

Domínios

GET /v1/cotas

Uso e teto de domínios da conta, pelo plano

pages_subdominios = grátis (*.apages.xyz) + subdomínios próprios; pages_dominios_principais = domínios próprios apex. limite: null = ilimitado. O teto vem do plano (plataforma, cache 60 s); sem resposta dela, 503 — nunca "0 de 0". Tenant da sessão, nunca da query.

Onde é usadamedidor de cota da tela de domínios.
Efeitosuma leitura na plataforma (cache 60 s) e uma no banco.

Respostas

códigosignificado
200ok
401Sem sessão / sem tenant na sessão.
503plataforma indisponível para conferir o plano

Exemplo

curl -X GET 'https://api.pages.asender.net/v1/cotas' \
  -H 'Authorization: Bearer SEU_TOKEN'
GET /v1/dominios

Listar domínios da conta

Sessão ou API key com pages:read. O domínio padrão da conta (apages.xyz, criado automaticamente) vem primeiro e traz padrao: true; os demais seguem com padrao: false.

Onde é usadatela de domínios.
Efeitosuma leitura.

Respostas

códigosignificado
200ok (o padrão primeiro)
401Sem sessão / sem tenant na sessão.
403A chave não tem o escopo exigido pela rota (ou a rota não aceita chave). Ex.: `{"erro":"a chave não tem o escopo forms:write"}`.
503Não foi possível validar a chave agora (core fora do ar). Tente de novo.

Exemplo

curl -X GET 'https://api.pages.asender.net/v1/dominios' \
  -H 'Authorization: Bearer SEU_TOKEN'
POST /v1/dominios

Adicionar domínio (gera o TXT de posse)

Onde é usadatela de domínios.
Efeitosuma escrita. # O domínio nasce NÃO verificado E é isso que impede o pior desfecho do produto: sem a prova de DNS, qualquer conta declararia banco.com.br e passaria a receber certificado TLS para esse nome — servindo uma página de captura no domínio de outra pessoa, com cadeado verde.

Respostas

códigosignificado
201criado
401Sem sessão / sem tenant na sessão.
402limite do plano atingido para a classe do domínio
409host já cadastrado
422host inválido, reservado, sufixo público ou sob o domínio dos grátis
503limite do plano indisponível para conferir agora

Exemplo

curl -X POST 'https://api.pages.asender.net/v1/dominios' \
  -H 'Authorization: Bearer SEU_TOKEN' \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: <uuid-v4>' \
  -d '{ }'
DELETE /v1/dominios/{id}

Remover domínio

Onde é usadatela de domínios.
Efeitosuma escrita. Apagar TIRA a autorização de TLS, e é o único caminho para isso — de propósito: um domínio verificado que pudesse ser "desverificado" por outro caminho sairia do ar sem ninguém entender por quê.

Parâmetros

nomeemobrigatóriodescrição
idpath sim

Respostas

códigosignificado
204removido
401Sem sessão / sem tenant na sessão.
404Recurso inexistente ou fora do escopo da sessão (não revela existência em outra conta).

Exemplo

curl -X DELETE 'https://api.pages.asender.net/v1/dominios/id_AQUI' \
  -H 'Authorization: Bearer SEU_TOKEN'
GET /v1/dominios/{id}/cloudflare

Estado do DNS automático na Cloudflare

Diz se a zona está na Cloudflare (NS / _domainconnect), se o botão "Conectar com Cloudflare" (Domain Connect) está disponível e se há token guardado. Ritmo: 60/min por conta. Domínio de outra conta = 404.

Onde é usadacartão do domínio na tela de domínios.
Efeitosconsultas DNS; no máximo um GET (cacheado) à Cloudflare.

Parâmetros

nomeemobrigatóriodescrição
idpath sim

Respostas

códigosignificado
200estado
401Sem sessão / sem tenant na sessão.
404Recurso inexistente ou fora do escopo da sessão (não revela existência em outra conta).
429muitas tentativas

Exemplo

curl -X GET 'https://api.pages.asender.net/v1/dominios/id_AQUI/cloudflare' \
  -H 'Authorization: Bearer SEU_TOKEN'
POST /v1/dominios/{id}/cloudflare/aplicar

Aplicar na Cloudflare os registros do Asender

Com o token guardado, cria ou atualiza SÓ os registros do Asender — idempotente, nunca apaga registro alheio, proxied:false em CNAME/A/AAAA. Cada aplicação vai para a trilha de auditoria (também quando falha no meio). Ritmo: 10/min.

Onde é usadabotão "Aplicar registros na Cloudflare". Depois, a tela chama o verificar de sempre.
Efeitoschamadas à API da Cloudflare em nome do cliente; uma linha de auditoria.

Parâmetros

nomeemobrigatóriodescrição
idpath sim

Respostas

códigosignificado
200relatório por registro
401Sem sessão / sem tenant na sessão.
404Recurso inexistente ou fora do escopo da sessão (não revela existência em outra conta).
409sem token guardado (ou token ilegível — cole de novo)
422token recusado
429muitas tentativas
503Cloudflare indisponível

Exemplo

curl -X POST 'https://api.pages.asender.net/v1/dominios/id_AQUI/cloudflare/aplicar' \
  -H 'Authorization: Bearer SEU_TOKEN' \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: <uuid-v4>' \
  -d '{ }'
POST /v1/dominios/{id}/cloudflare/domain-connect

URL assinada do Domain Connect (1 clique)

Monta a URL da tela de consentimento da Cloudflare para o template do Asender, só com os grupos que faltam, ASSINADA (RSA-SHA256). retorno é o caminho da tela de domínios (/t/<conta>/dominios); o host de volta vem do servidor. Na volta (?cloudflare=<id>) a tela só chama a verificação. Ritmo: 10/min por conta.

Onde é usadabotão "Conectar com Cloudflare".
Efeitosconsultas DNS; um GET cacheado à Cloudflare; uma linha de auditoria. # O retorno não carrega state A Cloudflare não suporta state (doc oficial). Por isso o retorno NÃO faz escrita alguma: a tela só chama o verificar de sempre, que é idempotente e autenticado — forjar um retorno não dá a ninguém nada além de uma verificação.

Parâmetros

nomeemobrigatóriodescrição
idpath sim

Respostas

códigosignificado
200URL
401Sem sessão / sem tenant na sessão.
404Recurso inexistente ou fora do escopo da sessão (não revela existência em outra conta).
409Domain Connect indisponível para o domínio (ver `motivo`) ou nada a aplicar
422retorno inválido
429muitas tentativas

Exemplo

curl -X POST 'https://api.pages.asender.net/v1/dominios/id_AQUI/cloudflare/domain-connect' \
  -H 'Authorization: Bearer SEU_TOKEN' \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: <uuid-v4>' \
  -d '{ }'
DELETE /v1/dominios/{id}/cloudflare/token

Remover o token guardado

Onde é usadabotão "remover token".
Efeitosuma escrita e uma linha de auditoria. O token continua válido NA Cloudflare: a tela pede para o cliente revogá-lo lá.

Parâmetros

nomeemobrigatóriodescrição
idpath sim

Respostas

códigosignificado
204removido (também quando não havia)
401Sem sessão / sem tenant na sessão.
404Recurso inexistente ou fora do escopo da sessão (não revela existência em outra conta).
429muitas tentativas

Exemplo

curl -X DELETE 'https://api.pages.asender.net/v1/dominios/id_AQUI/cloudflare/token' \
  -H 'Authorization: Bearer SEU_TOKEN'
PUT /v1/dominios/{id}/cloudflare/token

Guardar o token da Cloudflare (Zone → DNS → Edit)

Valida o token na API da Cloudflare (a zona do domínio tem de estar visível e o DNS legível), cifra (AES-256-GCM) e guarda. O token nunca é devolvido. Ritmo: 10/min.

Onde é usadaformulário "usar token" do cartão do domínio.
Efeitosaté N+1 GETs à Cloudflare; uma escrita; uma linha de auditoria. O token NUNCA volta em resposta nem em log — nem cifrado.

Parâmetros

nomeemobrigatóriodescrição
idpath sim

Respostas

códigosignificado
200estado novo
401Sem sessão / sem tenant na sessão.
404Recurso inexistente ou fora do escopo da sessão (não revela existência em outra conta).
422formato inválido
429muitas tentativas
503Cloudflare indisponível ou cifra não configurada neste ambiente

Exemplo

curl -X PUT 'https://api.pages.asender.net/v1/dominios/id_AQUI/cloudflare/token' \
  -H 'Authorization: Bearer SEU_TOKEN' \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: <uuid-v4>' \
  -d '{ }'
DELETE /v1/dominios/{id}/pagina-inicial

Remover a página inicial do domínio

Idempotente — domínio já sem home marcada continua respondendo 200.

Onde é usadatela de domínios (botão "remover página inicial").
Efeitosuma escrita.

Parâmetros

nomeemobrigatóriodescrição
idpath sim

Respostas

códigosignificado
200domínio com pagina_inicial: null
401Sem sessão / sem tenant na sessão.
404Recurso inexistente ou fora do escopo da sessão (não revela existência em outra conta).

Exemplo

curl -X DELETE 'https://api.pages.asender.net/v1/dominios/id_AQUI/pagina-inicial' \
  -H 'Authorization: Bearer SEU_TOKEN'
PUT /v1/dominios/{id}/pagina-inicial

Definir a página inicial (home/apex) do domínio

Marca uma página, já publicada ou não, como a home deste domínio (migration 029) — GET https://host/ passa a resolver nela. A página precisa ser do MESMO tenant e ter dominio igual ao host deste domínio; não publicada é aceita (fica publicada:false na resposta, e / continua 404 até a pessoa publicar).

Onde é usadatela de domínios (botão "definir página inicial").
Efeitosuma escrita. # Os dois 404 são indistinguíveis de propósito "Domínio não encontrado" cobre id inexistente E id de outra conta; "página não encontrada" cobre id inexistente, apagada E de outra conta. Distinguir qualquer um desses pares confirmaria a um tenant que aquele id existe na plataforma, atribuído a alguém — o mesmo raciocínio do 404 de Verificar e Apagar (dominios.go).

Parâmetros

nomeemobrigatóriodescrição
idpath sim

Respostas

códigosignificado
200domínio com a home atualizada
400Parâmetro de query fora do conjunto aceito.
401Sem sessão / sem tenant na sessão.
404domínio OU página não encontrados (ids distintos e indistinguíveis de outra conta)
422a página existe (e é do tenant) mas não é deste domínio

Exemplo

curl -X PUT 'https://api.pages.asender.net/v1/dominios/id_AQUI/pagina-inicial' \
  -H 'Authorization: Bearer SEU_TOKEN' \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: <uuid-v4>' \
  -d '{ }'
DELETE /v1/dominios/{id}/push

Desligar a coleta de Web Push do domínio

Idempotente. Mantém os valores gravados para religar sem nova emissão.

Onde é usadabotão "Desligar" do pages_front.

Parâmetros

nomeemobrigatóriodescrição
idpath sim

Respostas

códigosignificado
204desligado
401Sem sessão / sem tenant na sessão.
403sem papel de editor
404Recurso inexistente ou fora do escopo da sessão (não revela existência em outra conta).

Exemplo

curl -X DELETE 'https://api.pages.asender.net/v1/dominios/id_AQUI/push' \
  -H 'Authorization: Bearer SEU_TOKEN'
GET /v1/dominios/{id}/push

Coleta de Web Push nativa do domínio (ADR-0102)

Estado da coleta de inscrições de push neste domínio. Sem configuração responde {"ligado":false}. Domínio de outra conta = 404.

Onde é usadatela de A/B do painel.
Efeitosuma leitura. Página SEM experimento responde 200 com o corpo vazio de variantes, e não 404: "esta página não tem teste" é uma resposta sobre o mundo, e a tela precisa dela para mostrar o botão de criar. 404 mandaria a tela tratar o caso normal como erro.

Parâmetros

nomeemobrigatóriodescrição
idpath sim

Respostas

códigosignificado
200estado
401Sem sessão / sem tenant na sessão.
404Recurso inexistente ou fora do escopo da sessão (não revela existência em outra conta).

Exemplo

curl -X GET 'https://api.pages.asender.net/v1/dominios/id_AQUI/push' \
  -H 'Authorization: Bearer SEU_TOKEN'
PUT /v1/dominios/{id}/push

Ligar ou configurar a coleta de Web Push do domínio

Grava só valores PÚBLICOS emitidos pela ferramenta Push (GET /v1/push/site-config da asender_push_api): chave VAPID pública, chave de site pk_push_… e a base da API, mais o convite. Com ligado:true, as páginas do domínio servem /asender-sw.js e o convite de inscrição. Exige papel de editor. O domínio precisa estar verificado (409).

Onde é usadabotão salvar do editor.
Efeitoscria uma versão.

Parâmetros

nomeemobrigatóriodescrição
idpath sim

Respostas

códigosignificado
200configuração gravada
400Parâmetro de query fora do conjunto aceito.
401Sem sessão / sem tenant na sessão.
403sem papel de editor
404Recurso inexistente ou fora do escopo da sessão (não revela existência em outra conta).
409domínio ainda não verificado
422valor inválido (chave

Exemplo

curl -X PUT 'https://api.pages.asender.net/v1/dominios/id_AQUI/push' \
  -H 'Authorization: Bearer SEU_TOKEN' \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: <uuid-v4>' \
  -d '{ }'
POST /v1/dominios/{id}/verificar

Verificar os 3 passos (TXT, apontamento e certificado)

Confere a posse (TXT, se ainda não provada), o apontamento (CNAME/A para a borda) e, com os dois ok, faz um handshake TLS que aquece o certificado. Responde 200 com o estado de CADA passo (um passo reprovado vem com ok:false e o motivo), e 503 só quando o DNS não respondeu a nada.

Onde é usadabotão "verificar agora" da tela de domínios.
Efeitosconsultas DNS, um handshake TLS com a nossa borda e escritas. # 200 com o estado de cada passo, e não 422 no primeiro que falha A tela é em passos: o cliente publica o TXT e o CNAME de uma vez, e precisa ver qual dos dois ainda falta. Recusar com 422 no TXT esconderia o estado do apontamento — que foi exatamente a queixa ("só tem opção do TXT"). O 503 continua para quando NADA pôde ser consultado: aí a resposta certa é "tente de novo", não um estado. # A posse já provada não é reconsultada O cliente costuma apagar o TXT depois de verificar. Reconsultar transformaria essa limpeza num erro vermelho no passo 1 de um domínio que está no ar.

Parâmetros

nomeemobrigatóriodescrição
idpath sim

Respostas

códigosignificado
200estado dos 3 passos
401Sem sessão / sem tenant na sessão.
404Recurso inexistente ou fora do escopo da sessão (não revela existência em outra conta).
503DNS indisponível para checar agora

Exemplo

curl -X POST 'https://api.pages.asender.net/v1/dominios/id_AQUI/verificar' \
  -H 'Authorization: Bearer SEU_TOKEN' \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: <uuid-v4>' \
  -d '{ }'
POST /v1/subdominios-gratis

Reservar um subdomínio grátis

Nasce verificado e pronto (posse da plataforma, TLS pelo certificado curinga). Conta no recurso pages_subdominios (que conta SÓ os grátis; host do cliente, como bio.cliente.com.br, conta em pages_dominios_principais). base opcional escolhe a base grátis (vazia = a padrão). Liberar é DELETE /v1/dominios/{id}. Até 10 reservas/trocas por hora por conta (429).

Onde é usadatela de domínios.
Efeitosuma escrita. # O domínio nasce NÃO verificado E é isso que impede o pior desfecho do produto: sem a prova de DNS, qualquer conta declararia banco.com.br e passaria a receber certificado TLS para esse nome — servindo uma página de captura no domínio de outra pessoa, com cadeado verde.

Respostas

códigosignificado
201reservado
400corpo inválido
401Sem sessão / sem tenant na sessão.
402cota do plano esgotada
409nome em uso (ou em quarentena)
422rótulo recusado (com `motivo`)
429reservas demais na última hora
503plataforma indisponível para conferir o plano (fail-closed)

Exemplo

curl -X POST 'https://api.pages.asender.net/v1/subdominios-gratis' \
  -H 'Authorization: Bearer SEU_TOKEN' \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: <uuid-v4>' \
  -d '{ }'
POST /v1/subdominios-gratis/{id}/trocar

Trocar o nome de um subdomínio grátis (páginas e links vão junto)

O nome antigo fica 30 dias indisponível para outras contas. Grátis de outra conta, ou id que não é grátis, responde 404.

Onde é usadaação "trocar nome" do bloco "Domínio grátis".
Efeitosuma escrita (apaga o antigo, cria o novo, move páginas e links).

Parâmetros

nomeemobrigatóriodescrição
idpath sim

Respostas

códigosignificado
200trocado
401Sem sessão / sem tenant na sessão.
404Recurso inexistente ou fora do escopo da sessão (não revela existência em outra conta).
409nome em uso
422rótulo recusado
429trocas demais na última hora

Exemplo

curl -X POST 'https://api.pages.asender.net/v1/subdominios-gratis/id_AQUI/trocar' \
  -H 'Authorization: Bearer SEU_TOKEN' \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: <uuid-v4>' \
  -d '{ }'
GET /v1/subdominios-gratis/bases

Listar as bases em que um subdomínio grátis pode ser reservado

As bases grátis da plataforma (apages.xyz e as que forem disponibilizadas). padrao é a primária, usada quando base não é enviada.

Onde é usadao seletor de base do bloco "Domínio grátis" (aparece só se há >1).

Respostas

códigosignificado
200bases e a padrão
401Sem sessão / sem tenant na sessão.

Exemplo

curl -X GET 'https://api.pages.asender.net/v1/subdominios-gratis/bases' \
  -H 'Authorization: Bearer SEU_TOKEN'
GET /v1/subdominios-gratis/disponivel

Conferir se um nome grátis (<rotulo>.<base>) pode ser reservado

Rótulo DNS de 3 a 63 caracteres [a-z0-9-], sem punycode; fora dos nomes reservados da plataforma e das marcas/termos sensíveis (inclusive imitações como 1tau/paypa1). em_uso não diz de qual conta. Teto de 30 consultas por minuto por conta (429).

Onde é usadao campo "domínio grátis" da tela, enquanto a pessoa digita.
Efeitosno máximo uma leitura. 429 acima de 30 consultas/min por conta — sem teto, a rota vira enumerador de quais nomes existem na plataforma.

Parâmetros

nomeemobrigatóriodescrição
rotuloquery sim
basequery nãoBase grátis (de GET /v1/subdominios-gratis/bases); vazia = a padrão. Fora da lista: 422 base_desconhecida.

Respostas

códigosignificado
200resposta (disponível ou o motivo)
401Sem sessão / sem tenant na sessão.
422base desconhecida (`motivo: base_desconhecida`)
429consultas demais

Exemplo

curl -X GET 'https://api.pages.asender.net/v1/subdominios-gratis/disponivel' \
  -H 'Authorization: Bearer SEU_TOKEN'

f

GET /f/{formID}/embed.js sem schema na spec

devolve o loader com o HTML atual do formulário embutido como string JSON.

Onde é usada<script src=".../f/{id}/embed.js" async> colado no site do cliente (o snippet_js do painel).
Efeitosnenhum além da resposta. # Sem checagem de origem aqui <script src> não manda Origin, e o HTML não é segredo: é o mesmo que o modo HTML entrega para colar à vista. A política de origem continua onde age — no carimbo e no POST, que o HTML injetado chama. # JSON como literal JS json.Marshal escapa <, >, & e U+2028/U+2029: o HTML não fecha o <script> e não quebra a string. # Cache HTTP com revalidação (forms-rapidos, 2026-10-01) max-age=60, stale-while-revalidate=600: o navegador reusa a resposta por 1 min sem perguntar nada (a maioria das visitas de um mesmo site, no mesmo minuto, não paga NADA); depois disso, por até 10 min, ainda serve o que tem na hora enquanto revalida em paralelo (quem não implementa stale-while-revalidate cai para a validação normal, sem quebrar). Não é max-age longo e cego: o formulário muda quando o dono publica, e 1 min é curto o bastante para "editei e já vale" continuar parecendo imediato. public porque o HTML não é segredo (§ "sem checagem de origem" acima) nem varia por visitante — é o mesmo snippet pra qualquer um que peça aquele formID. ETag é o hash dos bytes já montados (embedCache, por formID) — forte (mesmo conteúdo byte a byte, mesmo valor) e barato de comparar: o Go só lê o If-None-Match do pedido, nunca remonta o snippet para decidir o 304. # Comparação FRACA, nunca == (achado em hml, 2026-10-01) A borda (nginx) comprime o embed.js on-the-fly e, ao fazer isso, REBAIXA o ETag forte que o runtime manda para fraco (W/"…") — comportamento padrão do módulo de gzip dela, que não garante equivalência byte-a-byte entre o corpo comprimido e o cru. O navegador devolve esse W/"…" no If-None-Match da revisita; comparar com == contra o etag FORTE deste handler nunca batia, e toda revisita baixava o corpo inteiro de novo — a compressão/cache da rodada 1/2 chegavam a produção, só a comparação estava errada. ifNoneMatchCasa (RFC 9110 §13.1.2) ignora o prefixo W/ de QUALQUER lado — é a única comparação válida para If-None-Match (a forte só existiria para If-Match/Range), então o mesmo código já cobre um Caddy que algum dia fizer o mesmo rebaixamento.

Exemplo

curl -X GET 'https://api.pages.asender.net/f/formID_AQUI/embed.js' \
  -H 'Authorization: Bearer SEU_TOKEN'

favicon.ico

GET /favicon.ico sem schema na spec

redireciona (302) para o favicon do site (o de SEO, senão o da marca) quando o dono do host configurou um https; senão para o padrão `/favicon.svg`.

Onde é usadarota pública, pedida pelo navegador e pelo buscador por convenção.
Efeitosuma leitura no asender_pages_api (quando há fonte). 302 e não 301: o favicon do site muda quando o cliente troca a marca, e um 301 ficaria gravado no navegador do visitante para sempre. Falha na consulta cai no padrão — favicon nunca vale um erro.

Exemplo

curl -X GET 'https://api.pages.asender.net/favicon.ico' \
  -H 'Authorization: Bearer SEU_TOKEN'

favicon.svg

GET /favicon.svg sem schema na spec

o SVG fixo, cacheável por um dia.

Onde é usadarota pública; é o <link rel="icon"> da página sem favicon (seo.go do renderizador).
Efeitosnenhum.

Exemplo

curl -X GET 'https://api.pages.asender.net/favicon.svg' \
  -H 'Authorization: Bearer SEU_TOKEN'

formularios

GET /v1/formularios/{formID}/envios sem schema na spec

GET /v1/formularios/{formID}/envios?limite=&cursor=&desde=&ate=.

Onde é usadaa tela de leads da ferramenta de Páginas/Formulários.
Efeitosuma consulta; uma abertura de AES-GCM por linha; um registro de acesso a PII no log. Devolve {"envios": [...], "proximo_cursor": ""} — cursor vazio significa que acabou. A alternativa (omitir o campo no fim) obrigaria o cliente a distinguir ausente de vazio, e é o tipo de sutileza que vira laço infinito na tela.

Exemplo

curl -X GET 'https://api.pages.asender.net/v1/formularios/formID_AQUI/envios' \
  -H 'Authorization: Bearer SEU_TOKEN'
GET /v1/formularios/{formID}/envios.csv sem schema na spec

GET /v1/formularios/{formID}/envios.csv?desde=&ate=.

Onde é usadabotão "exportar" da tela de leads.
EfeitosN consultas (a exportação pagina por dentro), uma abertura de AES-GCM por linha, um registro de acesso a PII no log. # Escreve direto no ResponseWriter, sem montar o arquivo na memória Um CSV de 10.000 leads com campos livres passa de 10 MB. Montá-lo num buffer para só então enviar duplicaria isso por requisição concorrente, e o pico aconteceria justamente quando várias pessoas exportam no mesmo dia (o dia seguinte a uma campanha). O encoding/csv escreve incrementalmente. O custo dessa escolha é honesto e está registrado em falharNoMeio: depois do primeiro byte não há mais como trocar o status.

Exemplo

curl -X GET 'https://api.pages.asender.net/v1/formularios/formID_AQUI/envios.csv' \
  -H 'Authorization: Bearer SEU_TOKEN'

Formulários

GET /v1/formularios

Listar formulários

Aceita sessão ou API key com forms:read. Com ?external_id= devolve só o formulário com esse id externo (lista vazia se não houver).

Onde é usadatela de domínios.
Efeitosuma leitura.

Parâmetros

nomeemobrigatóriodescrição
external_idquery nãofiltra pelo id do formulário no seu sistema

Respostas

códigosignificado
200ok
401Sem sessão / sem tenant na sessão.
403A chave não tem o escopo exigido pela rota (ou a rota não aceita chave). Ex.: `{"erro":"a chave não tem o escopo forms:write"}`.
503Não foi possível validar a chave agora (core fora do ar). Tente de novo.

Exemplo

curl -X GET 'https://api.pages.asender.net/v1/formularios' \
  -H 'Authorization: Bearer SEU_TOKEN'
POST /v1/formularios

Criar formulário

Sessão ou API key com forms:write. Envie external_id para amarrar o formulário ao do seu sistema (colisão → 409). Com o header Idempotency-Key, repetir a chamada (mesma chave e mesmo corpo) devolve 201 com o MESMO formulário e Idempotent-Replayed: true; a mesma chave com outro corpo responde 422. Para criar-ou-atualizar sem guardar o id do Asender, prefira PUT /v1/formularios/externo/{external_id}.

Onde é usadatela de domínios.
Efeitosuma escrita. # O domínio nasce NÃO verificado E é isso que impede o pior desfecho do produto: sem a prova de DNS, qualquer conta declararia banco.com.br e passaria a receber certificado TLS para esse nome — servindo uma página de captura no domínio de outra pessoa, com cadeado verde.

Parâmetros

nomeemobrigatóriodescrição
Idempotency-Keyheader nãoChave de idempotência da criação (1–255 caracteres, única por conta). Repetir a mesma chave com o MESMO corpo devolve 201 com o MESMO formulário e o header `Idempotent-Replayed: true`; com corpo diferente, 422.

Respostas

códigosignificado
201criado (ou replay idempotente — então vem `Idempotent-Replayed`)
400corpo inválido
401Sem sessão / sem tenant na sessão.
403A chave não tem o escopo exigido pela rota (ou a rota não aceita chave). Ex.: `{"erro":"a chave não tem o escopo forms:write"}`.
409nome ou `external_id` já em uso
422definição inválida, ou `Idempotency-Key` já usada com outro corpo
503Não foi possível validar a chave agora (core fora do ar). Tente de novo.

Exemplo

curl -X POST 'https://api.pages.asender.net/v1/formularios' \
  -H 'Authorization: Bearer SEU_TOKEN' \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: <uuid-v4>' \
  -d '{ }'
DELETE /v1/formularios/{id}

Apagar formulário

Sessão ou API key com forms:write. Apagar é lógico: o external_id fica livre para um novo formulário.

Onde é usadatela de domínios.
Efeitosuma escrita. Apagar TIRA a autorização de TLS, e é o único caminho para isso — de propósito: um domínio verificado que pudesse ser "desverificado" por outro caminho sairia do ar sem ninguém entender por quê.

Parâmetros

nomeemobrigatóriodescrição
idpath sim

Respostas

códigosignificado
204apagado
401Sem sessão / sem tenant na sessão.
403A chave não tem o escopo exigido pela rota (ou a rota não aceita chave). Ex.: `{"erro":"a chave não tem o escopo forms:write"}`.
404Recurso inexistente ou fora do escopo da sessão (não revela existência em outra conta).
503Não foi possível validar a chave agora (core fora do ar). Tente de novo.

Exemplo

curl -X DELETE 'https://api.pages.asender.net/v1/formularios/id_AQUI' \
  -H 'Authorization: Bearer SEU_TOKEN'
GET /v1/formularios/{id}

Obter formulário (com o snippet de embed e o rascunho do editor)

Sessão ou API key com forms:read. Formulário de outra conta responde 404.

Onde é usadatela de A/B do painel.
Efeitosuma leitura. Página SEM experimento responde 200 com o corpo vazio de variantes, e não 404: "esta página não tem teste" é uma resposta sobre o mundo, e a tela precisa dela para mostrar o botão de criar. 404 mandaria a tela tratar o caso normal como erro.

Parâmetros

nomeemobrigatóriodescrição
idpath sim

Respostas

códigosignificado
200ok
401Sem sessão / sem tenant na sessão.
403A chave não tem o escopo exigido pela rota (ou a rota não aceita chave). Ex.: `{"erro":"a chave não tem o escopo forms:write"}`.
404Recurso inexistente ou fora do escopo da sessão (não revela existência em outra conta).
503Não foi possível validar a chave agora (core fora do ar). Tente de novo.

Exemplo

curl -X GET 'https://api.pages.asender.net/v1/formularios/id_AQUI' \
  -H 'Authorization: Bearer SEU_TOKEN'
PUT /v1/formularios/{id}

Atualizar (publicar) formulário

Substitui o recurso inteiro — inclusive o layout — e ZERA o rascunho do editor na mesma escrita. É o "Publicar" do editor. Sessão ou API key com forms:write. external_id ausente mantém o atual; presente e diferente troca, se estiver livre (409 se em uso).

Onde é usadaedição.
Efeitosescreve a linha.

Parâmetros

nomeemobrigatóriodescrição
idpath sim

Respostas

códigosignificado
200ok
401Sem sessão / sem tenant na sessão.
403A chave não tem o escopo exigido pela rota (ou a rota não aceita chave). Ex.: `{"erro":"a chave não tem o escopo forms:write"}`.
404Recurso inexistente ou fora do escopo da sessão (não revela existência em outra conta).
409nome ou `external_id` já em uso
422definição inválida
503Não foi possível validar a chave agora (core fora do ar). Tente de novo.

Exemplo

curl -X PUT 'https://api.pages.asender.net/v1/formularios/id_AQUI' \
  -H 'Authorization: Bearer SEU_TOKEN' \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: <uuid-v4>' \
  -d '{ }'
GET /v1/formularios/{id}/destinos

Listar destinos do formulário (crm/marketing/webhook)

Os destinos para onde a submissão é copiada, com o mapa de campos de cada um. Anti-IDOR: um formulário de outra conta responde 404.

Onde é usadaseção "Destinos" da tela do formulário.
Efeitosuma leitura, com checagem de posse (anti-IDOR).

Parâmetros

nomeemobrigatóriodescrição
idpath sim

Respostas

códigosignificado
200ok — os destinos (com `condicao_erro` nos quebrados) e se a conta tem o Marketing. Formulário com caixa de aceite numa conta com Marketing ganha, UMA vez, o destino Marketing com a condição "caixa marcada" (preset).
401Sem sessão / sem tenant na sessão.
404Recurso inexistente ou fora do escopo da sessão (não revela existência em outra conta).

Exemplo

curl -X GET 'https://api.pages.asender.net/v1/formularios/id_AQUI/destinos' \
  -H 'Authorization: Bearer SEU_TOKEN'
PUT /v1/formularios/{id}/destinos

Substituir a lista de destinos do formulário

SUBSTITUI a lista inteira (como campos/origens). Valida tipo, mapa e a CONDIÇÃO de cada destino contra os campos atuais (campo inexistente, operador do tipo errado, opção que não existe → 422); Marketing só com "aceite marcado"; webhook exige URL http(s). O SSRF do webhook é conferido no runtime, no instante do despacho. Destino integracao NOVO ou ALTERADO é conferido no serviço de Integrações (instalação existe e é da conta; opções valem) — 422 com codigo (IntegracaoRepetida, LimiteIntegracoes, IntegracaoInvalida, IntegracaoInexistente, OpcaoInvalida) e local; serviço fora → 503 IntegracoesIndisponivel só nesse caso.

Onde é usadaseção "Destinos" da tela do formulário, ao salvar.
Efeitosapaga e regrava os destinos, atômico. A validação mora no domínio (destino.ValidarListaComCampos): tipo conhecido, mapa com alvos válidos, webhook com URL http(s), condições contra os campos do formulário e o Marketing só com "aceite marcado". O SSRF do webhook é conferido no runtime, no instante do despacho — aqui a checagem é de FORMA, a que dá mensagem clara para quem configura.

Parâmetros

nomeemobrigatóriodescrição
idpath sim

Respostas

códigosignificado
200ok
400corpo inválido
401Sem sessão / sem tenant na sessão.
404Recurso inexistente ou fora do escopo da sessão (não revela existência em outra conta).
422destino ou condição inválidos (integracao: + codigo e local)
503IntegracoesIndisponivel — só com destino integracao novo/alterado

Exemplo

curl -X PUT 'https://api.pages.asender.net/v1/formularios/id_AQUI/destinos' \
  -H 'Authorization: Bearer SEU_TOKEN' \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: <uuid-v4>' \
  -d '{ }'
DELETE /v1/formularios/{id}/rascunho

Descartar o rascunho do editor

Idempotente; o formulário no ar não muda. Formulário de outra conta responde 404.

Onde é usada"Descartar alterações" do editor-página.
Efeitoszera o rascunho; o formulário no ar não muda. Idempotente.

Parâmetros

nomeemobrigatóriodescrição
idpath sim

Respostas

códigosignificado
204descartado
401Sem sessão / sem tenant na sessão.
404Recurso inexistente ou fora do escopo da sessão (não revela existência em outra conta).

Exemplo

curl -X DELETE 'https://api.pages.asender.net/v1/formularios/id_AQUI/rascunho' \
  -H 'Authorization: Bearer SEU_TOKEN'
PUT /v1/formularios/{id}/rascunho

Salvar o rascunho do editor (sem publicar)

Guarda o corpo do PUT do formulário SEM publicá-lo — o formulário no ar não muda. Só a FORMA é conferida (campos desconhecidos → 400, teto de 512 KiB → 413): rascunho pode estar incompleto; as regras rodam no publicar. Anti-IDOR: formulário de outra conta responde 404.

Onde é usadabotão "Salvar rascunho" do editor-página (e o salvamento automático).
Efeitosescreve rascunho; o formulário no ar NÃO muda. # Só a FORMA é conferida Rascunho existe para guardar trabalho pela metade — um campo sem rótulo, uma lista sem opção. Recusá-lo pelas regras de publicação faria a pessoa perder o que fez justamente quando ainda não terminou. A borda confere que o corpo é o do PUT (nenhum campo desconhecido, tipos certos, dentro do teto) e grava a forma DECODIFICADA re-serializada: chave estranha não chega ao banco. Toda a regra de domínio roda no PUBLICAR.

Parâmetros

nomeemobrigatóriodescrição
idpath sim

Respostas

códigosignificado
200salvo
400corpo inválido
401Sem sessão / sem tenant na sessão.
404Recurso inexistente ou fora do escopo da sessão (não revela existência em outra conta).
413rascunho acima do teto

Exemplo

curl -X PUT 'https://api.pages.asender.net/v1/formularios/id_AQUI/rascunho' \
  -H 'Authorization: Bearer SEU_TOKEN' \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: <uuid-v4>' \
  -d '{ }'
GET /v1/formularios/externo/{external_id}

Obter formulário pelo id externo

Sessão ou API key com forms:read. Procura entre os formulários não apagados da conta.

Onde é usadaintegrações por chave (forms:read) e sessão.
Efeitosuma leitura.

Parâmetros

nomeemobrigatóriodescrição
external_idpath simid do formulário no sistema do integrador (campo `external_id`).

Respostas

códigosignificado
200ok
401Sem sessão / sem tenant na sessão.
403A chave não tem o escopo exigido pela rota (ou a rota não aceita chave). Ex.: `{"erro":"a chave não tem o escopo forms:write"}`.
404Recurso inexistente ou fora do escopo da sessão (não revela existência em outra conta).
503Não foi possível validar a chave agora (core fora do ar). Tente de novo.

Exemplo

curl -X GET 'https://api.pages.asender.net/v1/formularios/externo/external_id_AQUI' \
  -H 'Authorization: Bearer SEU_TOKEN'
PUT /v1/formularios/externo/{external_id}

Criar ou atualizar formulário pelo id externo (upsert idempotente)

Sessão ou API key com forms:write. Se não existe formulário com esse external_id na conta, cria (201); se existe, SUBSTITUI a definição (200), como o PUT /v1/formularios/{id}. Rodar de novo com o mesmo corpo não muda nada. O external_id do path é o que vale.

Onde é usadaintegrações por chave (forms:write) e sessão.
Efeitosescreve a linha; atualizar zera o rascunho do editor.

Parâmetros

nomeemobrigatóriodescrição
external_idpath simid do formulário no sistema do integrador (campo `external_id`).

Respostas

códigosignificado
200atualizado
201criado
400corpo inválido
401Sem sessão / sem tenant na sessão.
403A chave não tem o escopo exigido pela rota (ou a rota não aceita chave). Ex.: `{"erro":"a chave não tem o escopo forms:write"}`.
409nome já em uso
422definição inválida
503Não foi possível validar a chave agora (core fora do ar). Tente de novo.

Exemplo

curl -X PUT 'https://api.pages.asender.net/v1/formularios/externo/external_id_AQUI' \
  -H 'Authorization: Bearer SEU_TOKEN' \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: <uuid-v4>' \
  -d '{ }'

Geração por IA

POST /v1/ia/gerar-pagina

Gerar um rascunho de página por IA

Produz um rascunho a partir de um pedido em linguagem natural — SEMPRE um documento v2 (sem cabeçalho forçado, sem form_id: o formulário é escolhido no editor). O html que o modelo devolver é REMOVIDO e listado em removidos; item fora do contrato é descartado. NÃO grava nada. Responde 503 quando o provedor de IA não está configurado neste ambiente.

Onde é usadabotão "gerar com IA" do editor.
Efeitosuma chamada externa; NÃO grava nada. # Não grava: devolve o rascunho Gravar direto faria o modelo publicar na conta do cliente. O que sai daqui é uma sugestão que a pessoa vê no editor e salva se quiser — a versão continua nascendo do gesto dela. # Sem provedor, a rota DEGRADA 503 e não 500: é configuração ausente, e a distinção separa "o produto não tem essa função ligada" de "o produto quebrou". O resto do serviço segue de pé — nenhuma página deixa de ser servida porque não há chave de IA (piso 10).

Respostas

códigosignificado
200rascunho gerado
401Sem sessão / sem tenant na sessão.
422pedido vazio/longo demais
502o provedor falhou ao gerar
503geração por IA desligada neste ambiente

Exemplo

curl -X POST 'https://api.pages.asender.net/v1/ia/gerar-pagina' \
  -H 'Authorization: Bearer SEU_TOKEN' \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: <uuid-v4>' \
  -d '{ }'

Links curtos

POST /v1/links

Criar link (código curto + UTM + parâmetros + repasse)

Onde é usadatela de domínios.
Efeitosuma escrita. # O domínio nasce NÃO verificado E é isso que impede o pior desfecho do produto: sem a prova de DNS, qualquer conta declararia banco.com.br e passaria a receber certificado TLS para esse nome — servindo uma página de captura no domínio de outra pessoa, com cadeado verde.

Respostas

códigosignificado
200o editor já tinha criado este link (origem pagina, mesmo bloco e destino)
201criado
400corpo inválido
401Sem sessão / sem tenant na sessão.
409endereço /r/ já em uso
422destino/configuração/domínio inválidos

Exemplo

curl -X POST 'https://api.pages.asender.net/v1/links' \
  -H 'Authorization: Bearer SEU_TOKEN' \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: <uuid-v4>' \
  -d '{ }'
POST /v1/links/previa

Prévia da URL final (mesma regra do redirect)

Onde é usadaeditor de link do painel (prévia ao vivo, enquanto a pessoa digita).
Efeitosnenhum — não grava nada. A prévia vem do servidor, e não de uma terceira cópia da regra em TypeScript: o que a pessoa vê é exatamente o que o clique vai fazer.

Respostas

códigosignificado
200ok
401Sem sessão / sem tenant na sessão.
422destino/configuração inválidos

Exemplo

curl -X POST 'https://api.pages.asender.net/v1/links/previa' \
  -H 'Authorization: Bearer SEU_TOKEN' \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: <uuid-v4>' \
  -d '{ }'
GET /v1/redirects

Listar links curtos

Onde é usadatela de domínios.
Efeitosuma leitura.

Respostas

códigosignificado
200ok
401Sem sessão / sem tenant na sessão.

Exemplo

curl -X GET 'https://api.pages.asender.net/v1/redirects' \
  -H 'Authorization: Bearer SEU_TOKEN'
POST /v1/redirects

Criar link curto

Onde é usadatela de domínios.
Efeitosuma escrita. # O domínio nasce NÃO verificado E é isso que impede o pior desfecho do produto: sem a prova de DNS, qualquer conta declararia banco.com.br e passaria a receber certificado TLS para esse nome — servindo uma página de captura no domínio de outra pessoa, com cadeado verde.

Respostas

códigosignificado
201criado
401Sem sessão / sem tenant na sessão.
409slug já em uso
422destino/slug inválidos

Exemplo

curl -X POST 'https://api.pages.asender.net/v1/redirects' \
  -H 'Authorization: Bearer SEU_TOKEN' \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: <uuid-v4>' \
  -d '{ }'
DELETE /v1/redirects/{id}

Apagar um link curto

Onde é usadatela de domínios.
Efeitosuma escrita. Apagar TIRA a autorização de TLS, e é o único caminho para isso — de propósito: um domínio verificado que pudesse ser "desverificado" por outro caminho sairia do ar sem ninguém entender por quê.

Parâmetros

nomeemobrigatóriodescrição
idpath sim

Respostas

códigosignificado
204apagado
401Sem sessão / sem tenant na sessão.
404Recurso inexistente ou fora do escopo da sessão (não revela existência em outra conta).

Exemplo

curl -X DELETE 'https://api.pages.asender.net/v1/redirects/id_AQUI' \
  -H 'Authorization: Bearer SEU_TOKEN'
GET /v1/redirects/{id}

Obter um link curto

Onde é usadatela de A/B do painel.
Efeitosuma leitura. Página SEM experimento responde 200 com o corpo vazio de variantes, e não 404: "esta página não tem teste" é uma resposta sobre o mundo, e a tela precisa dela para mostrar o botão de criar. 404 mandaria a tela tratar o caso normal como erro.

Parâmetros

nomeemobrigatóriodescrição
idpath sim

Respostas

códigosignificado
200ok
401Sem sessão / sem tenant na sessão.
404Recurso inexistente ou fora do escopo da sessão (não revela existência em outra conta).

Exemplo

curl -X GET 'https://api.pages.asender.net/v1/redirects/id_AQUI' \
  -H 'Authorization: Bearer SEU_TOKEN'
PUT /v1/redirects/{id}

Atualizar um link curto

Onde é usadaedição.
Efeitosescreve a linha.

Parâmetros

nomeemobrigatóriodescrição
idpath sim

Respostas

códigosignificado
200ok
401Sem sessão / sem tenant na sessão.
404Recurso inexistente ou fora do escopo da sessão (não revela existência em outra conta).
409slug já em uso

Exemplo

curl -X PUT 'https://api.pages.asender.net/v1/redirects/id_AQUI' \
  -H 'Authorization: Bearer SEU_TOKEN' \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: <uuid-v4>' \
  -d '{ }'
GET /v1/redirects/{id}/cliques

Contagem de cliques do link

Onde é usadarelatório da tela de links curtos.
Efeitosuma leitura. Devolve o CONTADOR, e não uma lista de eventos: a pergunta é sempre "quantos", nunca "quais". Uma linha por clique transformaria o caminho mais quente do redirect num INSERT por requisição.

Parâmetros

nomeemobrigatóriodescrição
idpath sim

Respostas

códigosignificado
200ok
401Sem sessão / sem tenant na sessão.
404Recurso inexistente ou fora do escopo da sessão (não revela existência em outra conta).

Exemplo

curl -X GET 'https://api.pages.asender.net/v1/redirects/id_AQUI/cliques' \
  -H 'Authorization: Bearer SEU_TOKEN'

Marca

GET /v1/configuracoes/marca

Obter a marca da conta

Onde é usadatela de A/B do painel.
Efeitosuma leitura. Página SEM experimento responde 200 com o corpo vazio de variantes, e não 404: "esta página não tem teste" é uma resposta sobre o mundo, e a tela precisa dela para mostrar o botão de criar. 404 mandaria a tela tratar o caso normal como erro.

Respostas

códigosignificado
200ok
401Sem sessão / sem tenant na sessão.

Exemplo

curl -X GET 'https://api.pages.asender.net/v1/configuracoes/marca' \
  -H 'Authorization: Bearer SEU_TOKEN'
PUT /v1/configuracoes/marca

Definir a marca da conta

Onde é usadaclientes do contrato anterior à 037. O corpo NÃO tem tenant_id: a conta vem da sessão, e o campo desconhecido é recusado (400) — o silêncio faria quem tentou achar que funcionou.

Respostas

códigosignificado
200gravada
400corpo inválido
401Sem sessão / sem tenant na sessão.
403sem o papel de editor no Pages (PEP nível 20)
422valor inválido — a mensagem nomeia o campo

Exemplo

curl -X PUT 'https://api.pages.asender.net/v1/configuracoes/marca' \
  -H 'Authorization: Bearer SEU_TOKEN' \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: <uuid-v4>' \
  -d '{ }'
GET /v1/configuracoes/marca/efetivo

Kit de marca efetivo de uma página/host

Fallback página → domínio → padrão. Contrato único de consumo do construtor (autopreenchimento) e da geração por IA. Conta sem kit: origem: vazio, kit: null.

Onde é usadaautopreenchimento dos widgets do construtor e a geração por IA.

Parâmetros

nomeemobrigatóriodescrição
paginaquery nãoid público da página
dominioquery nãohost; ausente = o da página

Respostas

códigosignificado
200ok
401Sem sessão / sem tenant na sessão.

Exemplo

curl -X GET 'https://api.pages.asender.net/v1/configuracoes/marca/efetivo' \
  -H 'Authorization: Bearer SEU_TOKEN'
GET /v1/configuracoes/marca/kits

Listar os kits de marca da conta

O padrão primeiro; cada kit traz quantos domínios/páginas o usam. Leitura no nível 0 (quem acessa o Pages).

Onde é usadatela de marca e seletores de kit (domínios, editor).

Respostas

códigosignificado
200ok
401Sem sessão / sem tenant na sessão.

Exemplo

curl -X GET 'https://api.pages.asender.net/v1/configuracoes/marca/kits' \
  -H 'Authorization: Bearer SEU_TOKEN'
POST /v1/configuracoes/marca/kits

Criar (ou duplicar) um kit de marca

O primeiro kit da conta nasce padrão. duplicar_de copia os dados de outro kit da conta. Exige papel de editor.

Onde é usadabotões "novo kit" e "duplicar" da tela.

Respostas

códigosignificado
201criado
400corpo inválido (inclui campo desconhecido, ex. tenant_id)
401Sem sessão / sem tenant na sessão.
403sem o papel de editor no Pages
404kit de origem (duplicar_de) não existe nesta conta
409já existe um kit com esse nome
422campo inválido (a mensagem nomeia o campo) ou limite de kits

Exemplo

curl -X POST 'https://api.pages.asender.net/v1/configuracoes/marca/kits' \
  -H 'Authorization: Bearer SEU_TOKEN' \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: <uuid-v4>' \
  -d '{ }'
DELETE /v1/configuracoes/marca/kits/{id}

Apagar um kit de marca

Recusado (409) para o kit padrão e para kit em uso por domínio/página — reatribua antes.

Onde é usadamenu do kit na tela.

Parâmetros

nomeemobrigatóriodescrição
idpath sim

Respostas

códigosignificado
204apagado
401Sem sessão / sem tenant na sessão.
403sem o papel de editor no Pages
404não existe — ou é de outra conta
409kit padrão ou em uso

Exemplo

curl -X DELETE 'https://api.pages.asender.net/v1/configuracoes/marca/kits/id_AQUI' \
  -H 'Authorization: Bearer SEU_TOKEN'
GET /v1/configuracoes/marca/kits/{id}

Obter um kit de marca

Onde é usadaformulário de edição do kit.

Parâmetros

nomeemobrigatóriodescrição
idpath sim

Respostas

códigosignificado
200ok
401Sem sessão / sem tenant na sessão.
404não existe — ou é de outra conta (mesma resposta)

Exemplo

curl -X GET 'https://api.pages.asender.net/v1/configuracoes/marca/kits/id_AQUI' \
  -H 'Authorization: Bearer SEU_TOKEN'
PUT /v1/configuracoes/marca/kits/{id}

Salvar um kit de marca (nome e dados de uma vez)

PUT: o que não vier é apagado. A resposta traz os valores NORMALIZADOS (URL canônica de rede, E.164, CEP, documento só com dígitos/letras). Se o kit é o padrão, pages.marcas é reprojetada na mesma transação.

Onde é usadabotão salvar da tela de marca. duplicar_de aqui é recusado (400).

Parâmetros

nomeemobrigatóriodescrição
idpath sim

Respostas

códigosignificado
200gravado
400corpo inválido (campo desconhecido; duplicar_de só na criação)
401Sem sessão / sem tenant na sessão.
403sem o papel de editor no Pages
404não existe — ou é de outra conta
409já existe um kit com esse nome
422campo inválido — a mensagem nomeia o campo

Exemplo

curl -X PUT 'https://api.pages.asender.net/v1/configuracoes/marca/kits/id_AQUI' \
  -H 'Authorization: Bearer SEU_TOKEN' \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: <uuid-v4>' \
  -d '{ }'
POST /v1/configuracoes/marca/kits/{id}/padrao

Tornar um kit o padrão da conta

O padrão vale para todo domínio/página sem kit próprio.

Onde é usadamenu do kit na tela.

Parâmetros

nomeemobrigatóriodescrição
idpath sim

Respostas

códigosignificado
200ok
401Sem sessão / sem tenant na sessão.
403sem o papel de editor no Pages
404não existe — ou é de outra conta

Exemplo

curl -X POST 'https://api.pages.asender.net/v1/configuracoes/marca/kits/id_AQUI/padrao' \
  -H 'Authorization: Bearer SEU_TOKEN' \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: <uuid-v4>' \
  -d '{ }'
GET /v1/configuracoes/marca/vinculos

Onde cada kit vale (domínios e páginas)

Todo domínio vivo da conta com o kit escolhido (kit_id vazio = padrão) e as páginas com kit próprio.

Onde é usadalista de páginas do painel.
Efeitosuma leitura sob o tenant da sessão.

Respostas

códigosignificado
200ok
401Sem sessão / sem tenant na sessão.

Exemplo

curl -X GET 'https://api.pages.asender.net/v1/configuracoes/marca/vinculos' \
  -H 'Authorization: Bearer SEU_TOKEN'
PUT /v1/configuracoes/marca/vinculos/dominios/{id}

Escolher o kit de um domínio

kit_id vazio volta ao padrão da conta.

Onde é usadaseletor de kit por domínio.

Parâmetros

nomeemobrigatóriodescrição
idpath simid público do domínio

Respostas

códigosignificado
204gravado
400corpo inválido
401Sem sessão / sem tenant na sessão.
403sem o papel de editor no Pages
404domínio ou kit não existe nesta conta

Exemplo

curl -X PUT 'https://api.pages.asender.net/v1/configuracoes/marca/vinculos/dominios/id_AQUI' \
  -H 'Authorization: Bearer SEU_TOKEN' \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: <uuid-v4>' \
  -d '{ }'
PUT /v1/configuracoes/marca/vinculos/paginas/{id}

Escolher o kit de uma página

kit_id vazio volta ao kit do domínio (ou ao padrão).

Onde é usadaseletor de kit por página.

Parâmetros

nomeemobrigatóriodescrição
idpath simid público da página

Respostas

códigosignificado
204gravado
400corpo inválido
401Sem sessão / sem tenant na sessão.
403sem o papel de editor no Pages
404página ou kit não existe nesta conta

Exemplo

curl -X PUT 'https://api.pages.asender.net/v1/configuracoes/marca/vinculos/paginas/id_AQUI' \
  -H 'Authorization: Bearer SEU_TOKEN' \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: <uuid-v4>' \
  -d '{ }'

Mídia

GET /v1/midias

Listar a galeria da conta

Onde é usadatela de domínios.
Efeitosuma leitura.

Parâmetros

nomeemobrigatóriodescrição
limitequery nãoteto de itens

Respostas

códigosignificado
200ok
401Sem sessão / sem tenant na sessão.
403A chave não tem o escopo exigido pela rota (ou a rota não aceita chave). Ex.: `{"erro":"a chave não tem o escopo forms:write"}`.
503Não foi possível validar a chave agora (core fora do ar). Tente de novo.

Exemplo

curl -X GET 'https://api.pages.asender.net/v1/midias' \
  -H 'Authorization: Bearer SEU_TOKEN'
POST /v1/midias

Enviar uma imagem (multipart)

Envie o arquivo no campo arquivo de um multipart/form-data. O arquivo passa por antivírus antes de ser gravado: infectado = 422; antivírus configurado e fora do ar = 503 com Retry-After (fail-closed — tente de novo).

Onde é usadabotão de upload do editor.
Efeitosescreve no disco e no banco. # A ordem é DISCO e depois BANCO Se o disco falhar, nada foi gravado e o erro é honesto. A ordem inversa deixaria uma linha apontando para um arquivo inexistente, e o visitante receberia erro numa página publicada. O caso duplicado não grava nada: a mídia já está lá, com os mesmos bytes.

Respostas

códigosignificado
200já existia (deduplicada por conteúdo)
201enviada
400campo `arquivo` ausente
401Sem sessão / sem tenant na sessão.
403A chave não tem o escopo exigido pela rota (ou a rota não aceita chave). Ex.: `{"erro":"a chave não tem o escopo forms:write"}`.
413arquivo grande demais
422arquivo recusado pelo antivírus
429Escritas demais por esta chave (teto por minuto: 60 nas escritas de `pages:write`, 20 nas de `pages:publish`). Header `Retry-After: 60`.
503chave indisponível OU antivírus sem responder (reenvie; vem `Retry-After`)

Exemplo

curl -X POST 'https://api.pages.asender.net/v1/midias' \
  -H 'Authorization: Bearer SEU_TOKEN' \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: <uuid-v4>' \
  -d '{ }'
DELETE /v1/midias/{id}

Apagar uma imagem (soft delete — o arquivo fica; o suporte restaura)

Some da galeria e do endereço público, mas a linha e o ARQUIVO (original e variantes) permanecem no disco; o painel master restaura (ADR-0112). SÓ sessão.

Onde é usadatela de domínios.
Efeitosuma escrita. Apagar TIRA a autorização de TLS, e é o único caminho para isso — de propósito: um domínio verificado que pudesse ser "desverificado" por outro caminho sairia do ar sem ninguém entender por quê.

Parâmetros

nomeemobrigatóriodescrição
idpath sim

Respostas

códigosignificado
204apagada (soft)
401Sem sessão / sem tenant na sessão.
404Recurso inexistente ou fora do escopo da sessão (não revela existência em outra conta).

Exemplo

curl -X DELETE 'https://api.pages.asender.net/v1/midias/id_AQUI' \
  -H 'Authorization: Bearer SEU_TOKEN'
POST /v1/midias/de-url

Importar uma imagem por URL

Baixa a imagem da URL (com defesa contra SSRF). Desligado quando a importação por URL não está configurada (503).

Onde é usadacolar o endereço de uma imagem no editor.
Efeitosuma requisição a servidor de terceiro; escreve no disco e no banco. # Esta é a rota mais perigosa do serviço Ela faz o SERVIDOR buscar um endereço que o USUÁRIO escolheu — a definição de SSRF. Sem as guardas, http://169.254.169.254/latest/meta-data/ devolveria as credenciais da instância, e http://redis-interno:6379/ alcançaria um serviço que nunca deveria ver tráfego de fora. Toda a defesa está em domain/midia: allowlist de esquema e porta, recusa de IP interno, conferência REPETIDA no momento de discar (contra DNS rebinding), zero redirecionamento e teto de tamanho durante a leitura.

Respostas

códigosignificado
200já existia
201importada
401Sem sessão / sem tenant na sessão.
403A chave não tem o escopo exigido pela rota (ou a rota não aceita chave). Ex.: `{"erro":"a chave não tem o escopo forms:write"}`.
422URL inválida / bloqueada
429Escritas demais por esta chave (teto por minuto: 60 nas escritas de `pages:write`, 20 nas de `pages:publish`). Header `Retry-After: 60`.
503importação por URL desligada

Exemplo

curl -X POST 'https://api.pages.asender.net/v1/midias/de-url' \
  -H 'Authorization: Bearer SEU_TOKEN' \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: <uuid-v4>' \
  -d '{ }'
GET /v1/public/midias/{id}

Servir uma imagem publicada (anônimo)

Rota PÚBLICA — quem pede é o navegador de um visitante numa página no ar, sem sessão. Devolve os bytes da imagem.

Onde é usadaa tag <img> de uma landing publicada.
Efeitosabre e transmite o arquivo. # Sem sessão, e por isso com cuidado extra O id é o único parâmetro, e ele é resolvido no BANCO antes de tocar o disco: o caminho do arquivo nunca é montado com o que veio da URL. Path traversal não tem por onde entrar — e o armazenamento recusa id fora do formato de qualquer jeito, como segunda linha. # X-Content-Type-Options: nosniff é obrigatório aqui O tipo é o DETECTADO no upload, e a allowlist só tem imagem. Sem nosniff, o navegador pode reinterpretar o conteúdo e executar como outra coisa — e a allowlist de tipos perderia o sentido no último metro.

Parâmetros

nomeemobrigatóriodescrição
idpath sim

Respostas

códigosignificado
200os bytes da imagem
404mídia não encontrada

Exemplo

curl -X GET 'https://api.pages.asender.net/v1/public/midias/id_AQUI' \
  -H 'Authorization: Bearer SEU_TOKEN'
GET /v1/public/midias/{id}/w/{largura}

Servir uma variante redimensionada da imagem (anônimo)

Rota PÚBLICA usada pelo srcset das páginas publicadas. Reduz a imagem para a largura pedida (nunca amplia; proporção e orientação EXIF preservadas) e a entrega em WebP quando o Accept anuncia image/webp (senão JPEG/PNG). A largura é FECHADA numa allowlist — qualquer outro valor é 404, sem tocar no banco nem no disco. Gerada uma vez e guardada; resposta com Cache-Control: public, max-age=31536000, immutable, Vary: Accept, X-Content-Type-Options: nosniff e ETag por (mídia, largura, formato). GIF, SVG, AVIF e imagens acima de 40 megapixels respondem o ORIGINAL (mesmos cabeçalhos da rota do original, inclusive a CSP do SVG).

Onde é usadao srcset do <img> de uma landing publicada (renderizador do runtime, imagens_v2.go).
Efeitosuma leitura no banco; leitura do disco; no primeiro pedido, um encode e uma escrita no disco. Largura fora da allowlist é 404 ANTES de qualquer acesso ao banco ou ao disco: é o que impede o moedor de CPU (ver midia.Larguras). Tudo o que dá errado DEPOIS disso (tipo sem variante, imagem acima do teto, falha do encoder) serve o ORIGINAL — a imagem da landing nunca quebra por causa da otimização.

Parâmetros

nomeemobrigatóriodescrição
idpath sim
largurapath sim
Acceptheader nãoCom `image/webp`, a variante sai em WebP.

Respostas

códigosignificado
200os bytes da variante (ou do original
404largura fora da allowlist

Exemplo

curl -X GET 'https://api.pages.asender.net/v1/public/midias/id_AQUI/w/largura_AQUI' \
  -H 'Authorization: Bearer SEU_TOKEN'

Páginas

GET /v1/paginas

Listar as páginas da conta

Onde é usadatela de domínios.
Efeitosuma leitura.

Respostas

códigosignificado
200ok
401Sem sessão / sem tenant na sessão.
403A chave não tem o escopo exigido pela rota (ou a rota não aceita chave). Ex.: `{"erro":"a chave não tem o escopo forms:write"}`.
503Não foi possível validar a chave agora (core fora do ar). Tente de novo.

Exemplo

curl -X GET 'https://api.pages.asender.net/v1/paginas' \
  -H 'Authorization: Bearer SEU_TOKEN'
POST /v1/paginas

Criar página

Cria uma página. Um dominio próprio exige posse PROVADA do host (422 se não verificado); um template com conteúdo privilegiado (bloco ou item html, HTML cru) exige nível pelo PEP (403). O template pode ser v1 ou v2 (ver TemplatePagina); recusa de v2 responde 422 com codigo e caminho (ErroConteudo).

Onde é usadatela de domínios.
Efeitosuma escrita. # O domínio nasce NÃO verificado E é isso que impede o pior desfecho do produto: sem a prova de DNS, qualquer conta declararia banco.com.br e passaria a receber certificado TLS para esse nome — servindo uma página de captura no domínio de outra pessoa, com cadeado verde.

Respostas

códigosignificado
201criada
400corpo inválido
401Sem sessão / sem tenant na sessão.
403conteúdo privilegiado (html) sem nível
409já existe página nesse endereço
422título ausente / domínio não verificado / template fora do contrato (v2: com `codigo` e `caminho`)
429Escritas demais por esta chave (teto por minuto: 60 nas escritas de `pages:write`, 20 nas de `pages:publish`). Header `Retry-After: 60`.
503Não foi possível validar a chave agora (core fora do ar). Tente de novo.

Exemplo

curl -X POST 'https://api.pages.asender.net/v1/paginas' \
  -H 'Authorization: Bearer SEU_TOKEN' \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: <uuid-v4>' \
  -d '{ }'
DELETE /v1/paginas/{id}

Arquivar a página (soft delete — despublica e esconde; o suporte restaura)

SÓ sessão (chave de API é recusada: chave nunca apaga). Despublica a página e a esconde da lista; as versões e o rascunho ficam guardados e o painel master restaura (volta despublicada). Nada é destruído (ADR-0112). Página inexistente, de outra conta ou já arquivada responde 404 (indistinguível).

Onde é usadatela de domínios.
Efeitosuma escrita. Apagar TIRA a autorização de TLS, e é o único caminho para isso — de propósito: um domínio verificado que pudesse ser "desverificado" por outro caminho sairia do ar sem ninguém entender por quê.

Parâmetros

nomeemobrigatóriodescrição
idpath simid da página; o acesso é conferido contra a sessão (deny-default).

Respostas

códigosignificado
204arquivada
401Sem sessão / sem tenant na sessão.
404Recurso inexistente ou fora do escopo da sessão (não revela existência em outra conta).
503arquivar indisponível neste serviço

Exemplo

curl -X DELETE 'https://api.pages.asender.net/v1/paginas/id_AQUI' \
  -H 'Authorization: Bearer SEU_TOKEN'
GET /v1/paginas/{id}

Obter uma página (com suas versões)

Onde é usadatela de A/B do painel.
Efeitosuma leitura. Página SEM experimento responde 200 com o corpo vazio de variantes, e não 404: "esta página não tem teste" é uma resposta sobre o mundo, e a tela precisa dela para mostrar o botão de criar. 404 mandaria a tela tratar o caso normal como erro.

Parâmetros

nomeemobrigatóriodescrição
idpath simid da página; o acesso é conferido contra a sessão (deny-default).

Respostas

códigosignificado
200ok
401Sem sessão / sem tenant na sessão.
403A chave não tem o escopo exigido pela rota (ou a rota não aceita chave). Ex.: `{"erro":"a chave não tem o escopo forms:write"}`.
404Recurso inexistente ou fora do escopo da sessão (não revela existência em outra conta).
503Não foi possível validar a chave agora (core fora do ar). Tente de novo.

Exemplo

curl -X GET 'https://api.pages.asender.net/v1/paginas/id_AQUI' \
  -H 'Authorization: Bearer SEU_TOKEN'
PATCH /v1/paginas/{id}

Editar slug, domínio ou título

Onde é usadatela de páginas do painel (renomear) e configurações da página.
Efeitosuma escrita. # Só o que veio é tocado nil é "não mandado" e &"" é "mandado vazio". Sem essa distinção, editar o título apagaria o endereço público da página — e ela sairia do ar sem ninguém ter pedido. # Validação IGUAL à da criação O slug e o título passam pelas mesmas regras do Criar. Um PATCH mais frouxo seria a porta dos fundos: bastaria criar válido e editar para inválido para pôr no ar um endereço que a criação recusa.

Parâmetros

nomeemobrigatóriodescrição
idpath simid da página; o acesso é conferido contra a sessão (deny-default).

Respostas

códigosignificado
200atualizada
400corpo inválido / nenhum campo
401Sem sessão / sem tenant na sessão.
404Recurso inexistente ou fora do escopo da sessão (não revela existência em outra conta).
409endereço em uso
422domínio não verificado / título vazio

Exemplo

curl -X PATCH 'https://api.pages.asender.net/v1/paginas/id_AQUI' \
  -H 'Authorization: Bearer SEU_TOKEN' \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: <uuid-v4>' \
  -d '{ }'
PUT /v1/paginas/{id}/dominio

Mover a página de domínio e/ou de caminho

Sessão ou API key com pages:write. Troca o endereço público da página: dominio_id é o id do domínio da conta (GET /v1/dominios); "" = host da plataforma. caminho é o slug de um segmento (^[a-z0-9]+(-[a-z0-9]+)*$); "" mantém o slug atual. Se a página já foi publicada alguma vez, o endereço antigo passa a responder 301 (308 em métodos que não GET/HEAD) para o novo. Repetir o mesmo corpo é idempotente (alterado:false). Página ou domínio inexistente, ou de outra conta, responde 404 (indistinguível).

Onde é usadaPUT /v1/paginas/{id}/dominio (sessão OU chave com pages:write).
Efeitosuma transação.

Parâmetros

nomeemobrigatóriodescrição
idpath sim

Respostas

códigosignificado
200movida (ou já estava lá)
400corpo inválido
401Sem sessão / sem tenant na sessão.
403A chave não tem o escopo exigido pela rota (ou a rota não aceita chave). Ex.: `{"erro":"a chave não tem o escopo forms:write"}`.
404Recurso inexistente ou fora do escopo da sessão (não revela existência em outra conta).
409`motivo`: `caminho_em_uso` (o endereço já é de outra página) ou `pagina_inicial` (a página é a home do domínio de origem)
422`motivo`: `dominio_nao_verificado` ou `caminho_invalido`
503Não foi possível validar a chave agora (core fora do ar). Tente de novo.

Exemplo

curl -X PUT 'https://api.pages.asender.net/v1/paginas/id_AQUI/dominio' \
  -H 'Authorization: Bearer SEU_TOKEN' \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: <uuid-v4>' \
  -d '{ }'
POST /v1/paginas/{id}/midias/copiar

Tentar copiar de novo as mídias que ficaram na URL original

Roda a cópia de mídia sobre a ÚLTIMA versão da página (a mídia que já é da conta vira ja_na_conta, sem duplicar). Se alguma URL mudou, grava uma versão nova (não publica); senão responde 200 sem versão nova.

Onde é usadabotão "tentar copiar de novo" do relatório de importação.
Efeitosdownloads, galeria, no máximo uma versão nova.

Parâmetros

nomeemobrigatóriodescrição
idpath simid da página; o acesso é conferido contra a sessão (deny-default).

Respostas

códigosignificado
200nada mudou
201versão nova gravada
401Sem sessão / sem tenant na sessão.
403conteúdo privilegiado (html) sem nível
404Recurso inexistente ou fora do escopo da sessão (não revela existência em outra conta).
429Escritas demais por esta chave (teto por minuto: 60 nas escritas de `pages:write`, 20 nas de `pages:publish`). Header `Retry-After: 60`.
503a cópia de mídia está indisponível neste servidor

Exemplo

curl -X POST 'https://api.pages.asender.net/v1/paginas/id_AQUI/midias/copiar' \
  -H 'Authorization: Bearer SEU_TOKEN' \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: <uuid-v4>' \
  -d '{ }'
POST /v1/paginas/{id}/publicar

Publicar uma versão

Publica a versão indicada; na sessão, sem corpo publica a última salva. API key (pages:publish): o corpo com versao é OBRIGATÓRIO (422 sem ele) — a chave publica uma versão que ela mesma identificou, nunca "a mais recente" às cegas (que poderia ser uma edição de pessoa ainda não revisada). Publicar é visível ao público e indexável.

Onde é usadabotão publicar.
Efeitosmove o ponteiro da página; o visitante passa a ver esta versão.

Parâmetros

nomeemobrigatóriodescrição
idpath simid da página; o acesso é conferido contra a sessão (deny-default).

Respostas

códigosignificado
200publicada
401Sem sessão / sem tenant na sessão.
403A chave não tem o escopo exigido pela rota (ou a rota não aceita chave). Ex.: `{"erro":"a chave não tem o escopo forms:write"}`.
404Recurso inexistente ou fora do escopo da sessão (não revela existência em outra conta).
422essa versão não existe
429Escritas demais por esta chave (teto por minuto: 60 nas escritas de `pages:write`, 20 nas de `pages:publish`). Header `Retry-After: 60`.
503Não foi possível validar a chave agora (core fora do ar). Tente de novo.

Exemplo

curl -X POST 'https://api.pages.asender.net/v1/paginas/id_AQUI/publicar' \
  -H 'Authorization: Bearer SEU_TOKEN' \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: <uuid-v4>' \
  -d '{ }'
DELETE /v1/paginas/{id}/rascunho

Descartar o rascunho pendente

Idempotente; versões e a publicada não mudam. Página de outra conta responde 404.

Onde é usada"Descartar alterações" do editor-página.
Efeitoszera o rascunho; o formulário no ar não muda. Idempotente.

Parâmetros

nomeemobrigatóriodescrição
idpath simid da página; o acesso é conferido contra a sessão (deny-default).

Respostas

códigosignificado
204descartado
401Sem sessão / sem tenant na sessão.
403A chave não tem o escopo exigido pela rota (ou a rota não aceita chave). Ex.: `{"erro":"a chave não tem o escopo forms:write"}`.
404Recurso inexistente ou fora do escopo da sessão (não revela existência em outra conta).
429Escritas demais por esta chave (teto por minuto: 60 nas escritas de `pages:write`, 20 nas de `pages:publish`). Header `Retry-After: 60`.
503Não foi possível validar a chave agora (core fora do ar). Tente de novo.

Exemplo

curl -X DELETE 'https://api.pages.asender.net/v1/paginas/id_AQUI/rascunho' \
  -H 'Authorization: Bearer SEU_TOKEN'
GET /v1/paginas/{id}/rascunho

Ler o rascunho pendente da página

O rascunho é UM por página, guardado SEM criar versão e SEM publicar. rascunho_em é a etiqueta de concorrência otimista; rascunho_por aparece quando foi uma chave de API quem gravou (apikey:<key_id>). Sem rascunho: 204.

Onde é usadaabertura do builder (recuperação).
Efeitosnenhum.

Parâmetros

nomeemobrigatóriodescrição
idpath simid da página; o acesso é conferido contra a sessão (deny-default).

Respostas

códigosignificado
200ok
204sem rascunho pendente
401Sem sessão / sem tenant na sessão.
403A chave não tem o escopo exigido pela rota (ou a rota não aceita chave). Ex.: `{"erro":"a chave não tem o escopo forms:write"}`.
404Recurso inexistente ou fora do escopo da sessão (não revela existência em outra conta).
503Não foi possível validar a chave agora (core fora do ar). Tente de novo.

Exemplo

curl -X GET 'https://api.pages.asender.net/v1/paginas/id_AQUI/rascunho' \
  -H 'Authorization: Bearer SEU_TOKEN'
PUT /v1/paginas/{id}/rascunho

Gravar o rascunho (sem criar versão e sem publicar)

Guarda o template como ele está, SEM as regras de publicação (só a forma: objeto JSON dentro do teto de 512 KiB) — elas rodam ao salvar versão e ao publicar. ?base=<rascunho_em> é a concorrência otimista: se o rascunho do servidor mudou desde o que você leu, nada é gravado e a resposta é 409 (codigo: RascunhoConflito) trazendo o rascunho do servidor — os dois lados ficam preservados. ?base= vazio significa "esperava não haver rascunho". Na sessão ?base= é opcional (clientes antigos gravam sem condição); na API key é OBRIGATÓRIO (428 sem ele): o agente nunca pisa no trabalho de uma pessoa.

Onde é usadabotão "Salvar rascunho" do editor-página (e o salvamento automático).
Efeitosescreve rascunho; o formulário no ar NÃO muda. # Só a FORMA é conferida Rascunho existe para guardar trabalho pela metade — um campo sem rótulo, uma lista sem opção. Recusá-lo pelas regras de publicação faria a pessoa perder o que fez justamente quando ainda não terminou. A borda confere que o corpo é o do PUT (nenhum campo desconhecido, tipos certos, dentro do teto) e grava a forma DECODIFICADA re-serializada: chave estranha não chega ao banco. Toda a regra de domínio roda no PUBLICAR.

Parâmetros

nomeemobrigatóriodescrição
idpath simid da página; o acesso é conferido contra a sessão (deny-default).
basequery nãoo `rascunho_em` que você leu; vazio = esperava nenhum rascunho

Respostas

códigosignificado
200gravado
400corpo inválido ou `base` ilegível
401Sem sessão / sem tenant na sessão.
403A chave não tem o escopo exigido pela rota (ou a rota não aceita chave). Ex.: `{"erro":"a chave não tem o escopo forms:write"}`.
404Recurso inexistente ou fora do escopo da sessão (não revela existência em outra conta).
409o rascunho mudou desde a `base` (`codigo: RascunhoConflito`); o corpo traz `rascunho` do servidor
413rascunho acima do teto
428chave de API sem `?base=`
429Escritas demais por esta chave (teto por minuto: 60 nas escritas de `pages:write`, 20 nas de `pages:publish`). Header `Retry-After: 60`.
503Não foi possível validar a chave agora (core fora do ar). Tente de novo.

Exemplo

curl -X PUT 'https://api.pages.asender.net/v1/paginas/id_AQUI/rascunho' \
  -H 'Authorization: Bearer SEU_TOKEN' \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: <uuid-v4>' \
  -d '{ }'
POST /v1/paginas/{id}/versoes

Salvar uma nova versão (auto-save do editor)

Onde é usadabotão salvar do editor.
Efeitoscria uma versão.

Parâmetros

nomeemobrigatóriodescrição
idpath simid da página; o acesso é conferido contra a sessão (deny-default).

Respostas

códigosignificado
201salva
401Sem sessão / sem tenant na sessão.
403conteúdo privilegiado (html) sem nível
404Recurso inexistente ou fora do escopo da sessão (não revela existência em outra conta).
422template fora do contrato; v2 responde `codigo` + `caminho` do nó
429Escritas demais por esta chave (teto por minuto: 60 nas escritas de `pages:write`, 20 nas de `pages:publish`). Header `Retry-After: 60`.
503Não foi possível validar a chave agora (core fora do ar). Tente de novo.

Exemplo

curl -X POST 'https://api.pages.asender.net/v1/paginas/id_AQUI/versoes' \
  -H 'Authorization: Bearer SEU_TOKEN' \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: <uuid-v4>' \
  -d '{ }'
POST /v1/paginas/importar

Importar um arquivo exportado, copiando a mídia para a conta

Cria uma página (despublicada) a partir do template de um arquivo exportado (asender_pages_template: 2) e COPIA para a biblioteca da conta da sessão toda mídia referenciada (imagem de item, galeria, fundo de seção/coluna/página, ícone enviado, avatar/logo, imagem de compartilhamento): baixa de novo no servidor (mesmas guardas de SSRF da importação por URL, até 3 redirecionamentos, teto por arquivo e por importação, sniff de tipo, SVG sanitizado, antivírus) e troca cada URL pela nova. O que não puder ser copiado MANTÉM a URL original e aparece no relatório como mantido_original com o motivo. Referências presas à conta de origem (form_id que a conta não tem, *_rastreio, vínculos) são removidas, com aviso. A conta vem da sessão; o corpo não aceita tenant_id. Regras de POST /v1/paginas (domínio verificado, item privilegiado) valem aqui.

Onde é usada"Importar template" da galeria de novo-template; reutiliza o mesmo serviço que o CLI de importação.
Efeitosdownloads de terceiros (com SSRF), arquivos e linhas na galeria, uma página e uma versão. NUNCA publica. # Falha de mídia não falha a importação Um ativo que não copia mantém a URL original e aparece no relatório; o painel oferece "tentar copiar de novo" (RecopiarMidias).

Respostas

códigosignificado
201criada; o relatório diz o que foi copiado
400corpo inválido (campo desconhecido
401Sem sessão / sem tenant na sessão.
403conteúdo privilegiado (html) sem nível
409já existe página nesse endereço (checado antes de copiar)
422título/template ausente, endereço inválido, SEO inválido ou template fora do contrato
429Escritas demais por esta chave (teto por minuto: 60 nas escritas de `pages:write`, 20 nas de `pages:publish`). Header `Retry-After: 60`.
500não consegui preparar a importação (por exemplo, conferir os formulários)
503Não foi possível validar a chave agora (core fora do ar). Tente de novo.

Exemplo

curl -X POST 'https://api.pages.asender.net/v1/paginas/importar' \
  -H 'Authorization: Bearer SEU_TOKEN' \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: <uuid-v4>' \
  -d '{ }'
POST /v1/paginas/lote

Ação em massa sobre páginas (publicar, despublicar, duplicar, mudar domínio, arquivar)

Executa UMA ação sobre até 100 páginas, cada uma de forma independente (sem tudo-ou-nada): o corpo da resposta traz o desfecho POR PÁGINA (ok, pulada ou erro, com motivo). A conta vem da sessão/chave; cada id é resolvido dentro dela e um id de outra conta é reportado como nao_encontrada, indistinguível de um id inexistente. simular: true devolve o que aconteceria, sem escrever nada. publicar publica a última versão SALVA e é idempotente (já no ar = ok com sem_mudanca). Conteúdo recusado pelo contrato de domínio é pulada/conteudo_invalido. Se houver rascunho de servidor mais novo que a última versão, a política rascunho decide: pular (padrão — pulada/rascunho_mais_novo) ou publicar_salva (publica a última versão salva; o rascunho NUNCA vira versão nem é descartado). despublicar tira do ar sem tocar em versões nem rascunho (idempotente). duplicar cria uma cópia despublicada da última versão salva (-copia, -copia-2…), com a mesma guarda de bloco privilegiado da criação. mudar_dominio delega ao serviço de mudança de domínio (mesmo contrato de PUT /v1/paginas/{id}/dominio, incluindo o 301 do endereço antigo); dominio_id é obrigatório ("" = host da plataforma) e caminho_por_pagina opcional (ausente = mantém o caminho atual). Cada página processada grava um log de auditoria (lote_id, conta, usuário, ação, desfecho). Limite de ritmo: 12 chamadas por minuto por conta (429). arquivar é o soft delete da página (despublica e esconde; versões e rascunho ficam; o master restaura — ADR-0112). SÓ sessão: chave de API é recusada (403). API key: publicar e despublicar exigem pages:publish; duplicar e mudar_dominio exigem pages:write (sem o escopo da ação, 403).

Onde é usadabarra de ações em massa da lista de páginas.
Entradascorpo JSON (campos desconhecidos recusados). O tenant vem só do contexto autenticado; cada id é resolvido DENTRO do tenant (RLS + filtro), e id de outra conta é reportado como nao_encontrada — nunca confirma existência.
Saídas200 com {lote_id, acao, simulado, total, ok, puladas, erros, resultados[]}; 400/422 no pedido malformado; 429 acima do ritmo.
Efeitosescreve conforme a ação; um log auditoria por página com lote_id/trace_id, conta e usuário. Idempotente: publicar o que já está no ar e despublicar o que já está fora são ok com sem_mudanca. Sem tudo-ou-nada.

Respostas

códigosignificado
200processado; veja `resultados` página a página
400corpo inválido (campo desconhecido
401Sem sessão / sem tenant na sessão.
403A chave não tem o escopo exigido pela rota (ou a rota não aceita chave). Ex.: `{"erro":"a chave não tem o escopo forms:write"}`.
422ação inválida, 0 ou mais de 100 ids, política de rascunho inválida ou `dominio_id` ausente em mudar_dominio
429acima de 12 chamadas por minuto na conta
503ações em massa indisponíveis (sem banco)

Exemplo

curl -X POST 'https://api.pages.asender.net/v1/paginas/lote' \
  -H 'Authorization: Bearer SEU_TOKEN' \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: <uuid-v4>' \
  -d '{ }'

pixel.{versao:[0-9a-f]{16}}.js

GET /pixel.{versao:[0-9a-f]{16}}.js sem schema na spec

GET /pixel.{versao}.js — o mesmo script de `Script`, com `CacheScriptVersionado` quando a versão pedida é a servida e `CacheScriptVersaoAntiga` quando não é.

Onde é usadao <script src> de toda página publicada (renderizador, PixelSrc).
Efeitosescreve o script. Versão antiga NÃO vira 404 nem redirect: um 404 desligaria a analytics da página em cache até ela expirar, e um redirect custaria uma ida à rede a mais no caminho de quem só queria o script.

Exemplo

curl -X GET 'https://api.pages.asender.net/pixel.versao:[0-9a-f]{16_AQUI}.js' \
  -H 'Authorization: Bearer SEU_TOKEN'

Pixels

GET /v1/pixels

Listar pixels

Onde é usadaa tela de leads da ferramenta de Páginas/Formulários.
Efeitosuma consulta; uma abertura de AES-GCM por linha; um registro de acesso a PII no log. Devolve {"envios": [...], "proximo_cursor": ""} — cursor vazio significa que acabou. A alternativa (omitir o campo no fim) obrigaria o cliente a distinguir ausente de vazio, e é o tipo de sutileza que vira laço infinito na tela.

Respostas

códigosignificado
200ok
401Sem sessão / credencial ausente ou inválida.

Exemplo

curl -X GET 'https://api.pages.asender.net/v1/pixels' \
  -H 'Authorization: Bearer SEU_TOKEN'
POST /v1/pixels

Criar pixel (devolve o snippet com a credencial)

Onde é usadatela de pixels.
Efeitosuma escrita.

Respostas

códigosignificado
201criado
401Sem sessão / credencial ausente ou inválida.
422nome inválido

Exemplo

curl -X POST 'https://api.pages.asender.net/v1/pixels' \
  -H 'Authorization: Bearer SEU_TOKEN' \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: <uuid-v4>' \
  -d '{ }'
DELETE /v1/pixels/{id}

Apagar um pixel

Onde é usadatela de detalhe.
Efeitosuma escrita; a coleta daquele pixel para.

Parâmetros

nomeemobrigatóriodescrição
idpath sim

Respostas

códigosignificado
204apagado
401Sem sessão / credencial ausente ou inválida.
404não encontrado

Exemplo

curl -X DELETE 'https://api.pages.asender.net/v1/pixels/id_AQUI' \
  -H 'Authorization: Bearer SEU_TOKEN'
GET /v1/pixels/{id}

Obter um pixel

Onde é usadatela de detalhe do pixel.
Efeitosuma leitura.

Parâmetros

nomeemobrigatóriodescrição
idpath sim

Respostas

códigosignificado
200ok
401Sem sessão / credencial ausente ou inválida.
404não encontrado

Exemplo

curl -X GET 'https://api.pages.asender.net/v1/pixels/id_AQUI' \
  -H 'Authorization: Bearer SEU_TOKEN'
PATCH /v1/pixels/{id}

Editar um pixel

Onde é usadatela de detalhe.
Efeitosuma escrita. PATCH parcial de verdade: campo ausente não é tocado. Sem isso, editar só o nome DESATIVARIA o pixel — e a coleta pararia sem ninguém ter pedido.

Parâmetros

nomeemobrigatóriodescrição
idpath sim

Respostas

códigosignificado
200ok
401Sem sessão / credencial ausente ou inválida.
404não encontrado
422informe `nome` e/ou `ativo`

Exemplo

curl -X PATCH 'https://api.pages.asender.net/v1/pixels/id_AQUI' \
  -H 'Authorization: Bearer SEU_TOKEN' \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: <uuid-v4>' \
  -d '{ }'
GET /v1/pixels/{id}/conversions

Listar conversões do pixel

Onde é usadatela de detalhe.
Efeitosuma leitura.

Parâmetros

nomeemobrigatóriodescrição
idpath sim

Respostas

códigosignificado
200ok
401Sem sessão / credencial ausente ou inválida.

Exemplo

curl -X GET 'https://api.pages.asender.net/v1/pixels/id_AQUI/conversions' \
  -H 'Authorization: Bearer SEU_TOKEN'
POST /v1/pixels/{id}/conversions

Criar conversão

Onde é usadatela de detalhe.
Efeitosuma escrita.

Parâmetros

nomeemobrigatóriodescrição
idpath sim

Respostas

códigosignificado
201criada
401Sem sessão / credencial ausente ou inválida.
409nome já em uso
422nome inválido

Exemplo

curl -X POST 'https://api.pages.asender.net/v1/pixels/id_AQUI/conversions' \
  -H 'Authorization: Bearer SEU_TOKEN' \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: <uuid-v4>' \
  -d '{ }'
DELETE /v1/pixels/{id}/conversions/{cid}

Apagar conversão

Onde é usadatela de detalhe.
Efeitosuma escrita.

Parâmetros

nomeemobrigatóriodescrição
idpath sim
cidpath sim

Respostas

códigosignificado
204apagada
401Sem sessão / credencial ausente ou inválida.
404não encontrada

Exemplo

curl -X DELETE 'https://api.pages.asender.net/v1/pixels/id_AQUI/conversions/cid_AQUI' \
  -H 'Authorization: Bearer SEU_TOKEN'
PATCH /v1/pixels/{id}/conversions/{cid}

Editar conversão

Onde é usadatela de detalhe.
Efeitosuma escrita.

Parâmetros

nomeemobrigatóriodescrição
idpath sim
cidpath sim

Respostas

códigosignificado
200ok
401Sem sessão / credencial ausente ou inválida.
404não encontrada
422informe nome, tipo e/ou seletor

Exemplo

curl -X PATCH 'https://api.pages.asender.net/v1/pixels/id_AQUI/conversions/cid_AQUI' \
  -H 'Authorization: Bearer SEU_TOKEN' \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: <uuid-v4>' \
  -d '{ }'
GET /v1/pixels/{id}/domains

Listar as origens autorizadas do pixel

Onde é usadatela de detalhe.
Efeitosuma leitura.

Parâmetros

nomeemobrigatóriodescrição
idpath sim

Respostas

códigosignificado
200ok
401Sem sessão / credencial ausente ou inválida.

Exemplo

curl -X GET 'https://api.pages.asender.net/v1/pixels/id_AQUI/domains' \
  -H 'Authorization: Bearer SEU_TOKEN'
POST /v1/pixels/{id}/domains

Autorizar uma origem para o pixel

Onde é usadatela de detalhe.
Efeitosuma escrita — e, a partir dela, o /collect ecoa CORS para aquele host.

Parâmetros

nomeemobrigatóriodescrição
idpath sim

Respostas

códigosignificado
201adicionado
401Sem sessão / credencial ausente ou inválida.
409host já na lista

Exemplo

curl -X POST 'https://api.pages.asender.net/v1/pixels/id_AQUI/domains' \
  -H 'Authorization: Bearer SEU_TOKEN' \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: <uuid-v4>' \
  -d '{ }'
DELETE /v1/pixels/{id}/domains/{domainId}

Remover uma origem autorizada

Onde é usadatela de detalhe.
Efeitosuma escrita — a origem para de coletar na hora.

Parâmetros

nomeemobrigatóriodescrição
idpath sim
domainIdpath sim

Respostas

códigosignificado
204removido
401Sem sessão / credencial ausente ou inválida.
404não encontrado

Exemplo

curl -X DELETE 'https://api.pages.asender.net/v1/pixels/id_AQUI/domains/domainId_AQUI' \
  -H 'Authorization: Bearer SEU_TOKEN'
PATCH /v1/pixels/{id}/domains/{domainId}

Trocar uma origem autorizada

Onde é usadatela de detalhe.
Efeitosuma escrita.

Parâmetros

nomeemobrigatóriodescrição
idpath sim
domainIdpath sim

Respostas

códigosignificado
200ok
401Sem sessão / credencial ausente ou inválida.
404não encontrado

Exemplo

curl -X PATCH 'https://api.pages.asender.net/v1/pixels/id_AQUI/domains/domainId_AQUI' \
  -H 'Authorization: Bearer SEU_TOKEN' \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: <uuid-v4>' \
  -d '{ }'

raiz

GET / sem schema na spec

304 quando o `If-None-Match` bate; senão o bundle, com cache de uma hora e `X-Content-Type-Options: nosniff`.

Onde é usadarota /chat.js do runtime.
Entradasw; r.
Efeitosnenhum — é leitura de bytes já em memória. Não há CORS aqui, e é de propósito. Um <script src> não precisa de CORS para executar, e liberar Access-Control-Allow-Origin: * neste arquivo só abriria a leitura do conteúdo por fetch de qualquer origem sem ganho nenhum. Quem precisa de CORS é a API do chat, e lá a origem é conferida contra a chave de site — não liberada em *. ifNoneMatchCasa (comparação FRACA, RFC 9110 §13.1.2), não ==: a borda comprime este script on-the-fly e rebaixa o ETag forte para W/"…" na resposta — o navegador devolve esse W/ na revisita, e == contra o forte nunca batia (achado em embed.js, forms-rapidos 2026-10-01; mesma causa aqui, mesmo remédio — ver etag_match.go).

Exemplo

curl -X GET 'https://api.pages.asender.net/' \
  -H 'Authorization: Bearer SEU_TOKEN'
POST / sem schema na spec

resolve a página pela mesma rota que a serviu, monta a submissão com `Modo = pagina` e chama o MESMO pipeline dos outros dois modos.

Onde é usadaPOST no caminho da própria página.
Efeitosos do pipeline (banco, outbox). O tenant e o form vêm da PÁGINA resolvida no servidor, nunca do corpo: o form da página é HTML público, e qualquer um pode reenviá-lo com outro form_id. Resolver pelo host+caminho é o que impede postar no formulário de outro tenant.

Exemplo

curl -X POST 'https://api.pages.asender.net/' \
  -H 'Authorization: Bearer SEU_TOKEN' \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: <uuid-v4>' \
  -d '{ }'

Resumo

GET /v1/resumo

Resumo da ferramenta para o hub e a home (contrato comum de resumo).

Visitas (sessões), visualizações, conversões de formulário e taxa de conversão — com a janela anterior —, páginas publicadas (no ar agora) e a série de visitas por dia. Visitas e conversões vêm do runtime; com ele fora do ar, esses indicadores e a série SOMEM (nunca viram zero) e o cartão segue com as páginas publicadas. Tenant e usuário vêm da credencial verificada, nunca da query. Conta alheia → 404; membro sem acesso ao produto → 403.

Onde é usadatela de A/B do painel.
Efeitosuma leitura. Página SEM experimento responde 200 com o corpo vazio de variantes, e não 404: "esta página não tem teste" é uma resposta sobre o mundo, e a tela precisa dela para mostrar o botão de criar. 404 mandaria a tela tratar o caso normal como erro.

Parâmetros

nomeemobrigatóriodescrição
periodoquery nãojanela dos indicadores — os N últimos dias fechados em hoje (UTC). Padrão 30d.

Respostas

códigosignificado
200o resumo da ferramenta `pages`.
400Parâmetro de query fora do conjunto aceito.
401Sem sessão / sem tenant na sessão.
403membro da conta sem acesso a esta ferramenta.
404Recurso inexistente ou fora do escopo da sessão (não revela existência em outra conta).

Exemplo

curl -X GET 'https://api.pages.asender.net/v1/resumo' \
  -H 'Authorization: Bearer SEU_TOKEN'

robots.txt

GET /robots.txt sem schema na spec

libera a indexação e aponta o sitemap do MESMO host — ou fecha o host inteiro (`Disallow: /`) quando ele não deve ir à busca (ver `fechado`).

Onde é usadarota pública.
Efeitosuma leitura no asender_pages_api (quando há fonte). # Por que liberar é o padrão A página existe para ser achada: é landing page de campanha, e quem a publica quer tráfego. Fechar o host é DECISÃO — do cliente (noindex do site), do estado (nada publicado e indexável) ou da política (PAGES_INDEXACAO) —, nunca o default da plataforma. Falha ao consultar o asender_pages_api LIBERA: um blip não pode tirar o site de um cliente da busca (o cache curto de 5 min do documento fechado vale pelo mesmo motivo). O noindex de UMA página não vira Disallow: o buscador precisa conseguir ler a página para ver o noindex dela — bloqueada no robots, ela pode continuar no índice pelos links que apontam para ela. O sitemap é apontado com o HOST da requisição, e não com um domínio configurado: cada cliente tem o dele, e um endereço fixo aqui mandaria o buscador de todo mundo para o sitemap de um só.

Exemplo

curl -X GET 'https://api.pages.asender.net/robots.txt' \
  -H 'Authorization: Bearer SEU_TOKEN'

Roteamento

GET /v1/paginas/{id}/cliques

Cliques por destino de roteamento

Onde é usadarelatório da tela de links curtos.
Efeitosuma leitura. Devolve o CONTADOR, e não uma lista de eventos: a pergunta é sempre "quantos", nunca "quais". Uma linha por clique transformaria o caminho mais quente do redirect num INSERT por requisição.

Parâmetros

nomeemobrigatóriodescrição
idpath simid da página; o acesso é conferido contra a sessão (deny-default).
diasquery nãojanela em dias

Respostas

códigosignificado
200ok
401Sem sessão / sem tenant na sessão.

Exemplo

curl -X GET 'https://api.pages.asender.net/v1/paginas/id_AQUI/cliques' \
  -H 'Authorization: Bearer SEU_TOKEN'
DELETE /v1/paginas/{id}/roteamento

Apagar a regra de roteamento

Onde é usadatela de domínios.
Efeitosuma escrita. Apagar TIRA a autorização de TLS, e é o único caminho para isso — de propósito: um domínio verificado que pudesse ser "desverificado" por outro caminho sairia do ar sem ninguém entender por quê.

Parâmetros

nomeemobrigatóriodescrição
idpath simid da página; o acesso é conferido contra a sessão (deny-default).

Respostas

códigosignificado
204apagada
401Sem sessão / sem tenant na sessão.
404Recurso inexistente ou fora do escopo da sessão (não revela existência em outra conta).

Exemplo

curl -X DELETE 'https://api.pages.asender.net/v1/paginas/id_AQUI/roteamento' \
  -H 'Authorization: Bearer SEU_TOKEN'
GET /v1/paginas/{id}/roteamento

Obter a regra de roteamento da página

Onde é usadatela de A/B do painel.
Efeitosuma leitura. Página SEM experimento responde 200 com o corpo vazio de variantes, e não 404: "esta página não tem teste" é uma resposta sobre o mundo, e a tela precisa dela para mostrar o botão de criar. 404 mandaria a tela tratar o caso normal como erro.

Parâmetros

nomeemobrigatóriodescrição
idpath simid da página; o acesso é conferido contra a sessão (deny-default).

Respostas

códigosignificado
200ok
401Sem sessão / sem tenant na sessão.

Exemplo

curl -X GET 'https://api.pages.asender.net/v1/paginas/id_AQUI/roteamento' \
  -H 'Authorization: Bearer SEU_TOKEN'
PUT /v1/paginas/{id}/roteamento

Salvar a regra de roteamento

Exclusivo com o experimento A/B (ADR-0014). Máximo de 20 destinos.

Onde é usadabotão salvar do editor.
Efeitoscria uma versão.

Parâmetros

nomeemobrigatóriodescrição
idpath simid da página; o acesso é conferido contra a sessão (deny-default).

Respostas

códigosignificado
200salva
401Sem sessão / sem tenant na sessão.
404Recurso inexistente ou fora do escopo da sessão (não revela existência em outra conta).
409conflita com um experimento ativo
422estratégia/destinos inválidos

Exemplo

curl -X PUT 'https://api.pages.asender.net/v1/paginas/id_AQUI/roteamento' \
  -H 'Authorization: Bearer SEU_TOKEN' \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: <uuid-v4>' \
  -d '{ }'

SEO

GET /v1/configuracoes/seo

Obter o SEO do site (um host da conta)

Desde a 041 só noindex é do site. Os demais campos de SEOSite são o ESPELHO, só leitura, do kit de marca efetivo do host, e seo.kit identifica o kit ({id, nome, origem}).

Onde é usadatela "SEO do site".
Efeitosuma leitura. Host de outra conta (ou não verificado) → 404.

Parâmetros

nomeemobrigatóriodescrição
dominioquery nãohost do domínio verificado da conta; ausente/vazio = o host da plataforma

Respostas

códigosignificado
200ok
401Sem sessão / sem tenant na sessão.
404host não é um domínio verificado desta conta

Exemplo

curl -X GET 'https://api.pages.asender.net/v1/configuracoes/seo' \
  -H 'Authorization: Bearer SEU_TOKEN'
PUT /v1/configuracoes/seo

Definir a indexação do site (um host da conta)

Grava só noindex, que muda robots.txt, sitemap.xml e o robots do <head> de todas as páginas do host. Nome do site, imagem, favicon e organização moram no kit de marca (041): mandá-los preenchidos aqui dá 422.

Onde é usadatela "SEO do site".
Efeitosuma escrita; muda robots, sitemap e <head> de todo o host.

Parâmetros

nomeemobrigatóriodescrição
dominioquery não

Respostas

códigosignificado
200gravado
400corpo inválido (inclui campo desconhecido, ex. tenant_id)
401Sem sessão / sem tenant na sessão.
404host não é um domínio verificado desta conta
422campo que mora no kit de marca veio preenchido — a mensagem diz onde editá-lo

Exemplo

curl -X PUT 'https://api.pages.asender.net/v1/configuracoes/seo' \
  -H 'Authorization: Bearer SEU_TOKEN' \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: <uuid-v4>' \
  -d '{ }'
GET /v1/paginas/{id}/seo

Obter o SEO e o compartilhamento de uma página

Página nunca configurada responde 200 com os campos vazios — o runtime usa os padrões (título ← nome da página, descrição ← primeiro texto, og:image ← primeira imagem ou a do site).

Onde é usadatela "SEO e compartilhamento" do painel.
Efeitosuma leitura. Página nunca configurada responde 200 com os padrões.

Parâmetros

nomeemobrigatóriodescrição
idpath sim

Respostas

códigosignificado
200ok
401Sem sessão / sem tenant na sessão.
403A chave não tem o escopo exigido pela rota (ou a rota não aceita chave). Ex.: `{"erro":"a chave não tem o escopo forms:write"}`.
404não existe — ou é de outra conta (mesma resposta)
503Não foi possível validar a chave agora (core fora do ar). Tente de novo.

Exemplo

curl -X GET 'https://api.pages.asender.net/v1/paginas/id_AQUI/seo' \
  -H 'Authorization: Bearer SEU_TOKEN'
PUT /v1/paginas/{id}/seo

Definir o SEO e o compartilhamento de uma página

Todos os campos de uma vez; vazio volta ao padrão. Vale na próxima resolução do runtime, sem publicar versão.

Onde é usadatela "SEO e compartilhamento".
Efeitosuma escrita; vale na próxima resolução do runtime (sem publicar).

Parâmetros

nomeemobrigatóriodescrição
idpath sim

Respostas

códigosignificado
200gravado
400corpo inválido (inclui campo desconhecido, ex. tenant_id)
401Sem sessão / sem tenant na sessão.
403A chave não tem o escopo exigido pela rota (ou a rota não aceita chave). Ex.: `{"erro":"a chave não tem o escopo forms:write"}`.
404não existe — ou é de outra conta (mesma resposta)
422campo inválido — a mensagem nomeia o campo (tamanho, URL fora de http/https, imagem sem https, dimensões)
429Escritas demais por esta chave (teto por minuto: 60 nas escritas de `pages:write`, 20 nas de `pages:publish`). Header `Retry-After: 60`.
503Não foi possível validar a chave agora (core fora do ar). Tente de novo.

Exemplo

curl -X PUT 'https://api.pages.asender.net/v1/paginas/id_AQUI/seo' \
  -H 'Authorization: Bearer SEU_TOKEN' \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: <uuid-v4>' \
  -d '{ }'

Session replay

POST /replay

Gravar um lote de jornada (anônimo, credencial de pixel)

Chamado pelo pixel no navegador do visitante. A credencial (token assinado do pixel) vai no corpo; o relógio é o do SERVIDOR. Responde 204 quase sempre; 403 para credencial/origem inválida, 429 para excesso. Servido no runtime.

Onde é usadarota pública, chamada pelo pixel a cada poucos segundos de uma sessão gravada.
Efeitospublica no broker; NUNCA escreve no banco. Responde 204 quase sempre, como o /collect: credencial inválida é 403 (o HTML está velho), e o resto é aproveitado. O pixel roda no browser de terceiros, e transformar um lote parcialmente ruim em erro visível não conserta nada.

Respostas

códigosignificado
204aceito
400corpo inválido
403credencial ou origem inválida
429excesso (rate-limit por credencial × IP)
503gravação indisponível agora

Exemplo

curl -X POST 'https://api.pages.asender.net/replay' \
  -H 'Authorization: Bearer SEU_TOKEN' \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: <uuid-v4>' \
  -d '{ }'
GET /v1/replay/sessions

Listar as sessões gravadas

Onde é usadatela de replay.
Efeitosuma leitura.

Parâmetros

nomeemobrigatóriodescrição
limitequery não

Respostas

códigosignificado
200ok
400limite inválido
401Sem sessão / credencial ausente ou inválida.

Exemplo

curl -X GET 'https://api.pages.asender.net/v1/replay/sessions' \
  -H 'Authorization: Bearer SEU_TOKEN'
GET /v1/replay/sessions/{id}/events

Eventos de uma jornada gravada

Onde é usadaplayer da tela de replay.
Efeitosuma leitura. Sessão de outra conta responde lista VAZIA, e não 404: o id é opaco e gerado no browser, e distinguir "não existe" de "não é sua" transformaria a rota num oráculo de existência de sessão alheia.

Parâmetros

nomeemobrigatóriodescrição
idpath sim

Respostas

códigosignificado
200ok
400sessão inválida
401Sem sessão / credencial ausente ou inválida.

Exemplo

curl -X GET 'https://api.pages.asender.net/v1/replay/sessions/id_AQUI/events' \
  -H 'Authorization: Bearer SEU_TOKEN'

sitemap.xml

GET /sitemap.xml sem schema na spec

lista as páginas PUBLICADAS e INDEXÁVEIS daquele host (sem noindex, sem canônica externa; a home do domínio pela raiz). Host fechado pela política ou site com noindex global → documento vazio.

Onde é usadarota pública.
Efeitosuma leitura no asender_pages_api. # Host sem página responde XML VAZIO, e não 404 Um 404 no sitemap faz o buscador registrar erro e tentar de novo; um documento vazio diz "não há nada para indexar aqui", que é a verdade. E o host que ainda não tem página publicada é o caso mais comum logo depois de alguém cadastrar um domínio. # Rascunho não entra A consulta do asender_pages_api filtra por versão publicada. Listar rascunho entregaria ao buscador o endereço de uma página que o cliente ainda não quis mostrar — e o buscador não esquece.

Exemplo

curl -X GET 'https://api.pages.asender.net/sitemap.xml' \
  -H 'Authorization: Bearer SEU_TOKEN'

Testes A/B

POST /v1/paginas/{id}/ab/desvincular

Desvincular a PRÓPRIA página do teste em que ela está (lista de páginas)

Onde é usadaação "Desvincular do teste" da lista de páginas.

Parâmetros

nomeemobrigatóriodescrição
idpath simid da página; o acesso é conferido contra a sessão (deny-default).

Respostas

códigosignificado
200desvinculada
401Sem sessão / sem tenant na sessão.
404Recurso inexistente ou fora do escopo da sessão (não revela existência em outra conta).

Exemplo

curl -X POST 'https://api.pages.asender.net/v1/paginas/id_AQUI/ab/desvincular' \
  -H 'Authorization: Bearer SEU_TOKEN' \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: <uuid-v4>' \
  -d '{ }'
POST /v1/paginas/{id}/ab/pausar

Pausar o teste (rodando → pausado; todo visitante volta à página publicada)

Onde é usadabotão "Pausar" da tela de A/B.
Efeitosuma escrita; todo visitante volta a ver a página publicada. A amostra fica congelada (o runtime só conta quem viu uma variante) e retomar continua a mesma medição: iniciado_em não muda.

Parâmetros

nomeemobrigatóriodescrição
idpath simid da página; o acesso é conferido contra a sessão (deny-default).

Respostas

códigosignificado
200pausado
401Sem sessão / sem tenant na sessão.
404Recurso inexistente ou fora do escopo da sessão (não revela existência em outra conta).
409o teste não está rodando

Exemplo

curl -X POST 'https://api.pages.asender.net/v1/paginas/id_AQUI/ab/pausar' \
  -H 'Authorization: Bearer SEU_TOKEN' \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: <uuid-v4>' \
  -d '{ }'
POST /v1/paginas/{id}/ab/promover

Promover uma variante (e opcionalmente iniciar o teste)

Com iniciar: true, inicia o teste (exige pesos válidos). Sem ele, ENCERRA com a vencedora e, se ela é uma página-variante, ela ASSUME A URL do teste (a principal antiga segue como página normal), na mesma transação (ADR-0095).

Onde é usadabotão "promover vencedora" e botão "iniciar teste" da tela de A/B.
Efeitosuma escrita; promover também põe a página vencedora na URL. # Duas operações na mesma rota, e por quê O lynz expõe POST /v1/pages/{id}/ab/promote, sem rota de iniciar — o teste começa ao ser promovido a "rodando". Manter uma rota só preserva a superfície do lynz, e o corpo diz qual das duas transições é pedida. # Iniciar EXIGE o experimento válido É o único ponto onde o rascunho inconsistente é barrado, e é o ponto certo: pesos que não somam 100 deixariam uma faixa do tráfego sem dono, e Escolher devolveria a última variante para essa fatia — um viés silencioso que o número final não denuncia.

Parâmetros

nomeemobrigatóriodescrição
idpath simid da página; o acesso é conferido contra a sessão (deny-default).

Respostas

códigosignificado
200promovida
401Sem sessão / sem tenant na sessão.
409já há experimento rodando
422variante inválida

Exemplo

curl -X POST 'https://api.pages.asender.net/v1/paginas/id_AQUI/ab/promover' \
  -H 'Authorization: Bearer SEU_TOKEN' \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: <uuid-v4>' \
  -d '{ }'
POST /v1/paginas/{id}/ab/retomar

Retomar um teste pausado (mesma medição; o início não muda)

Onde é usadabotão "Retomar" da tela de A/B.
Efeitosuma escrita; a distribuição volta para os visitantes novos, e quem já tinha braço fixado continua nele. Valida antes, como ao iniciar: com o teste pausado os braços puderam mudar.

Parâmetros

nomeemobrigatóriodescrição
idpath simid da página; o acesso é conferido contra a sessão (deny-default).

Respostas

códigosignificado
200rodando
401Sem sessão / sem tenant na sessão.
404Recurso inexistente ou fora do escopo da sessão (não revela existência em outra conta).
409o teste não está pausado, ou conflito (outro rodando / roteamento)
422variantes inválidas

Exemplo

curl -X POST 'https://api.pages.asender.net/v1/paginas/id_AQUI/ab/retomar' \
  -H 'Authorization: Bearer SEU_TOKEN' \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: <uuid-v4>' \
  -d '{ }'
GET /v1/paginas/{id}/experimento

Obter o experimento da página

Onde é usadatela de A/B do painel.
Efeitosuma leitura. Página SEM experimento responde 200 com o corpo vazio de variantes, e não 404: "esta página não tem teste" é uma resposta sobre o mundo, e a tela precisa dela para mostrar o botão de criar. 404 mandaria a tela tratar o caso normal como erro.

Parâmetros

nomeemobrigatóriodescrição
idpath simid da página; o acesso é conferido contra a sessão (deny-default).

Respostas

códigosignificado
200ok
401Sem sessão / sem tenant na sessão.
404Recurso inexistente ou fora do escopo da sessão (não revela existência em outra conta).

Exemplo

curl -X GET 'https://api.pages.asender.net/v1/paginas/id_AQUI/experimento' \
  -H 'Authorization: Bearer SEU_TOKEN'
PUT /v1/paginas/{id}/experimento/auto-stop

Definir o critério de decisão e o autobalanceamento (ADR-0093)

Campos ausentes mantêm o valor gravado. Aceito com o teste rodando — nada aqui reatribui visitante já contado. Campo desconhecido (inclusive o antigo margem_para_encerrar, removido em 2026-10-07) é recusado com 400.

Onde é usadapainel "Critério e autobalanceamento" da tela de A/B.
Efeitosuma escrita. Aceito com o teste RODANDO, ao contrário das variantes: nenhum destes campos reatribui visitante já contado (a atribuição é fixada por visitante) — mudam o rótulo da tela e a fatia do visitante NOVO.

Parâmetros

nomeemobrigatóriodescrição
idpath simid da página; o acesso é conferido contra a sessão (deny-default).

Respostas

códigosignificado
200definido
401Sem sessão / sem tenant na sessão.
404Recurso inexistente ou fora do escopo da sessão (não revela existência em outra conta).
422valores fora de faixa

Exemplo

curl -X PUT 'https://api.pages.asender.net/v1/paginas/id_AQUI/experimento/auto-stop' \
  -H 'Authorization: Bearer SEU_TOKEN' \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: <uuid-v4>' \
  -d '{ }'
PUT /v1/paginas/{id}/experimento/pesos

Definir a divisão de tráfego inteira, atomicamente (ADR-0093)

Um peso por variante existente, inteiros 0–100 somando exatamente 100 e ao menos duas com peso ≥ 1. Aceito com o teste rodando ou pausado (a atribuição é fixada por visitante: o peso novo vale para visitantes novos); 409 encerrado.

Onde é usadaa barra de divisão da tela de A/B ("Salvar divisão").
Efeitosuma escrita. # Atômico, e por quê O defeito que o dono encontrou ("Soma dos pesos: 150%") nasceu de editar peso por peso: entre duas edições a soma passa por valores inválidos, e a tela não tinha como chegar a 100 sem ida e volta por variante. Aqui o conjunto chega inteiro e o domínio valida o resultado inteiro: soma exatamente 100, cada peso em 0–100 e ao menos dois braços com peso ≥ 1 (ADR-0093). # Aceito com o teste RODANDO ou PAUSADO A atribuição é fixada por visitante no runtime (cookie por experimento): o peso novo só decide quem chega DEPOIS. Encerrado recusa (409) — o tráfego já é todo da vencedora.

Parâmetros

nomeemobrigatóriodescrição
idpath simid da página; o acesso é conferido contra a sessão (deny-default).

Respostas

códigosignificado
200divisão gravada
401Sem sessão / sem tenant na sessão.
404Recurso inexistente ou fora do escopo da sessão (não revela existência em outra conta).
409experimento encerrado
422soma ≠ 100, variante faltando/a mais, peso fora de 0–100 ou menos de duas ativas

Exemplo

curl -X PUT 'https://api.pages.asender.net/v1/paginas/id_AQUI/experimento/pesos' \
  -H 'Authorization: Bearer SEU_TOKEN' \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: <uuid-v4>' \
  -d '{ }'
GET /v1/paginas/{id}/variantes

Listar as variantes

Onde é usadatela de A/B.
Efeitosuma leitura.

Parâmetros

nomeemobrigatóriodescrição
idpath simid da página; o acesso é conferido contra a sessão (deny-default).

Respostas

códigosignificado
200ok
401Sem sessão / sem tenant na sessão.

Exemplo

curl -X GET 'https://api.pages.asender.net/v1/paginas/id_AQUI/variantes' \
  -H 'Authorization: Bearer SEU_TOKEN'
POST /v1/paginas/{id}/variantes

Acrescentar uma PÁGINA ao teste — duplicar ou vincular (ADR-0095)

{id} é a página da URL do teste; sem teste aberto, ele nasce aqui com {id} como principal. "duplicar" cria uma página normal nova (cópia da última versão de pagina_id, ou da principal), publicada, com slug <url>-ab-<braço> e sem rota própria. "vincular" liga uma página existente do MESMO domínio que não esteja em outro teste aberto. A divisão volta a ser igual. 409 com o teste rodando (pause antes).

Onde é usadabotões "Criar variante" e "Vincular página existente" da tela de A/B.
Efeitosescritas atômicas; a divisão é refeita em partes iguais. O braço é uma PÁGINA normal (aparece na lista, abre no editor, tem SEO e versões próprios); o teste só guarda a relação e o peso.

Parâmetros

nomeemobrigatóriodescrição
idpath simid da página; o acesso é conferido contra a sessão (deny-default).

Respostas

códigosignificado
201o experimento atualizado
401Sem sessão / sem tenant na sessão.
404Recurso inexistente ou fora do escopo da sessão (não revela existência em outra conta).
409teste rodando / id de braço já usado / página já em outro teste
422modo inválido / página de outro domínio / id fora do formato

Exemplo

curl -X POST 'https://api.pages.asender.net/v1/paginas/id_AQUI/variantes' \
  -H 'Authorization: Bearer SEU_TOKEN' \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: <uuid-v4>' \
  -d '{ }'
DELETE /v1/paginas/{id}/variantes/{varianteID}

Desvincular a página do braço (ela continua existindo)

Os pesos que ficam renormalizam; a amostra do braço fica no histórico. Saindo a principal, o braço de maior peso assume a URL; sobrando uma página, o teste encerra e ela fica na URL. Aceito com o teste rodando.

Onde é usadatela de A/B.
Efeitosescritas atômicas (ver repo.Desvincular): os pesos que ficam renormalizam; sobrando uma página, o teste encerra e ela fica na URL. Aceito com o teste rodando.

Parâmetros

nomeemobrigatóriodescrição
idpath simid da página; o acesso é conferido contra a sessão (deny-default).
varianteIDpath sim

Respostas

códigosignificado
200desvinculada
401Sem sessão / sem tenant na sessão.
404Recurso inexistente ou fora do escopo da sessão (não revela existência em outra conta).

Exemplo

curl -X DELETE 'https://api.pages.asender.net/v1/paginas/id_AQUI/variantes/varianteID_AQUI' \
  -H 'Authorization: Bearer SEU_TOKEN'
PUT /v1/paginas/{id}/variantes/{varianteID}

Renomear o braço e/ou ligar a rota própria da página-variante

Onde é usadatela de A/B.
Efeitosuma escrita; aceito com o teste rodando.

Parâmetros

nomeemobrigatóriodescrição
idpath simid da página; o acesso é conferido contra a sessão (deny-default).
varianteIDpath sim

Respostas

códigosignificado
200ok
401Sem sessão / sem tenant na sessão.
404Recurso inexistente ou fora do escopo da sessão (não revela existência em outra conta).

Exemplo

curl -X PUT 'https://api.pages.asender.net/v1/paginas/id_AQUI/variantes/varianteID_AQUI' \
  -H 'Authorization: Bearer SEU_TOKEN' \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: <uuid-v4>' \
  -d '{ }'
GET /v1/paginas/ab-vinculos

Páginas da conta em teste A/B não encerrado (selo da lista)

Onde é usadalista de páginas do painel.
Efeitosuma leitura sob o tenant da sessão.

Respostas

códigosignificado
200ok
401Sem sessão / sem tenant na sessão.

Exemplo

curl -X GET 'https://api.pages.asender.net/v1/paginas/ab-vinculos' \
  -H 'Authorization: Bearer SEU_TOKEN'

version

GET /version sem schema na spec

nome do serviço, versão e o commit que gerou o binário.

Onde é usadarota pública, usada por ops e por quem investiga incidente.
Efeitosescreve JSON na resposta. # Por que ela é PÚBLICA, e o que ela não conta A primeira pergunta de todo incidente é "que versão está no ar?" — e ela tem de ser respondível sem credencial, porque quem investiga muitas vezes ainda não tem uma. O que sai é o que já está no docker inspect de quem tem acesso ao host: nome, versão e commit. Nunca configuração, nunca dependência, nunca endereço interno — isso é topologia, e topologia é do console de plataforma. A versão vem de LDFLAGS no build, e o default é dev: um binário sem carimbo diz dev em vez de mentir uma versão que ninguém emitiu.

Exemplo

curl -X GET 'https://api.pages.asender.net/version' \
  -H 'Authorization: Bearer SEU_TOKEN'