# O Widget

<https://unifokal.com/docs/widget>

## O Widget

São 2 passos. No **backend**, você cria a sessão do usuário no Flow, enviando o seu `reference_id` (o id do usuário no seu sistema; obrigatório: ele volta no webhook e na API para você correlacionar). A resposta traz o `id` da sessão. No **front**, você monta o widget com esse `id`. **Nenhum segredo vai ao navegador** (o único segredo é a sua `sk_`, que fica no seu backend). O ambiente (sandbox × produção) é definido pela chave secreta usada na criação da sessão. Os módulos são os do Flow. Você não os declara no widget.

**HTML** · A forma de uma linha (sem JavaScript seu) · no navegador

```html
<!-- Se voce so precisa montar o widget e nada mais, o proprio <script> faz tudo:
     basta o data-session. Nenhum segredo aqui, como no exemplo acima: o id da sessao
     vem do SEU servidor, e a sua chave sk_ nunca sai de la. -->
<div id="identidade"></div>
<script
  src="https://api.unifokal.com/dist/widget.global.js"
  data-session="vs_01J8ZQ4S8M2N3P4Q5R6S7T8U9V"
  data-container="#identidade"
  crossorigin="anonymous"
></script>

<!-- O QUE ESPERAR DE VOLTA: o widget monta sozinho no #identidade assim que o script carrega.
     Sem data-session ele nao faz nada, em silencio: nao ha o que montar sem sessao. -->
```

A versão com o seu backend criando a sessão, os cabeçalhos que a câmera exige e o tratamento de erro está inteira em [Integração ponta a ponta](https://unifokal.com/docs/integracao#integracao).

Para quem trava numa etapa, a [central de ajuda](https://unifokal.com/ajuda) explica cada mensagem do widget e o que fazer para seguir. Vale um link para ela na tela de ajuda do seu app.

COMO O WIDGET APARECE NA SUA PÁGINA

São três formas, e você escolhe no próprio trecho. **Embutido**: o card entra no lugar que você reservou. **Botão flutuante**: um botão fixo num canto abre a verificação, sem mexer no seu layout. **Aberto pelo seu botão**: o botão continua seu, e o widget abre quando alguém clica nele. Em qualquer uma delas o widget aparece do mesmo jeito para quem vai se verificar, e o seu site não consegue escondê-lo sem querer: ele se afirma na tela mesmo dentro de um layout que apaga tudo à volta.

Quem já integrou não precisa mudar nada. Reservar um lugar para o widget já é escolher o modo embutido, e é o que continua valendo. O painel também tem o seletor, em **Customização**, para quando você preferir decidir sem mexer no código; o que está escrito no trecho vence o painel.

**HTML** · Embutido no seu layout · no navegador

```html
<!-- Reservar um lugar ja e escolher este modo: nao precisa de data-mode.
     E o comportamento de sempre, e nada muda para quem ja integrou. -->
<div id="identidade"></div>
<script
  src="https://api.unifokal.com/dist/widget.global.js"
  data-session="vs_01J8ZQ4S8M2N3P4Q5R6S7T8U9V"
  data-container="#identidade"
  crossorigin="anonymous"
></script>

<!-- O QUE ESPERAR DE VOLTA: o card aparece dentro do #identidade assim que o script carrega, e a
     verificacao comeca na hora. -->
```

**HTML** · Botão flutuante (sem reservar lugar na página) · no navegador

```html
<!-- Nao precisa de <div>: o botao e o painel nascem no fim do <body> e se posicionam
     contra a janela, entao nenhum layout seu precisa mudar. Cole onde preferir, inclusive
     no <head>: se o corpo da pagina ainda nao existir, o widget espera ele existir. -->
<script
  src="https://api.unifokal.com/dist/widget.global.js"
  data-session="vs_01J8ZQ4S8M2N3P4Q5R6S7T8U9V"
  data-mode="floating"
  data-position="bottom-right"
  crossorigin="anonymous"
></script>

<!-- O QUE ESPERAR DE VOLTA: um botao fixo no canto escolhido. A verificacao comeca quando a pessoa
     clica, nao quando a pagina carrega. Cantos: bottom-right (padrao), bottom-left,
     top-right, top-left. Valor fora da lista e ignorado e cai no padrao. -->
```

**HTML** · Aberto pelo SEU botão · no navegador

```html
<!-- O botao e seu: estilo, texto e lugar continuam seus. Duas formas de ligar, e as
     duas valem ao mesmo tempo. -->
<button id="verificar">Verificar identidade</button>
<script
  src="https://api.unifokal.com/dist/widget.global.js"
  data-session="vs_01J8ZQ4S8M2N3P4Q5R6S7T8U9V"
  data-mode="trigger"
  data-trigger="#verificar"
  crossorigin="anonymous"
></script>

<!-- Sem seletor: marque qualquer elemento com data-idsaas-open. Funciona junto. -->
<a href="#" data-idsaas-open>Comecar verificacao</a>

<!-- O QUE ESPERAR DE VOLTA: o clique abre o painel. Vale para elementos criados DEPOIS da carga
     (o ouvinte e um so, no documento), entao funciona em pagina que troca de tela sem
     recarregar. Seletor invalido e ignorado, e a forma por atributo continua valendo. -->
```

ANTES DE COLAR: REGISTRE A ORIGEM DO SITE

O widget só carrega em **origens que você registrou**. No painel, em **Organização, origens do widget**, cadastre cada endereço onde ele vai rodar (por exemplo `https://app.suaempresa.com` e o seu ambiente de homologação). Origem não registrada é recusada pelo navegador no preflight, e o widget simplesmente não sobe naquele site. O bloqueio aparece no console do navegador como erro de CORS, não como erro da nossa API, então é fácil procurar no lugar errado.

A tela grava em `PUT /v1/dashboard/organization/allowed-origins`, que é uma rota de **painel**: a sua `sk_` não a alcança, e isso é de propósito. Ligar o produto num domínio novo é operação de conta, não chamada de servidor.

Isso vale para **toda** origem: o seu domínio de produção, o de homologação e o endereço que você usa no desenvolvimento. Cada uma entra como origem completa (esquema, host e porta), até 20 por conta.

O QUE O TITULAR VÊ ENQUANTO A CONFERÊNCIA ACONTECE

Depois de enviar as fotos, o titular fica numa tela de espera que **anda**: o indicador se move, o widget conta **quantas conferências já concluíram** e, se a espera passar de alguns segundos, a mensagem muda para uma que pede para ele continuar na tela. Quando o conjunto é fixo, a contagem sai como _2 de 3_; quando o seu Flow decide etapas pelo caminho, sai só o quanto já andou, porque prometer um total que muda seria pior do que não prometer nenhum. Nada disso diz **o que** estamos conferindo nem revela resultado parcial: é andamento, não veredito.

Você não configura nem liga nada para isso, e não custa nada. Vale para a tela do aparelho que capturou; no computador que só mostra o QR Code, a tela continua sendo a de **siga no celular**. A contagem é apresentação: a verdade do desfecho continua sendo o seu **webhook**.

BOTÃO DE ATENDIMENTO DENTRO DA VERIFICAÇÃO

Quando o titular trava (documento recusado, selfie reprovada, análise pendente), o widget pode mostrar um botão que abre **o seu canal de atendimento**, com o `reference_id` daquela sessão já dentro do link. O atendente é seu: nós não hospedamos chat e não cobramos por isso, está incluso. Você liga no painel, em **Customização**, informando o endereço do seu canal (WhatsApp, Crisp, Intercom, Zendesk, a sua central) e o texto do botão.

O endereço é um **modelo**: onde você escrever `{reference_id}`, nós colocamos o identificador daquela sessão. Duas regras que a validação aplica no save, e vale saber antes de colar: o endereço precisa ser **https**, e o `{reference_id}` só pode aparecer na **query** ou no **fragmento**, nunca no host nem no caminho. Exemplos que passam: `https://suporte.suaempresa.com/chat?ref={reference_id}` e `https://wa.me/5511999999999?text=ajuda%20{reference_id}`.

Preferimos que você use o **fragmento** (`#ref=` em vez de `?ref=`): o fragmento não é enviado ao servidor de destino, então o identificador do titular fica fora do log HTTP do seu fornecedor de chat e continua legível pelo JavaScript dele. O `wa.me` precisa de query e continua valendo.

Três coisas que a validação recusa de propósito, e por quê: **chave secreta na URL** (parâmetros como `token`, `api_key` ou `secret`), porque não guardamos segredo de terceiro; **o nosso próprio domínio**, porque o titular veria a nossa marca na barra de endereço de uma página que não é nossa; e **o placeholder no host ou no caminho**, porque ali um `reference_id` mal-intencionado mudaria o destino final do link. O titular vê o host de destino escrito ao lado do botão antes de clicar, e a verificação continua aberta na aba dele.

O identificador só acompanha o link nas **primeiras 24 horas** da sessão. Depois disso o botão continua aparecendo, e o link continua abrindo o seu canal, mas sem o `reference_id`: o atendente identifica pelo próprio canal. Isso existe para que um endereço de sessão vazado não vire uma consulta permanente ao identificador do titular.

LEMBRETE DE VERIFICAÇÃO NÃO CONCLUÍDA

Quando o seu fluxo tem verificação de e-mail por código e o titular **confirma o código** mas abandona a verificação em seguida, enviamos **um único** lembrete para aquele endereço, ainda dentro do prazo em que a sessão pode ser concluída. É um por verificação, para sempre: não existe segunda cobrança, nem sequência.

Três coisas que esse e-mail **não** faz, e vale saber antes de perguntar: ele não carrega link de acesso à verificação (o titular volta pela sua página, e nenhuma credencial de sessão viaja por e-mail), não contém dado do documento nem resultado, e não vai para quem apenas **digitou** um endereço sem confirmar o código. Endereço não confirmado pode ser de outra pessoa, e mandar mensagem para ela seria o defeito, não a feature.

O titular pode recusar novos lembretes pelo próprio e-mail. Quando ele recusa, guardamos só um código derivado do endereço, nunca o endereço, e a recusa vale para toda a plataforma, não apenas para a sua conta. A cláusula C.14 da Política de Privacidade descreve o tratamento. Lembrete por **SMS** não existe hoje.

O QUE A SUA PÁGINA PRECISA PERMITIR

O widget roda **dentro da sua página**, e é ela que abre a câmera. Então a sua página precisa: ser servida por **https** (o navegador não entrega câmera em http, salvo em `localhost`), **não negar a câmera** no cabeçalho `Permissions-Policy`, e ter uma `Content-Security-Policy` que permita o nosso bundle, a nossa API, o canal de tempo real e os workers do detector de rosto. O exemplo em [Integração ponta a ponta](https://unifokal.com/docs/integracao#integracao) traz os cabeçalhos prontos.

**Uma ressalva sobre Trusted Types.** O widget roda no seu documento, não em um iframe, então uma política [Trusted Types](https://developer.mozilla.org/docs/Web/HTTP/Headers/Content-Security-Policy/trusted-types) que você publique alcança o nosso código junto com o seu. Hoje, `require-trusted-types-for 'script'` em modo de bloqueio na sua página interrompe a captura. O sintoma é ruim de diagnosticar: a tela fica em branco e o erro sai só no console do navegador, longe de qualquer log nosso. Se a sua política exige Trusted Types, use `Content-Security-Policy-Report-Only` na rota da verificação enquanto avalia, e fale com a gente antes de ligar o bloqueio. Essa decisão é sua, e o cabeçalho é da sua página: nós não temos como relaxá-lo do nosso lado.

**Quer fixar a versão do bundle (SRI)?** A URL acima é a **móvel**: ela sempre serve a versão atual, com `no-cache`, e é a escolha certa para a maioria (você recebe correção nossa sem fazer nada). Se a sua política exige [Subresource Integrity](https://developer.mozilla.org/docs/Web/Security/Subresource_Integrity), use a **URL versionada**: ela é imutável por versão e vem com o hash publicado por nós. O par (URL, hash) está sempre em `https://api.unifokal.com/dist/integrity.json`, que é **gerado no nosso build** a partir dos bytes servidos. Nunca copie um hash de um texto (inclusive deste): leia do manifesto, de preferência pelo seu CI.

```
# 1) leia o par URL + hash do manifesto (gerado no nosso build)
curl -s https://api.unifokal.com/dist/integrity.json
{
  "_leia": "Par URL + integrity do bundle do widget UNIFOKAL, gerado no build …",
  "version": "b1efde7e5c593de78",
  "files": {
    "widget.global.js": {
      "url": "/dist/b1efde7e5c593de78/widget.global.js",
      "integrity": "sha384-…",
      "bytes": 166951
    },
    "widget.esm.js": {
      "url": "/dist/b1efde7e5c593de78/widget.esm.js",
      "integrity": "sha384-…",
      "bytes": 166820
    }
  }
}
# são DOIS artefatos: widget.global.js (a tag <script>, expõe IdSaas) e widget.esm.js
# (import de módulo, para quem empacota o próprio front). O par URL + integrity é por arquivo.

# 2) use os dois JUNTOS na página (o crossorigin é obrigatório para o SRI ser avaliado)
<script src="https://api.unifokal.com/dist/b1efde7e5c593de78/widget.global.js"
        integrity="sha384-…"
        crossorigin="anonymous"></script>
```

O que isso significa na prática: a versão que você fixou **não muda de conteúdo**, nunca. Quando publicamos um bundle novo, a versão passa a ser outra e o manifesto muda junto, então **fixar exige atualizar** (por isso a recomendação de ler o manifesto no seu CI em vez de colar o hash na mão). Uma versão que sai do ar responde `404`, e não outro conteúdo: preferimos falhar claro a servir bytes diferentes sob a URL que você assinou.

O EVENTO QUE O WIDGET DISPARA NA SUA PÁGINA

O widget avisa a sua página quando o estado da verificação muda, e avisa de novo quando ela **acaba**. São dois eventos, e os nomes são estáveis: `unifokal:state` a cada transição e `unifokal:done` uma única vez, no fim. É por eles que um aplicativo que abre a verificação num webview sabe a hora de fechar a tela, sem esperar o seu servidor avisar.

O mesmo aviso chega por **três canais**, e você escolhe o que couber: um evento no `window` da página (`window.addEventListener`), o `onEvent` que você passa no `mount()`, e `window.ReactNativeWebView.postMessage` quando a página está dentro de um webview React Native. O payload é o mesmo nos três: `{ event, state, outcome }`. O `outcome` vem preenchido só no `unifokal:done`, e é `null` em qualquer aviso intermediário.

```
// A sua página, ouvindo o fim da verificação. Nada além disto é necessário.
window.addEventListener("unifokal:done", (ev) => {
  const { outcome } = (ev as CustomEvent<{ outcome: string }>).detail;
  // outcome: "approved" | "pending" | "declined" | "failed" | "expired" | "analysis_incomplete"
  fecharTelaDaVerificacao(outcome);
});

// Quem monta por JS recebe o mesmo payload pelo callback, sem ouvir o window:
IdSaas.mount({ sessionId: "vs_01J8ZQ4S8M2N3P4Q5R6S7T8U9V", onEvent: (e) => console.log(e) });
```

**Duas garantias que você pode assumir.** O aviso intermediário não se repete: enquanto o widget espera, ele reconsulta o estado a cada poucos segundos, e só uma transição de verdade vira evento. E o final é **único**: depois do `unifokal:done` nenhum outro evento sai daquele widget, então você pode fechar a tela sem se proteger de um segundo disparo. Um erro dentro do seu listener nunca derruba a verificação do titular.

**O que o evento nunca carrega**, e isso é desenho, não omissão: nenhuma credencial (o identificador da sessão é a credencial do widget, e ele não viaja por aqui), nenhum dado do titular, nenhum resultado de módulo e nenhuma mensagem de erro. Quem escuta pode estar fora do seu controle, porque é a página que hospeda o widget, e o que sai daqui é sinal de interface para fechar tela.

**E a verdade do desfecho continua sendo o seu webhook.** Este evento vive no navegador, e quem controla a página pode forjá-lo. Use-o para a experiência, nunca para liberar, creditar ou dar por concluído qualquer coisa do seu lado: isso se faz com a entrega assinada que chega ao seu servidor.

OPÇÕES DO mount()

| campo | tipo | descrição |
| --- | --- | --- |
| sessionId | obrigatório | ID da sessão criada no backend (vs\_…). É a credencial do widget: nenhum segredo na página. |
| container | opcional | Seletor CSS onde o widget monta (default: #idsaas-widget). |
| baseUrl | opcional | Origem da API UNIFOKAL (default: a origem de onde o widget é servido). |
| locale | opcional | Dica de idioma da interface (por exemplo "en"). É só uma dica: o idioma da sessão, resolvido no servidor, vence, e um valor desconhecido é ignorado. |
| theme | opcional | Tema do widget: "light", "dark" ou "auto" (auto segue o modo claro/escuro do aparelho do titular, ao vivo). Sem o campo, vale a customização da sua conta; sem escolha nenhuma, o widget é claro como sempre. Valor desconhecido é ignorado. |
| accent | opcional | Cor de acento em hex (por exemplo "#7c5cff"): barra, botões e destaques daquela página. A tinta do texto sobre o acento (clara ou escura) é escolhida automaticamente conforme a cor. Valor fora do formato hex é ignorado e o widget fica na cor da conta. |

Todo campo da tabela também existe na **forma declarativa**, como atributo `data-` do próprio `<script>`: `data-session`, `data-container`, `data-base-url`, `data-locale`, `data-theme`, `data-accent`. Com `data-session` presente o widget monta sozinho, sem você escrever uma linha de JavaScript. É a forma do exemplo acima.

**Como o idioma é escolhido**, do mais forte para o mais fraco: o idioma da **sua conta**, se você escolheu um em Customização; depois a dica desta página (`locale` ou `data-locale`); depois o idioma do navegador de quem acessa; e por último o português. Hoje o widget fala português, inglês e espanhol.

Escolha o idioma da conta quando o seu público fala uma língua só: ele vale para toda sessão criada a partir daquele momento, e fica **congelado** em cada sessão. Trocar a configuração no meio de uma verificação não muda a língua de quem já está com o widget aberto, de propósito. Deixando em detectar, cada pessoa vê o widget no idioma do próprio aparelho, que costuma ser a escolha certa para público misto.

TEMA (CLARO, ESCURO, AUTO) E COR DE ACENTO

O widget nasce **claro** e continua claro para quem não configura nada. Se a sua página tem modo escuro, peça `dark` (fixo) ou `auto`: no auto o widget segue o modo do aparelho do titular e acompanha a troca ao vivo. O `accent` muda a cor de destaque (barra, botões) só naquela página, sem mexer na customização da conta. Os dois são locais: viram classe e variável CSS no próprio widget e **nunca viajam para a API**.

```
// TypeScript, na sua página (a forma declarativa usa data-theme e data-accent):
IdSaas.mount({
  sessionId: "vs_01J8ZQ4S8M2N3P4Q5R6S7T8U9V",
  theme: "auto",      // "light" | "dark" | "auto"
  accent: "#7c5cff",  // hex; a tinta sobre o acento (clara ou escura) e escolhida automaticamente
});
```

A precedência é simples: o que a **página** pede vence a customização da **conta**; contas que já usam fundo escuro na tela de customização ganham o widget escuro sem mudar nada. Os dois temas mantêm contraste AA nos textos e ícones da paleta padrão. Valor fora da lista (`theme`) ou fora do formato hex (`accent`) é ignorado em silêncio: o widget cai no visual padrão, nunca quebra.
