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-navigatione semallow-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ção | tipo | dados |
|---|---|---|
| página → Botzetta | pronto | { "versaoSdk": "<qualquer texto>" } |
| Botzetta → página | contexto | { protocolo, token, expiraEm, app, empresa: { id, nome }, usuario: { id, nome }, tema, rota, extensao? } |
| página → Botzetta | pedido (com id) | { metodo, args } |
| Botzetta → página | resposta (mesmo id) | { "ok": true, "valor": … } ou { "ok": false, "erro": { codigo, mensagem } } |
| Botzetta → página | evento | { "nome": "tema", "valor": "claro" | "escuro" } ou { "nome": "token", "valor": { token, expiraEm } } |
Handshake
- Ao carregar, a página manda
prontopara owindow.parent, comtargetOriginigual à origem do Botzetta. - O Botzetta responde com
contexto. Guarde a origem dessa mensagem: daí em diante, aceite e envie mensagens só para ela. - Sem
prontoem 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 mandaprontode 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
metodo | args | Resposta (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 / false | Um 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) ecid(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étodotoken; - guarde em memória. Nunca em
localStorage,sessionStorageou 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:
-
Verifique o JWT com as chaves públicas de
GET https://api.botzetta.com/plataforma/v1/.well-known/jwks.json: algoritmoRS256, a chave pelokiddo cabeçalho,iss = "botzetta-nucleo",aud = "app:<slug>",expno futuro etipo = "usuario". Guarde as chaves em cache (ex.: 10 minutos) e busque de novo se aparecer umkiddesconhecido, no máximo uma vez a cada 30 segundos: senão um token forjado comkidaleatório vira uma busca por requisição. -
Consulte a sessão atual, com o mesmo token:
GET https://api.botzetta.com/plataforma/v1/sessaoAuthorization: Bearer <token de usuário>{ "usuarioId": 7, "companyId": 88, "perfil": "admin", "acessos": ["meuapp.notas.view"], "nivel": "nenhum" }Confira que
usuarioIdecompanyIdbatem comsubecid. 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) e403 ERR_APP_NAO_HABILITADO. -
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. -
Autorize pelo
perfile pelosacessosda sessão: tela que só o admin vê exigeperfil === "admin"; tela com acesso GBAC exige o acesso na lista (["*"]= todos). Esconder no menu é só apresentação. -
Com
nivelpaywalloutotal, 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.