# API Asender — documentação completa Este arquivo é a documentação INTEIRA da API aberta, em texto puro: convenções, guias e todas as 612 operações, com o que cada uma faz, o que recebe e o que devolve. Ele existe porque um agente não navega — precisa do contexto todo de uma vez. A versão navegável para pessoas está em https://developers.asender.net; cada API tem a sua página em https://developers.asender.net/apis/.html. Especificação OpenAPI unificada: https://developers.asender.net/openapi.json Dúvidas de uso do produto (não de integração): https://developers.asender.net Cada ferramenta é um sistema à parte, com endereço próprio. Elas compartilham a identidade (a mesma credencial vale nas superfícies da conta) e se integram entre si, mas falham separadamente: uma fora do ar não derruba as outras. APIs: https://api.crm.asender.net CRM -> /apis/crm.html https://api.mail.asender.net E-mail -> /apis/mail.html https://auth.asender.net Identidade (OIDC / OAuth 2.1) -> /apis/auth.html https://api.pages.asender.net Páginas -> /apis/pages.html https://api.asender.net Plataforma -> /apis/platform.html (interno) Atendimento [em breve] -> /apis/atendimento.html (interno) E-mail Marketing [em breve] -> /apis/emailmarketing.html (interno) Push [em breve] -> /apis/push.html (interno) SMS [em breve] -> /apis/sns.html (interno) WhatsApp [em breve] -> /apis/zapify.html ============================================================================== # GUIA: Integrações: todos os meios ============================================================================== Este é o mapa de **todos os meios de integrar com a Asender**. Para cada um: para que serve, quando usar, como autenticar, onde fica e qual guia aprofunda. Se você ainda não sabe qual usar, vá direto para a [tabela de decisão](#quero-x-use-y). A Asender é um conjunto de ferramentas independentes (e-mail, CRM, páginas, mensageria, atendimento) sob uma identidade só. Cada ferramenta tem a API dela num endereço próprio; uma **API pública da plataforma** reúne o envio de mensagens e a leitura de dados. Uma ferramenta fora do ar não derruba as outras. ## Visão rápida | Meio | Para que serve | Autenticação | Endereço | | --- | --- | --- | --- | | [API REST pública](#api-rest-publica) | Enviar e-mail, SMS, WhatsApp, push, Telegram; ler leads, campanhas, mensagens, analytics | API key `sk_live_`/`sk_test_` com escopos; token de app OAuth (só leitura) | `https://api.asender.net/v1` | | [API de E-mail](#api-de-e-mail) | Caixas, domínios e envio a partir de uma caixa | API key `sk_…` | `https://api.mail.asender.net` | | [API do CRM](#api-do-crm) | Operar o CRM como o painel faz | Sessão do usuário | `https://api.crm.asender.net` | | [API de Páginas](#api-de-paginas) | Criar e publicar páginas, formulários, enviar leads | API key com escopos `pages:*`/`forms:*`; sessão | `https://api.pages.asender.net` | | [Entrar com Asender](#entrar-com-asender-oauth) | Login de usuários Asender no seu app e leitura de dados com consentimento | OAuth 2.1 + PKCE | `https://auth.asender.net` | | [Servidor MCP](#servidor-mcp) | Agentes de IA operando a conta | OAuth (token de app, com registro automático do cliente) ou chave `sk_*` | `https://api.asender.net/mcp` | | [Webhooks](#webhooks) | Ser avisado de eventos no seu sistema | Assinatura v1 (`Asender-Signature`) | seu endpoint | | [SMTP e IMAP](#smtp-e-imap) | Clientes de e-mail e sistemas legados | Senha de aplicativo | hosts no guia | | [No navegador](#no-navegador) | Formulário, pixel, chat e Web Push no seu site | Chaves públicas do navegador (nunca `sk_`) | `https://sites.asender.net` | | [Domínios e DNS](#dominios-e-dns) | Enviar e publicar no seu domínio | Conta com acesso ao DNS | seu provedor DNS | | [Conversões e conectores](#conversoes-e-conectores) | Meta/Google/TikTok; Kommo; HTTP próprio | Credenciais do destino | painel | | [Para agentes de IA](#para-agentes-de-ia) | Documentação em texto puro e OpenAPI | pública | `https://developers.asender.net` | --- ## API REST pública **Para que serve.** É a porta de integração de máquina da plataforma. Dois grupos de rotas: - **Enviar mensagens**, de servidor para servidor: `POST /v1/emails/send` e `/v1/emails/batch` (até 500), `POST /v1/sms/send`, `POST /v1/whatsapp/send`, `POST /v1/push/send`, `POST /v1/telegram/send`, `POST /v1/whatsweb/send` (WhatsApp pelo aparelho) e `POST /v1/telegramconta/send` (Telegram pela conta). Também `GET /v1/senders` (de quais endereços sua chave pode enviar). - **Ler dados** da conta: `/v1/leads`, `/v1/forms` (e envios), `/v1/conversions`, `/v1/campaigns` e `/v1/reports/email/*`, `/v1/messaging/*` (SMS, WhatsApp, push), `/v1/analytics/*`, `/v1/conversations`, `/v1/journeys`, mais `/v1/emails` (histórico), `/v1/account`, `/v1/account/usage` e `/v1/logs` (o log das suas próprias requisições). **Quando usar.** Seu backend dispara mensagens transacionais ou de campanha, ou sincroniza leads e métricas para outro sistema. **Autenticação.** `Authorization: Bearer sk_live_…` (produção) ou `sk_test_…` (teste). Cada chave tem **escopos** (`email:send`, `sms:send`, `leads:read`, `campaigns:read`…); rota fora do escopo responde `403`. Um app OAuth de terceiro também chama as rotas de **leitura**, com o consentimento do usuário. A chave **nunca** vai para o navegador. ```bash curl -X POST 'https://api.asender.net/v1/emails/send' \ -H 'Authorization: Bearer sk_live_SUA_CHAVE' \ -H 'Content-Type: application/json' \ -H 'Idempotency-Key: 5b0c4d1e-8a3f-4e7b-9c21-aa17c2f0e001' \ -d '{ "Source": "avisos@seudominio.com.br", "Destination": { "ToAddresses": ["cliente@example.com"] }, "Message": { "Subject": { "Data": "Seu código" }, "Body": { "Text": { "Data": "Código: 123456" } } } }' ``` **Guias.** [Referência da Plataforma](../apis/platform.html) · [Convenções](convencoes.html) (formato, erros, paginação, idempotência, limites) · [Chaves de API e escopos](chaves-de-api.html). --- ## API de E-mail **Para que serve.** Administrar domínios e caixas de e-mail corporativo, ler e enviar mensagens de uma caixa, rascunhos, contatos e assinatura. **Quando usar.** Você precisa enviar **a partir de uma caixa que existe** (ou construir um cliente de e-mail sobre a plataforma). Para e-mail transacional de qualquer endereço de um domínio verificado (por exemplo, código de verificação), use `POST /v1/emails/send` da API pública, acima — não exige criar caixa. **Autenticação.** API key `Authorization: Bearer sk_…`. Escopo de administração: `mail:admin`. **Endereço.** `https://api.mail.asender.net` — envelope de erro `{ "error", "message" }`. **Guias.** [API de E-mail](../apis/mail.html) · [Domínios e DNS](dominios.html) · [SMTP e IMAP](smtp-imap.html). --- ## API do CRM **Para que serve.** Contatos, estágios, segmentos, jornadas, alertas e destinos de lead. **Quando usar.** Você está construindo uma interface que opera o CRM **como usuário logado**. A API do CRM é uma superfície de **sessão**: **não aceita API key**. Integração máquina-a-máquina com dados de CRM passa pela API pública: leitura de contatos em `GET /v1/leads`, jornadas em `GET /v1/journeys` e conversões em `GET /v1/conversions`, com os escopos `leads:read`, `journeys:read` e `conversions:read`. **Autenticação.** Sessão OIDC do usuário (`Authorization: Bearer `) e o cabeçalho `X-Asender-Tenant` com a conta em que opera. **Endereço.** `https://api.crm.asender.net` — envelope de erro `{ "erro": "…" }`. **Guia.** [API do CRM](../apis/crm.html). --- ## API de Páginas **Para que serve.** Criar, editar e publicar páginas e formulários, mídia, domínios e redirects; ler a analytics do que foi publicado; **enviar leads** de um sistema seu para um formulário. **Quando usar.** Automatizar a construção de páginas (inclusive por agentes de IA) ou capturar leads de um backend próprio. **Autenticação.** Três públicos: sessão do usuário (construtor), **API key** com escopo (`pages:read`, `pages:write`, `pages:publish`, `forms:read`, `forms:write`) para integração de servidor, e credenciais públicas do navegador (formulário embutido, pixel). **Endereço.** `https://api.pages.asender.net` (a página publicada e o runtime ficam em `https://sites.asender.net`) — envelope de erro `{ "erro": "…" }` na autoria. **Guias.** [API de Páginas](../apis/pages.html) · [O JSON do documento v2](pages-documento-v2.html). --- ## Entrar com Asender (OAuth) **Para que serve.** Seu app identifica um usuário Asender e, com o consentimento dele, lê dados da conta pela API pública, com escopos mínimos (somente leitura). **Quando usar.** Você tem um produto de terceiro (site, SaaS, ferramenta interna) e quer "Entrar com Asender", ou precisa de acesso delegado a dados de clientes sem pedir a chave de API deles. **Autenticação.** Authorization Code com **PKCE `S256` obrigatório**, cliente confidencial (`client_secret_basic`), `redirect_uri` exata. Endpoints no discovery: `https://auth.asender.net/.well-known/openid-configuration`. O token resultante chama a API pública como `Authorization: Bearer `. **Cadastro do app.** Você mesmo cadastra e gerencia o app no **Portal de Aplicativos** do painel (`admin.asender.net`, em Desenvolvedores > Aplicativos): credenciais, escopos, testadores e pedido de revisão à equipe Asender. O guia de [Aplicativos](aplicativos.html) descreve o painel, o ciclo de vida (desenvolvimento, revisão, publicado) e o selo Verificado. **Guias.** [Entrar com Asender](entrar-com-asender.html) (fluxo completo, erros e exemplo de código) · [Identidade (OIDC / OAuth 2.1)](../apis/auth.html). --- ## Servidor MCP **Para que serve.** Conectar assistentes e agentes de IA (que falam o Model Context Protocol) diretamente à sua conta Asender, como ferramentas. **Quando usar.** Você quer que um agente consulte dados ou execute ações sem escrever um cliente HTTP. **Autenticação.** OAuth: o cliente MCP se registra sozinho em `auth.asender.net` (CIMD ou DCR), o usuário autoriza na tela de consentimento e o agente apresenta o token no servidor MCP. Também vale uma chave de API `sk_*` no header `Authorization`. O registro automático está em [Aplicativos](aplicativos.html#clientes-registrados-automaticamente-mcp). **Endereço.** `https://api.asender.net/mcp`. Somente leitura nesta fase. Veja o guia de [MCP](mcp.html). --- ## Webhooks **Para que serve.** A Asender chama o **seu endpoint** quando algo acontece (um e-mail ou SMS com resultado, um lead novo, uma submissão de formulário, uma entrega do conector), e recebe webhooks de provedores (status de mensagem, pagamento). **Quando usar.** Você prefere ser avisado a ficar consultando a API (polling). Para ler o que mudou desde a última vez, combine com `updated_after` nas rotas de dados da API pública. **Autenticação.** Todos os emissores usam o formato **Asender Webhook v1**: `Asender-Signature: t=,v1=` (HMAC-SHA256 sobre `"."`, tolerância de 300 s), mais `Asender-Webhook-Id` (deduplicação) e `Asender-Event`. **Verifique a assinatura sobre o corpo cru** antes de confiar no conteúdo. Os cabeçalhos antigos saem em 2027-04-20. Endpoints de eventos da conta: `/v1/webhooks` com chave de escopo `webhooks:manage`. **Guia.** [Webhooks](webhooks.html) traz o formato v1, um verificador em Node, Python, Go e PHP, o cadastro de cada emissor, as tentativas e como tratar duplicatas. --- ## SMTP e IMAP **Para que serve.** Usar a caixa Asender de um cliente de e-mail (Thunderbird, Outlook, celular) ou enviar de um sistema legado que só fala SMTP. **Quando usar.** O software não consegue chamar HTTP, ou o usuário final quer o e-mail no app de sempre. **Autenticação.** **Senha de aplicativo** (uma por dispositivo ou sistema, revogável), nunca a senha da conta. O DNS do domínio publica o autoconfig para que os clientes se configurem sozinhos. **Guia.** [SMTP e IMAP](smtp-imap.html) (hosts, portas, senhas de aplicativo) e [Domínios e DNS](dominios.html). --- ## No navegador **Para que serve.** O que roda no site do **visitante**: formulário de captura (embed por ` ``` `frm_0123456789abcdef` é o **id do formulário** (`formID`). Ele aparece no código copiado e na URL de envio do bloco **API**, na mesma tela. O id é público: quem abre o código-fonte da sua página o vê. A proteção da conta é a lista de sites autorizados (veja abaixo), não o sigilo do id. O `embed.js` injeta o formulário **no lugar da própria tag ` ``` `asender:enviado` e `asender:erro` não sobem até `document`; escute no próprio formulário (`form.addEventListener(...)`). No envio nativo (sem JavaScript) a página não fica aberta, então nenhum desses eventos acontece. ## 2. Pixel de analytics O pixel mede o comportamento na página: visita, rolagem, cliques, formulário visto, iniciado e enviado, tempo na página e saída. Ele é opcional; o formulário incorporado funciona e é contado sem ele (veja "Formulário sem pixel"). ### Criar e instalar 1. No painel de Páginas, abra **Analytics → Pontos de coleta** e crie um ponto de coleta (o pixel). Na API, `POST https://api.pages.asender.net/v1/pixels` (com sessão) devolve o `snippet` pronto. 2. Em **Domínios autorizados** do pixel, cadastre o domínio do seu site (por exemplo `www.seusite.com.br`). Enquanto ele não estiver na lista, o navegador do visitante recebe **403** e nenhum evento sai. 3. Cole o snippet antes de ``. Num site que não é do Asender, o snippet deve ficar assim: ```html ``` > **Endereço absoluto.** O snippet que o painel e a API (`POST /v1/pixels`) geram já traz `src`, `data-lz-collect` e `data-lz-replay-url` com o endereço público do runtime. Se você colou um snippet **antigo**, com `src="/pixel.js"` relativo, ele aponta para o seu próprio domínio e não carrega: troque pelo snippet acima ou gere o código de novo. Como reforço, o `pixel.js` também aceita `data-base` (a origem do runtime) e, sem ele, usa a origem do próprio `src` para `/collect` e `/replay`. Em páginas publicadas pelo próprio Asender nada disso é necessário. | Atributo | Para que serve | | --- | --- | | `data-lz` | A credencial assinada do ponto de coleta (identifica conta e pixel; não é segredo). Sem ela o script não faz nada. | | `data-lz-collect` | Endereço de `/collect`. Padrão: `/collect` na origem do `src` do script (ou de `data-base`). | | `data-lz-replay` | `1` liga a gravação de jornada (veja abaixo). Ausente, nada é gravado. | | `data-lz-replay-url` | Endereço de `/replay`. Padrão: `/replay` na origem do `src` do script (ou de `data-base`). | | `data-base` | Origem do runtime (ex.: `https://sites.asender.net`) para `/collect` e `/replay`. Padrão: a origem do `src`. | | `data-lz-click` | Em qualquer elemento seu: o texto que vira o nome do clique no relatório. | ### O que ele coleta O script manda lotes de eventos para `POST /collect` (JSON, com a credencial no corpo). Eventos: `page_view`, `scroll_depth` (25, 50, 75 e 100%), `click` (apenas em links, botões e elementos com `data-lz-click`, com posição em porcentagem da página), `form_view`, `form_start`, `form_submit`, `time_on_page` e `exit`. A sessão é um id aleatório guardado em `sessionStorage` (chave `_lz_s`), que morre quando a aba fecha. **Sem cookie**, sem e-mail, sem nome, sem fingerprint. ### Formulários: o atributo `data-asender-form` O pixel procura na página os `
` e emite: - `form_view`, uma vez por formulário, quando metade dele fica visível (ou no carregamento, em navegador sem `IntersectionObserver`); - `form_start` no primeiro foco em um campo do formulário; - `form_submit` no envio. Cada evento leva o `formID`, e é isso que agrupa os números por formulário no relatório. Como o HTML gerado já traz `data-asender-form`, nenhum passo extra é necessário. Se você monta o HTML à mão, escreva o atributo com o id do formulário (o nome antigo `data-form-id` ainda é aceito). ### Formulário sem pixel Um formulário incorporado em site **sem o pixel** também aparece no Analytics geral. O script do próprio formulário manda o funil dele para `POST /f/{formID}/eventos`, e o servidor espelha três eventos (`form_view`, `form_start`, `form_submit`) no Analytics geral, marcados com origem `embed` e o host do site. Se o script detecta que o pixel está na página (`script[data-lz]`), o espelho não acontece, para não contar em dobro. ### Gravação de jornada (`/replay`) Com `data-lz-replay="1"`, o pixel grava a **sequência de interações**: clique (seletor do elemento e posição), campo tocado, rolagem, navegação e tamanho da janela. **Não é gravação de tela nem de DOM.** Em campos de formulário, registra apenas o tipo do campo e o comprimento do valor em faixas (0, 3, 8, 16, 32, 64), nunca o texto digitado, e só quando o campo perde o foco, nunca por tecla. Campos de senha, cartão, CPF/CNPJ e semelhantes (pelo nome ou id) saem como "sensível" e sem comprimento. O seletor traz só tag, id e duas classes. Para ler as jornadas: `GET /v1/replay/sessions` e `.../sessions/{id}/events` na API de Páginas (exige sessão). Como o snippet do painel já inclui `data-lz-replay="1"`, **a gravação de jornada vem ligada**. Para desligar, remova o atributo. Veja "Privacidade" no fim. ## 3. Chat do atendimento O widget de chat abre uma conversa do visitante direto na caixa de entrada do Atendimento. ### Instalar 1. No Atendimento, abra **Bot com IA**, seção **Widget de chat**, informe as **origens do site** (até 10, como `https://www.seusite.com.br`) e copie o snippet. O painel gera a **chave de site** `pk_chat_…` a partir das origens. 2. Cole antes de ``: ```html ``` | Atributo | Obrigatório | Para que serve | | --- | --- | --- | | `src` | sim | O widget, servido por `sites.asender.net/chat.js`. | | `data-site-key` | sim | A chave de site `pk_chat_…`. Sem ela o widget **não monta** e escreve um aviso no console. | | `data-base` | sim | A API do chat (`https://api.atendimento.asender.net`). Sem ele o widget **não monta** e escreve um aviso no console: a origem do `src` é o host de sites, que não é a API do chat. | | `data-titulo` | não | Título do painel do chat. | | `data-saudacao` | não | Primeira mensagem de boas-vindas. | | `data-intervalo-ms` | não | Intervalo (ms) da busca de novas mensagens quando a conexão ao vivo (SSE) não está disponível. Padrão: 4000. | | `data-manual` | não | Presente, desliga a montagem automática (para quem chama `montarChat` por conta própria). | | `data-asender-chat` | não | Marca a tag como a do widget quando o `src` não é idêntico ao do snippet (por exemplo, atrás de proxy). | Cor, título, saudação e posição do widget vêm da **aparência** salva na tela do Widget, que o script lê ao montar. Os atributos `data-titulo` e `data-saudacao` sobrescrevem texto na instalação. ### Segurança da chave de site A chave de site é **pública**: ela está no código-fonte da página. A defesa é a lista de origens assinada dentro dela, conferida **no servidor** a cada chamada, mais o limite de frequência por chave. Ela só abre o chat; não vale para nenhuma outra API. Mudou o domínio do site? Gere a chave de novo com as origens novas. ### Cookie da conversa Na primeira mensagem, o servidor grava o cookie **`asender_chat`**: um token aleatório, `HttpOnly`, `Secure`, `SameSite=None`, válido por 30 dias e restrito ao caminho `/public/v1/chat` (é enviado só às rotas do chat, nunca a outras). É ele que reencontra a conversa quando a pessoa volta ao site. Como o widget fala com um domínio diferente do seu site, é um cookie de terceiro; navegadores configurados para bloquear cookies de terceiros podem impedir que a conversa seja retomada. ## 4. Web Push Web Push envia notificações a quem aceitou receber, mesmo com a aba fechada. O seu site registra o navegador do visitante no Asender; as campanhas saem depois, pela ferramenta de Push. ### Passo a passo 1. No app de **Push**, abra **Coletar inscrições**, informe as origens do site (de 1 a 10) e gere o pacote. Ele traz quatro itens: a **chave de site** `pk_push_…`, o **service worker** `asender-sw.js`, o **manifest** e o **snippet** de inscrição. A chave de site vale **só** para as origens informadas. 2. Salve o service worker como **`/asender-sw.js` na raiz do site**. Um service worker só controla o diretório em que é servido; num subdiretório ele não recebe push do site inteiro. 3. Salve o manifest como `/manifest.json` e referencie com ``. Coloque também os ícones `/icon-192.png` e `/icon-512.png`, que o manifest e as notificações usam. 4. Cole o snippet em uma tag ` ``` Spam e respostas sem `lead_id` não disparam o evento. O servidor lê `_fbp`/`_fbc` (só leitura) e os ids de clique (`gclid`, `fbclid`, `ttclid`…) capturados na chegada; nada disso aparece em log. ### 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: ```bash curl -X POST 'https://api.asender.net/api/tenants//api-keys' \ -H 'Authorization: Bearer ' \ -H 'Content-Type: application/json' \ -d '{ "name": "blog-formularios", "scopes": ["forms:write", "forms:read"] }' # → o segredo sk_… aparece UMA vez ``` - `forms:write` cria/edita/apaga formulários **e envia leads**; `forms:read` só lê. Para o fluxo deste guia, `forms:write` basta; `forms:read` é útil para conferir o que está no ar. - `sk_test_…` fora de produção, `sk_live_…` só em produção. - **O segredo vive só no servidor** (variável de ambiente, secret manager). Nunca no JavaScript do navegador nem em `NEXT_PUBLIC_*`. Chave vazada: revogue na Plataforma — para de valer na hora. ### 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. ```bash 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 blog | `tipo` no Asender | Observação | | --- | --- | --- | | texto (`input text`) | `texto` | | | área de texto (`textarea`) | `texto_longo` | | | e-mail | `email` | alimenta o e-mail do contato | | telefone (`tel`) | `telefone` | alimenta o telefone do contato | | número | `numero` | | | caixa de seleção única (`checkbox`) | `booleano` | `valor_marcado` define o valor enviado quando marcada | | `select` / `radio` | `opcao` | liste os valores em `opcoes[]` | | data | `data` | | | `hidden` | `oculto` | | | URL | `url` | | Outras propriedades do campo: - `chave` — a **chave do catálogo de campos nativos** (ex.: `empresa`, `cargo`) liga o campo ao atributo correspondente do contato no CRM. Sem `chave`, o campo vira um atributo personalizado com o nome do campo (nunca é descartado). - `obrigatorio` — recusa o envio sem o campo (400). - Consentimento: `exige_consentimento: true` faz o envio ser **recusado** sem consentimento; `texto_consentimento` e `base_legal` (`consentimento`, `legitimo_interesse`, `execucao_contrato`, `obrigacao_legal`) são do formulário. Alternativamente marque um campo `booleano` com `consentimento: true` (a caixa de aceite). 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): ```bash 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.) | Campo | O que é | | --- | --- | | `campos` | mapa nome→valor dos campos **declarados** no passo 2 | | `id_externo` | id desta submissão **no seu sistema**; é a chave de deduplicação (por formulário). Até 255 caracteres. O header `Idempotency-Key` serve no lugar | | `consentimento` | `true` equivale a `campos.consentimento = "on"`; obrigatório se o formulário `exige_consentimento` | | `ocorrido_em` | quando o visitante enviou (RFC 3339). Fora da janela — mais de 5 min no futuro ou mais de 365 dias no passado — é recusado (400) | | `origem` | `url` (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 - **CRM** — o contato é criado (ou reconhecido por e-mail/telefone) e o lead entra no funil; se a mesma pessoa **volta**, o lead é **requalificado** (estágio volta a "novo", com tag e contador) em vez de duplicar. - **Caixa de atendimento** — o envio vira mensagem na inbox, conversa por contato no canal **formulário**; reabre quando o lead volta. - **Pages** — os envios ficam na aba de envios do formulário (histórico de coleta, para auditoria). - **Marketing** — o contato entra na lista/segmento só se o destino do formulário estiver configurado para isso **e** houve opt-in (consentimento). Sem consentimento, não há comunicação de marketing. ### 5. Erros, escopos, limites e LGPD Envelopes (a spec diz a verdade por processo): Autoria (`/v1/formularios…`): ```json { "erro": "a chave não tem o escopo forms:write" } ``` Envio de lead (`/v1/leads`, `/v1/forms/…`): ```json { "error": { "message": "o formulário exige consentimento" } } ``` | HTTP | Significado nesta integração | | --- | --- | | 400 | corpo inválido; campo não declarado/inválido; consentimento exigido ausente; `ocorrido_em` fora da janela | | 401 | chave ausente, inválida, revogada ou conta inativa | | 403 | a chave não tem o escopo da rota (`forms:read`/`forms:write`) | | 404 | formulário inexistente **ou de outra conta** (não revela existência) | | 409 | `external_id` ou nome já em uso | | 422 | definição de formulário inválida; `Idempotency-Key` já usada com outro corpo | | 429 | limite excedido — respeite `Retry-After` | | 503 | não deu para validar a chave agora — tente de novo com backoff | - **Limites:** 300 requisições por minuto por conta no envio de lead (excesso = 429 com `Retry-After`) e **64 KB** por corpo. - **LGPD:** quem decide a base legal e o texto do aceite é o **formulário** (`base_legal`, `texto_consentimento`); com `exige_consentimento`, o envio sem consentimento é recusado. Mande o `consentimento` que o visitante de fato deu — não o fabrique — e `ocorrido_em` com o instante real. --- ## 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. > O pacote **ainda não está publicado no npm**: publicação planejada; hoje use o > snippet (veja o guia [No navegador](https://developers.asender.net/guias/navegador.html)). > O SDK vive no repositório (`asender_pages_sdk`). | modo | quando | endpoint | credencial | | --- | --- | --- | --- | | **server-to-server** | seu backend/route-handler envia o lead | `POST /v1/leads` | API key `sk_*`, **lida de env no servidor** | | **cliente anônimo** | o browser do visitante posta direto | `POST /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](https://developers.asender.net/apis/platform.html)). 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. ```ts // 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. ```ts 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; 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): ```bash curl 'https://api.pages.asender.net/v1/analytics/overview?pagina=&de=2026-08-01T00:00:00Z&ate=2026-08-29T00:00:00Z' \ -H 'Authorization: Bearer ' \ -H 'X-Asender-Tenant: ' ``` - `/overview`, `/breakdown` (`?dimensao=utm_source` — dimensão desconhecida responde **400 com a lista aceita**), `/events`, `/session/{id}`, `/traffic-quality`, `/ab`, `/heatmap` (`?lado=`). - **Tempo real**: `/live` (instantâneo) e `/live/stream` (**SSE**). ### Pixels O control plane dos pontos de coleta: `GET/POST /v1/pixels` (a criação devolve o **snippet** pronto com a credencial assinada e URLs **absolutas** do runtime público, prontas para colar em site externo), `…/domains` (as origens autorizadas a coletar) e `…/conversions` (as conversões rastreadas). ```bash curl -X POST 'https://api.pages.asender.net/v1/pixels' \ -H 'Authorization: Bearer ' \ -H 'X-Asender-Tenant: ' \ -H 'Content-Type: application/json' \ -d '{ "nome": "Landing principal", "ativo": true }' # → 201 { "id":"…", "nome":"Landing principal", "ativo":true, # "snippet":"" } ``` ### 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). ------------------------------------------------------------------------------ ## GUIA DA API: Páginas: o JSON do documento v2 (https://developers.asender.net/guias/pages-documento-v2.html) ------------------------------------------------------------------------------ # Documento de página v2: guia completo para agentes Este guia ensina uma pessoa **ou um agente de IA** a ler, criar, editar e publicar páginas e formulários do Asender Pages pela API pública, produzindo o JSON do documento **v2** certo na primeira tentativa. As tabelas de campos, limites e tipos são **geradas do JSON Schema do contrato** (`asender_docs/contratos/pages-documento-v2.schema.json`), o mesmo que os validadores Go (autoridade, roda no servidor) e TypeScript (roda no editor) provam concordar; por isso elas não envelhecem. Tudo que aparece em JSON aqui foi validado nos três lados. - **Base da API:** `https://api.pages.asender.net` (JSON, UTF-8). - **Referência de rotas:** [Pages API](https://developers.asender.net/apis/pages.html) (operações geradas do código) e o guia [Pages API: autenticação, erros, domínios](https://developers.asender.net/apis/pages.html#autenticacao). Este guia cobre o que elas não dizem: **o conteúdo da página**. - **Contexto inteiro para um agente:** [`llms-full.txt`](https://developers.asender.net/llms-full.txt) inclui este guia. - **Schema:** `pages-documento-v2.schema.json` (JSON Schema 2020-12), também publicado como `DocumentoV2` e `DocV2_*` na OpenAPI de Páginas. > **Regra de ouro para agentes:** o servidor é a autoridade. Valide localmente contra o > schema, **envie**, e se vier `422` leia `codigo` e `caminho` (seção 4), corrija **só aquele > nó** e reenvie. Nunca invente campo: campo desconhecido é recusado. ## 1. Início rápido para agentes ### 1.1 Credenciais: o que cada uma alcança Um agente externo trabalha com uma **API key** (`sk_live_...` / `sk_test_...`), criada pelo dono da conta em *Desenvolvedores → Chaves de API* com os **escopos mínimos** da tarefa. A conta é sempre a **da chave** (validada no servidor a cada chamada; chave revogada para na hora): nenhum header nem campo do corpo escolhe a conta, e um `tenant_id` no corpo é recusado. Página, rascunho, mídia ou domínio de outra conta responde `404`, igual a um id que não existe. | Escopo da chave | O que permite | | --- | --- | | `pages:read` | ler: `GET /v1/paginas`, `GET /v1/paginas/{id}`, `GET /v1/paginas/{id}/rascunho`, `GET /v1/paginas/{id}/seo`, `GET /v1/dominios`, `GET /v1/midias` | | `pages:write` | criar e editar, **sem publicar**: `POST /v1/paginas`, `POST /v1/paginas/{id}/versoes`, `PUT`/`DELETE /v1/paginas/{id}/rascunho`, `PUT /v1/paginas/{id}/seo`, `POST /v1/paginas/importar`, `POST /v1/paginas/{id}/midias/copiar`, `PUT /v1/paginas/{id}/dominio`, `POST /v1/paginas/lote` com `duplicar`/`mudar_dominio`, `POST /v1/midias`, `POST /v1/midias/de-url` | | `pages:publish` | **publicar e despublicar** (o ato visível ao público e indexável): `POST /v1/paginas/{id}/publicar` e `POST /v1/paginas/lote` com `publicar`/`despublicar` | | `forms:read` / `forms:write` | formulários e o envio de leads (seção 3.8) | Os escopos **não se implicam**: `pages:write` não lê (precisa de `pages:read` para listar) nem publica. Um agente que cria, confere e publica precisa dos três. Sem o escopo da rota, `403` (`{"erro":"a chave não tem o escopo pages:publish"}`); rota que não aceita chave, `401`. A **sessão OIDC** de uma pessoa (`Authorization: Bearer ` + `X-Asender-Tenant: `) continua alcançando tudo de autoria e é o que o painel usa; a chave **não** alcança editar slug/título (`PATCH`), testes A/B, roteamento, apagar mídia, SEO do site, redirects nem marca. **Regras que protegem o trabalho de pessoas** (valem só para chave): - **Rascunho com `?base=` obrigatório.** `PUT /v1/paginas/{id}/rascunho` sem `?base=` responde `428`. Leia o rascunho (`GET`, devolve `rascunho_em`) e devolva esse valor em `?base=`; sem rascunho anterior use `?base=` vazio. Se uma pessoa (ou outro agente) gravou no meio, o servidor responde `409` `RascunhoConflito` com o rascunho dela e **não grava nada**: os dois lados ficam preservados; mescle e tente de novo com o `rascunho_em` novo. - **Publicar exige a `versao`.** Com chave, `POST .../publicar` sem `{"versao": N}` responde `422`: o agente publica a versão que ele identificou (o retorno do `POST .../versoes`, ou `ultima_versao` em `GET /v1/paginas/{id}`), nunca "a mais recente" às cegas, que pode ser uma edição de pessoa ainda não revisada. `POST /v1/paginas/lote` com `publicar` publica a última versão **salva** e pula a página com rascunho mais novo (política `pular`, padrão). - **Nada é sobrescrito.** Cada `POST .../versoes` cria a versão seguinte; publicar move um ponteiro. Desfazer é publicar a versão anterior. - **Autoria visível.** Versões e rascunhos feitos por chave levam `apikey:` (coluna `criado_por`; o rascunho devolve `rascunho_por`), distinguindo agente de pessoa; as ferramentas de importação por CLI (`--nova-versao`) recusam sobrescrever a última versão feita por **pessoa**. - **Sem HTML cru.** Conteúdo com bloco/item `html` exige um usuário com nível elevado; chave de API recebe `403` nesse caso. - **Ritmo por chave.** Escritas: 60 por minuto por chave; publicar/despublicar: 20 por minuto; acima disso `429` com `Retry-After`. Leituras não contam. `POST /v1/paginas/lote` ainda tem o teto de 12 chamadas por minuto por conta. - **Tudo fica no log.** Cada chamada com chave aparece em *Desenvolvedores → Logs* (`servico: pages`), com a rota, o status, a chave e o conteúdo redigido. Os exemplos usam `$KEY` (a API key) e `$API`; o mesmo curl funciona com sessão trocando o header. ```bash export API=https://api.pages.asender.net export KEY='sk_live_...' # escopos: pages:read pages:write pages:publish H=(-H "Authorization: Bearer $KEY" -H 'Content-Type: application/json') # Com sessão de pessoa: -H "Authorization: Bearer $TOKEN" -H "X-Asender-Tenant: $CONTA" ``` ### 1.2 O documento mínimo válido ```json { "versao": 2, "secoes": [ { "id": "a", "colunas": [ { "id": "ac", "itens": [] } ] } ] } ``` Uma seção, uma coluna, zero itens. Com um título: ```json { "versao": 2, "secoes": [ { "id": "s1", "colunas": [ { "id": "s1c1", "itens": [ { "id": "s1c1i1", "tipo": "titulo", "props": { "texto": "Olá, mundo", "nivel": 1 } } ] } ] } ] } ``` Todo item tem `id`, `tipo` e `props` (objeto, pode ser `{}`). ### 1.3 Criar, editar e publicar (o caminho feliz) ```bash # 1) criar a página (nasce DESPUBLICADA, com a versão 1 = o template enviado) curl -sS -X POST "$API/v1/paginas" "${H[@]}" -d '{ "titulo": "Lançamento", "slug": "lancamento", "template": { "versao": 2, "secoes": [ { "id": "s1", "colunas": [ { "id": "s1c1", "itens": [ { "id": "t1", "tipo": "titulo", "props": { "texto": "Olá, mundo", "nivel": 1 } } ] } ] } ] } }' # -> 201 { "id":"", "slug":"lancamento", "dominio":".apages.xyz", "titulo":"Lançamento", # "versao_publicada":0, "ultima_versao":1, "url_publica":"https://.apages.xyz/lancamento" } # 2) salvar uma NOVA versão (cada POST cria a versão seguinte; nada é sobrescrito) curl -sS -X POST "$API/v1/paginas//versoes" "${H[@]}" -d @corpo-versao.json # {"template": {...}} # -> 201 { "versao": 2 } | 422 { "erro":"...", "codigo":"ErrPropInvalida", "caminho":"secoes[0]..." } # 3) publicar UMA versão, pelo número (chave: escopo pages:publish e `versao` obrigatória) curl -sS -X POST "$API/v1/paginas//publicar" "${H[@]}" -d '{ "versao": 2 }' # -> 200 { "versao_publicada": 2 } # Rascunho (auto-save, sem versão): leia o rascunho_em e devolva em ?base= (obrigatório com chave) curl -sS "$API/v1/paginas//rascunho" "${H[@]}" # -> 200 {template, rascunho_em} | 204 curl -sS -X PUT "$API/v1/paginas//rascunho?base=" "${H[@]}" -d '{"template": {...}}' # -> 200 { "rascunho_em": "..." } | 409 RascunhoConflito (com o rascunho do servidor) | 428 sem ?base= ``` Sem `template` no `POST /v1/paginas`, a página nasce com um documento inicial (uma seção `hero` com o título digitado). Corpo máximo: **512 KiB**. ### 1.4 O endereço da página e o domínio padrão Toda conta tem um **domínio padrão** num subdomínio de `apages.xyz` (ex.: `minhaconta.apages.xyz`). Uma página criada **sem** `dominio` nasce nele; a `url_publica` da resposta é o endereço para abrir. Para usar outro domínio (um próprio, já **verificado**), crie/atualize a página com `dominio` ou, depois, mova com `PUT /v1/paginas/{id}/dominio` (API key `pages:write`): ```bash curl -sS "$API/v1/dominios" "${H[@]}" # pages:read, o padrão vem primeiro curl -sS -X PUT "$API/v1/paginas//dominio" "${H[@]}" -d '{ "dominio_id": "", "caminho": "novo-slug" }' ``` Detalhes (301 do endereço antigo, erros 409/422) na seção 6.5. ### 1.5 Antes de gerar JSON 1. Escolha a estrutura (seção 2) e os tipos de item (seção 3.1). 2. Gere `id`s únicos no documento inteiro (sugestão: `s1`, `s1c1`, `s1c1i1`...). 3. Use **somente** os campos das tabelas da seção 3; tudo que não está lá é recusado. 4. Imagens: URL da galeria da conta ou `https://` externa (seção 5). 5. Valide contra o schema, envie, leia o `422` se vier (seção 4). ## 2. Modelo mental ``` documento (versao: 2) |- configuracoes (livre, passthrough) |- estilo (a PÁGINA: tema, fundo, textura, fontes, caixa do conteúdo) '- secoes[] (faixas horizontais, de cima para baixo) '- colunas[] (células lado a lado dentro da seção) '- itens[] (objetos atômicos: titulo, texto, imagem, botao...) '- abas / acordeao: painéis com itens aninhados (um único nível) ``` - **Seção** é uma faixa de página inteira: fundo, borda, padding, altura, âncora e comportamento (`normal`, `fixa_topo`, `popup`). Tem de ter 1..12 colunas. - **Coluna** é a célula da grade. Seu `largura` é um **peso** (`fr` do CSS grid): colunas 1+2 ocupam 1/3 e 2/3; cinco colunas de peso 1 dão quintos. Define também o alinhamento do conteúdo e o `gap` entre os itens. - **Item** é o objeto. Mora **sempre** dentro de uma coluna e não contém outros itens, exceto os painéis de `abas`/`acordeao` (um nível só, sem `abas`, `acordeao`, `html` nem `formulario` dentro). - **Estilo da página** (`estilo`) vem antes das seções: tema (`claro`/`escuro`/`auto`), fundo atrás de tudo (cor, degradê, imagem), textura sutil, fontes e a **caixa do conteúdo** (`estilo.conteudo`: o miolo contido de toda seção, a "coluna estreita" de uma página de links). **Responsividade.** Os pontos de quebra são fixos: celular até 767 px, tablet 768 a 1023 px, computador a partir de 1024 px. No celular as colunas **empilham** (`layout_celular: empilhar`, o padrão) ou ficam lado a lado (`manter`); `inverter_celular` inverte a ordem da pilha. `padding_celular` e `espacador.altura_celular` ajustam o respiro no celular. `ocultar_em` (seção ou item) esconde por dispositivo. Não há CSS livre: tudo é campo fechado. **Ids.** `^[A-Za-z0-9_-]{1,64}$`, **únicos no documento inteiro**: seção, coluna, item e os itens dentro de painéis dividem o mesmo espaço (um item de painel com o `id` de uma seção é `ErrID`). **Âncoras** (`secao.ancora`): `^[a-z][a-z0-9-]{0,63}$`, únicas na página; viram o `id` HTML da seção e o alvo de `#ancora` em botões e menus. **Ordem e tamanho.** As seções são renderizadas na ordem do array. Tetos importantes: 200 seções, 12 colunas por seção, 100 itens por coluna, **1000 itens por página** (contando os de painéis). Tabela completa na seção 3. **Versões do formato.** Só o `versao: 2` deve ser escrito. O formato v1 (lista plana de blocos) ainda é lido, mas está em saída; não gere v1. **Normalização.** O servidor valida e regrava o documento na forma canônica; o que você lê de volta pode ter a ordem das chaves diferente do que enviou. ## 3. Referência de campos Tudo nesta seção vem do schema. Convenções das tabelas: **Tipo** é o tipo JSON; "cor" e "url de mídia" são tipos nomeados (seção 3.4); **Padrão** é o que o renderer aplica quando a prop está ausente (só aparece quando verificado); "obrigatório" marca campos sem os quais o documento é recusado. Todas as props de item são opcionais, mas um item sem conteúdo (por exemplo um `botao` sem `rotulo`) não desenha nada útil. ### 3.1 Limites do documento | Limite | Valor | O que limita | |---|---|---| | `MaxSecoes` | 200 | seções por página | | `MaxColunas` | 12 | colunas por seção | | `MaxPesoColuna` | 12 | peso (`largura`) máximo de uma coluna | | `MaxItensPorColuna` | 100 | itens por coluna | | `MaxItensTotal` | 1000 | itens na página inteira (inclui os aninhados em abas/acordeão) | | `MaxItensLista` | 50 | entradas em `links`, `tags`, `cartoes`, `redes`, `menu` | | `MaxEspaco` | 400 | teto de padding/margem/gap/altura de espaçador (px) | | `MaxRaio` | 200 | teto de raio (px) | | `MaxBorda` | 20 | teto de espessura de borda (px) | | `MinLarguraMax` | 320 | menor `largura_max` de seção (px) | | `MaxLarguraMax` | 2400 | maior `largura_max` (px) | | `MaxAlturaPx` | 2000 | maior `altura_minima` em px | | `MaxAlturaVh` | 100 | maior `altura_minima` em vh | | `MaxAtrasoPopup` | 120 | maior `atraso_popup` (segundos) | | `GapPadraoItens` | 16 | gap padrão entre itens (px) | | `GapPadraoColunas` | 24 | gap padrão entre colunas (px) | | `LarguraMaxPadrao` | 1080 | largura do conteúdo contido sem `largura_max` (px) | | `MaxTamanhoID` | 64 | caracteres de um `id` | | `MaxTamanhoNome` | 80 | caracteres de `secao.nome` (conta code points, não bytes) | | `MaxTamanhoURL` | 2048 | caracteres de uma URL | | `MaxTextoCurto` | 300 | caracteres de títulos, rótulos, alt | | `MaxTextoLongo` | 20000 | caracteres do `texto` de um item `texto` | | `MaxTamanhoHTML` | 200000 | caracteres do item `html` | | `MaxTamanhoPrazo` | 64 | caracteres do `prazo` do contador | | `MaxTextoTag` | 60 | caracteres do texto de um chip / tag de cartão | | `MaxPaineis` | 20 | abas ou painéis por item | | `MaxItensPorPainel` | 20 | itens dentro de UM painel | | `MaxTituloPainel` | 200 | caracteres do título de aba/pergunta | | `MaxIconeTag` | 48 | maior `tamanho_icone` dos chips (px) | | `MaxFAQResposta` | 4000 | caracteres da resposta de uma pergunta de FAQ | | `MaxFAQPagina` | 50 | perguntas de FAQ somadas na página | | `MaxSubir` | 200 | maior `estilo.subir` (px) | | `MaxPaddingImagem` | 40 | maior `padding` da imagem (px) | | `MinIconeFundo` | 24 | menor contêiner de ícone (px) | | `MaxIconeFundo` | 72 | maior contêiner de ícone (px) | | `MaxColunasCartoes` | 4 | colunas da grade de cartões | | `MinLarguraCartao` | 96 | menor `largura_cartao` (px) | | `MaxLarguraCartao` | 400 | maior `largura_cartao` (px) | | `MinMiniatura` | 56 | menor `miniatura` (px) | | `MaxMiniatura` | 96 | maior `miniatura` (px) | | `MaxInclinar` | 12 | maior `inclinar` (graus) | | `MaxRotacaoCartao` | 15 | maior `rotacao` de um cartão (graus, para os dois lados) | ### 3.2 Os 20 tipos de item | Tipo | Para que serve | |---|---| | `titulo` | Título (`h1`..`h6`). Aceita marcação inline `[rótulo](url)` e `[[trecho]]`. | | `texto` | Parágrafo de texto. Aceita a mesma marcação inline do título e quebras de linha. | | `imagem` | Imagem, com forma, proporção, moldura e link opcional. | | `botao` | Botão (ou link-botão) com visual granular, vidro, ícones e imagem. | | `tags` | Chips (etiquetas) com ícone, link e visual por chip. | | `links` | Lista de botões de link (o miolo de um link-na-bio), com visual por entrada. | | `cartoes` | Galeria de cartões (foto, título, texto, tag, ícone, link) em grade, carrossel ou lista. | | `abas` | Abas; cada aba é um painel com itens aninhados (um nível só). | | `acordeao` | Acordeão (sanfona); com `faq: true` gera `FAQPage` no JSON-LD da página. | | `redes` | Ícones de redes sociais (Instagram, WhatsApp...) com link. | | `menu` | Menu de navegação (links de texto), horizontal ou vertical; pode colapsar no celular. | | `formulario` | Embute um formulário do Asender (campos reais, anti-spam e captura de lead). | | `video` | Vídeo incorporado (`