Pular para o conteúdo principal

Tela em iframe

Quando o app tem URL base no rascunho, o Botzetta mostra a sua página dentro do layout dele. A rota /apps/<slug>/<caminho>?<query> do Botzetta abre <URL base>/<caminho>?<query> num iframe:

<iframe
src="https://meuapp.exemplo.com/notas/1?x=2"
sandbox="allow-scripts allow-same-origin allow-forms allow-popups allow-popups-to-escape-sandbox allow-downloads"
allow=""
referrerpolicy="strict-origin">
</iframe>
  • A página roda na sua origem, isolada do Botzetta. A URL base tem de ser https: e nunca a origem do Botzetta.
  • Sem allow-top-navigation e sem allow-modals: alert(), confirm() e trocar a página do topo não funcionam. Peça ao Botzetta pelo protocolo (navegar, toast, confirmar).
  • Uma faixa "App de parceiro · conteúdo fornecido por …" fica sempre acima do iframe.
  • Caminhos aceitos: segmentos [A-Za-z0-9._~%-], sem ./.., // ou \. Caminho inválido abre a raiz do app.

Cabeçalho obrigatório: frame-ancestors​

O seu servidor precisa responder todas as páginas com:

Content-Security-Policy: frame-ancestors https://app2.botzetta.com

Use a origem do Botzetta que aparece no portal (página Sandbox) ou o valor que o rascunho do portal sugere. Sem o cabeçalho o navegador bloqueia o iframe; com outra origem, um site qualquer poderia enquadrar a sua página (clickjacking). A revisão do app confere esse cabeçalho.

Protocolo frame/1​

A página e o Botzetta conversam por window.postMessage com um envelope fixo:

{ "botzetta": "frame/1", "tipo": "pedido", "id": "p1", "dados": { "metodo": "toast", "args": { "mensagem": "Salvo", "tipo": "sucesso" } } }
Direçãotipodados
página → Botzettapronto{ "versaoSdk": "<qualquer texto>" }
Botzetta → páginacontexto{ protocolo, token, expiraEm, app, empresa: { id, nome }, usuario: { id, nome }, tema, rota, extensao? }
página → Botzettapedido (com id){ metodo, args }
Botzetta → páginaresposta (mesmo id){ "ok": true, "valor": … } ou { "ok": false, "erro": { codigo, mensagem } }
Botzetta → páginaevento{ "nome": "tema", "valor": "claro" | "escuro" } ou { "nome": "token", "valor": { token, expiraEm } }

Handshake​

  1. Ao carregar, a página manda pronto para o window.parent, com targetOrigin igual à origem do Botzetta.
  2. O Botzetta responde com contexto. Guarde a origem dessa mensagem: daí em diante, aceite e envie mensagens só para ela.
  3. Sem pronto em 10 segundos, o Botzetta mostra um erro com o seu e-mail de suporte e um botão "Tentar novamente". Cada recarga da página manda pronto de novo e recebe um contexto novo.
const ORIGEM_BOTZETTA = "https://app2.botzetta.com";
let contexto = null;

window.addEventListener("message", e => {
// Só o window.parent, só a origem do Botzetta, só o protocolo frame/1.
if (e.source !== window.parent || e.origin !== ORIGEM_BOTZETTA || e.data?.botzetta !== "frame/1") return;
if (e.data.tipo === "contexto" && !contexto) {
contexto = e.data.dados;
iniciar();
}
});
window.parent.postMessage({ botzetta: "frame/1", tipo: "pronto", dados: { versaoSdk: "meuapp-1" } }, ORIGEM_BOTZETTA);

Valide a origem sempre. Nunca use "*" como targetOrigin nem aceite mensagem de outra origem: o token de usuário viaja nessas mensagens.

Pedidos​

metodoargsResposta (valor)Regras
token—{ token, expiraEm }Token de usuário novo
navegar{ "caminho": "/pessoas" }—Só caminho interno do Botzetta (começa com /, sem //)
abrirPessoa{ "id": 42 }—Abre o cadastro da pessoa
toast{ "mensagem": "…", "tipo": "sucesso" | "erro" | "aviso" | "info" }—Até 200 caracteres; 5 a cada 10 s
confirmar{ "titulo": "…", "mensagem": "…", "destrutivo": true }true / falseUm por vez; mostrado como "<app> pergunta:". Espera o usuário: não ponha timeout
informarRota{ "caminho": "notas/124" }—Atualiza a URL do Botzetta sem recarregar o iframe (para o usuário copiar o link)
redimensionar{ "altura": 480 }—Só em extensão; 80 a 2000 px
fechar——Só em extensão

Erros possíveis em resposta.erro.codigo: ERR_FRAME_PARAMETRO, ERR_FRAME_METODO, ERR_FRAME_OCUPADO (já há um confirmar aberto), ERR_FRAME_LIMITE e ERR_FRAME_INDISPONIVEL (o Botzetta não conseguiu um token novo; tente de novo).

Tema​

contexto.tema e o evento tema dizem se o Botzetta está claro ou escuro. Aplique antes de mostrar a página para não piscar.

Token de usuário​

contexto.token identifica quem está usando a tela:

  • JWT RS256 de 10 minutos, iss: "botzetta-nucleo", aud: "app:<slug>", tipo: "usuario", sub (id do usuário) e cid (id da empresa). Só identidade: perfil e acessos não vêm no token;
  • o Botzetta manda um novo (evento token) 60 segundos antes de expirar. Você também pode pedir com o método token;
  • guarde em memória. Nunca em localStorage, sessionStorage ou cookie.

A página manda o token para o seu backend em cada chamada:

await fetch("/api/notas", { headers: { Authorization: `Bearer ${contexto.token}` } });

Validar no backend​

Nunca confie no que a página diz sobre usuário ou empresa. No seu backend, para cada requisição:

  1. Verifique o JWT com as chaves públicas de GET https://api.botzetta.com/plataforma/v1/.well-known/jwks.json: algoritmo RS256, a chave pelo kid do cabeçalho, iss = "botzetta-nucleo", aud = "app:<slug>", exp no futuro e tipo = "usuario". Guarde as chaves em cache (ex.: 10 minutos) e busque de novo se aparecer um kid desconhecido, no máximo uma vez a cada 30 segundos: senão um token forjado com kid aleatório vira uma busca por requisição.

  2. Consulte a sessão atual, com o mesmo token:

    GET https://api.botzetta.com/plataforma/v1/sessao
    Authorization: Bearer <token de usuário>
    { "usuarioId": 7, "companyId": 88, "perfil": "admin", "acessos": ["meuapp.notas.view"], "nivel": "nenhum" }

    Confira que usuarioId e companyId batem com sub e cid. Guarde a resposta em cache por até 30 segundos por token: um usuário desativado ou um acesso retirado deixa de valer nesse prazo. Erros: 401 ERR_TOKEN_INVALIDO / ERR_TOKEN_REVOGADO (logout, usuário desativado) e 403 ERR_APP_NAO_HABILITADO.

  3. Use a empresa do token (companyId) para escolher os tokens da instalação e os seus dados. Nunca uma empresa vinda da URL ou do corpo.

  4. Autorize pelo perfil e pelos acessos da sessão: tela que só o admin vê exige perfil === "admin"; tela com acesso GBAC exige o acesso na lista (["*"] = todos). Esconder no menu é só apresentação.

  5. Com nivel paywall ou total, a assinatura da empresa está pendente: recuse escritas.

O exemplo completo de verificação, sem bibliotecas, está no exemplo ponta a ponta.

Extensão no ticket​

Uma extensão ticket.acoes põe um botão nas ações do ticket, ao lado do campo de mensagem. Ela abre a página <URL base>/<url da extensão> num diálogo e entrega o ticket em contexto.extensao:

{ "ponto": "ticket.acoes", "id": "emitir-nota", "ticket": { "id": 901, "contatoId": 77, "whatsappId": 12, "filaId": 3, "usuarioId": 7 } }

Os ids são só contexto. Reconfira pela API antes de agir. No diálogo, redimensionar ajusta a altura e fechar fecha.