---
url: https://developers.zarv.com/sdk/smartflow.md
description: >-
  Embuta o funnel de cadastro e KYC da Zarv com Zarv('smartflow', config):
  telas, prefill, callbacks, identify e complete.
---

# Smartflow

O Smartflow é o funnel de cadastro/KYC embutido do SDK Zarv. Sua configuração é **imperativa**: você chama `Zarv('smartflow', config)`, que monta o funnel como um iframe na sua página.

> 🎮 **[Abrir o playground →](/sdk/playground)** — um embed real, com a sua chave de campanha: teste tela isolada, prefill, modal, tema, `identify` e `complete`, e veja cada callback disparar.

## Configuração básica

```html
<div id="zarv-smartflow"></div>
<script async src="https://js.zarv.com/sdk.js"></script>
<script>
  Zarv("smartflow", {
    publicKey: "pk-XXXXXXXX", // a chave pública do flow (admin Zarv)
    mount: "#zarv-smartflow", // embed inline; ou mode: 'modal'
    onComplete: ({ leadId }) => console.log("finalizou", leadId),
    onDecision: ({ leadId, status }) => console.log("decisão", status), // approved | rejected | manual
  });
</script>
```

O campo `publicKey` é obrigatório: é a chave pública do flow, no formato `pk-<id>`, obtida no admin Zarv (aba **Avançado** da campanha). Ela pode ser **regenerada** no admin — ao regenerar, a chave antiga para de funcionar imediatamente e qualquer embed que ainda a use deixa de carregar até você atualizar o snippet.

## Opções de configuração

| Campo             | Tipo                                                 | Descrição                                                                                                                                                                                                                                                                                             |
| ----------------- | ---------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `publicKey`       | `string`                                             | **Obrigatório.** Chave pública do flow (`pk-<id>`) do admin.                                                                                                                                                                                                                                          |
| `mount`           | `string`                                             | Seletor CSS do elemento onde o funnel é embutido inline.                                                                                                                                                                                                                                              |
| `mode`            | `'inline' \| 'modal'`                                | Forma de exibição: embutido na página ou em modal centralizada.                                                                                                                                                                                                                                       |
| `height`          | `number`                                             | Altura, em px, do embed inline. Por padrão há **auto-resize** ao conteúdo.                                                                                                                                                                                                                            |
| `modal`           | `{ width?, height?, margin? }`                       | Ajustes da modal. A modal é sempre centralizada; por padrão ajusta à altura do conteúdo (auto-resize). `height` fixa a altura.                                                                                                                                                                        |
| `screen`          | `'face' \| 'cnh' \| 'address' \| 'email' \| 'phone'` | Embute UMA tela isolada em vez do fluxo inteiro. Omita para o fluxo completo.                                                                                                                                                                                                                         |
| `leadId`          | `string`                                             | Usa um lead existente. Omita para iniciar uma auto-sessão (um lead é criado automaticamente).                                                                                                                                                                                                         |
| `prefill`         | `{ name?, nationalId?, birthDate?, phone?, email? }` | Persiste dados no lead, preenchendo a próxima tela. `phone` e `email` a partir do SDK **0.40.0**.                                                                                                                                                                                                     |
| `theme`           | `'light' \| 'dark' \| 'auto'`                        | Tema da interface.                                                                                                                                                                                                                                                                                    |
| `closeOnComplete` | `boolean`                                            | Fecha a modal ao finalizar. Default: `true` em `mode: 'modal'`, `false` inline. Com `onDecision` registrado, o fechamento automático espera a **decisão** (fechar antes derrubaria o iframe que a acompanha).                                                                                         |
| `onStep`          | `function`                                           | Callback `onStep(step, { progress, leadId })` a cada passo.                                                                                                                                                                                                                                           |
| `onProgress`      | `function`                                           | Callback `onProgress(progress)` com o progresso do funnel.                                                                                                                                                                                                                                            |
| `onComplete`      | `function`                                           | Callback `onComplete({ leadId, status? })` ao concluir o fluxo. Dispara **uma vez**; `status` vem preenchido quando a decisão já é conhecida nesse momento.                                                                                                                                           |
| `onDecision`      | `function`                                           | Callback `onDecision({ leadId, status })` quando a **decisão da verificação** chega: `approved`, `rejected` ou `manual`. Veja [Fim do fluxo](#fim-do-fluxo-oncomplete-e-ondecision).                                                                                                                  |
| `onClose`         | `function`                                           | Callback `onClose()` quando a modal é dispensada pelo usuário.                                                                                                                                                                                                                                        |
| `onSession`       | `function`                                           | Callback `onSession({ leadId, externalId? })` assim que a sessão é criada, **antes de o iframe renderizar**. É como você obtém o `leadId` para encadear várias telas. O `externalId` vem preenchido quando o lead retomado já tinha uma referência sua. SDK **0.40.0**+ (`externalId` em **0.41.0**). |
| `onError`         | `function`                                           | Callback `onError({ error, status })` quando o embed **não conseguiu montar** (falha HTTP no bootstrap, erro de rede, ou seletor de `mount` inexistente). SDK **0.40.0**+.                                                                                                                            |

## Posicionamento

Você pode embutir o funnel inline na página ou exibi-lo em uma modal.

**Inline** — informe `mount` com o seletor do elemento alvo (ou use `mode: 'inline'`). Por padrão o iframe faz auto-resize, acompanhando a altura do conteúdo; use `height` para fixar uma altura em px:

```js
Zarv("smartflow", {
  publicKey: "pk-XXXXXXXX",
  mount: "#zarv-smartflow",
  // height: 640, // opcional; sem isso, auto-resize ao conteúdo
});
```

**Modal** — use `mode: 'modal'`. A modal é sempre centralizada e, por padrão, ajusta à altura do conteúdo da tela (auto-resize). Use `modal` para ajustar largura, altura e margem:

```js
Zarv("smartflow", {
  publicKey: "pk-XXXXXXXX",
  mode: "modal",
  modal: { width: 480, margin: 24 }, // height fixa a altura, se informado
  onClose: () => console.log("modal fechada"),
});
```

**`modalSteps`** — mesmo embutido inline, você pode promover telas específicas (ex.: liveness / captura de documento) para uma modal overlay, listando os nomes das etapas:

```js
Zarv("smartflow", {
  publicKey: "pk-XXXXXXXX",
  mount: "#zarv-smartflow",
  modalSteps: ["face", "cnh"], // essas etapas abrem em modal; as demais ficam inline
});
```

## Aparência

O funnel aplica a identidade visual da campanha (logo, cores, alinhamento,
fundo) na própria página — você não configura nada disso pelo SDK.

O que o SDK faz, a partir do SDK **0.41.0**, é usar duas dessas configurações
no *enquadramento* que ele desenha em volta do iframe:

* **`bgColor`** pinta o card da modal e o overlay promovido. Sem isso, um
  funnel com tema escuro aparecia dentro de um card branco — branco nos cantos
  arredondados e um flash branco antes do primeiro paint.
* **`buttonShape`** define o canto do botão de fechar, que é um botão como os
  outros da campanha.

Nada a fazer do seu lado: os valores vêm da campanha e são aplicados no mount.

## Tela única (componentes isolados)

Use `screen` para embutir **uma única tela** em vez do fluxo inteiro:

```js
Zarv("smartflow", {
  publicKey: "pk-XXXXXXXX",
  mount: "#zarv-smartflow",
  screen: "face",
  onComplete: ({ leadId }) => console.log("face concluída", leadId),
});
```

Valores possíveis: `'face'`, `'cnh'`, `'address'`, `'email'`, `'phone'`. Omita `screen` para rodar o fluxo inteiro.

As telas são chamadas de forma **independente** — elas não seguem a ordem do fluxo. Ao concluir uma tela, o SDK dispara `onComplete` para que você trate o próximo passo do seu lado. A ordem do funnel é respeitada apenas pela versão hosted, não pelo SDK.

## Várias telas no seu formulário

O caso mais comum de tela única é rodar **várias** delas como etapas do seu próprio cadastro — por exemplo, validar telefone, e-mail, facial e CNH dentro do formulário que você já tem.

Todas as telas precisam cair no **mesmo lead**. O encadeamento funciona assim:

1. A **primeira** chamada vai sem `leadId`. O backend cria o lead e devolve o id em `onSession({ leadId })`.
2. Você guarda esse `leadId` e o passa em **todas** as chamadas seguintes.
3. No mesmo `onSession`, um `identify` amarra o seu identificador (`externalId`) ao lead — é por ele que o seu backend vai reconhecer o resultado depois.

```js
const SEQUENCIA = ["phone", "email", "face", "cnh"];
let leadId = null;

function etapa(i) {
  if (i >= SEQUENCIA.length) return finalizar();
  const screen = SEQUENCIA[i];

  const config = {
    publicKey: "pk-XXXXXXXX",
    screen,
    mount: "#slot-" + screen,
    onSession: ({ leadId: id }) => {
      if (leadId) return;
      leadId = id;
      // Amarra a sua referência (pedido, reserva, matrícula…) ao lead.
      Zarv("smartflow", {
        action: "identify",
        leadId,
        externalId: "RES-00042",
        name: cadastro.nome,
        nationalId: cadastro.cpf,
        birthDate: cadastro.nascimento,
      });
    },
    onComplete: () => etapa(i + 1),
    onError: (err) => mostrarFallbackProprio(err),
  };

  if (leadId) config.leadId = leadId;
  else
    config.prefill = {
      name: cadastro.nome,
      nationalId: cadastro.cpf,
      birthDate: cadastro.nascimento,
      phone: cadastro.telefone,
      email: cadastro.email,
    };

  Zarv("smartflow", config);
}

function finalizar() {
  Zarv("smartflow", {
    action: "complete",
    leadId,
    metadata: { reserva: "RES-00042" },
    onComplete: () => aguardarDecisaoNoSeuBackend(),
    onError: (err) => tratarErro(err),
  });
}

etapa(0);
```

Pontos que costumam morder:

* **Passe `leadId` explicitamente** em `identify` e `complete` quando houver mais de uma tela na página. Sem ele, essas chamadas miram a **última** sessão montada — o que raramente é a que você quer. (`leadId` nessas chamadas requer o SDK **0.40.0**+.)
* **A ordem é sua.** As telas isoladas não seguem a ordem do funnel: cada `onComplete` devolve o controle para você decidir a próxima.
* **`onError` não é opcional aqui.** Um embed que falha no meio do seu formulário deixa um espaço em branco; sem `onError` a sua página não tem como perceber e cair num caminho alternativo.
* **A decisão pode não chegar no browser.** Ela vem do polling de dentro do iframe; se o visitante fecha a página ou você desmonta o último embed antes de a verificação terminar, o `onDecision` simplesmente não dispara. Trate o [webhook da campanha](/sdk/webhooks) como o canal real — inclusive para confirmar cada etapa, via os eventos `lead.phone.completed`, `lead.email.completed`, `lead.face.completed` e `lead.cnh.completed`.

### E a tela de dados básicos?

Ela **não aparece** quando você monta telas isoladas. O campo `screen` aceita apenas `'phone'`, `'email'`, `'face'`, `'cnh'` e `'address'` — não existe uma tela de nome/CPF/nascimento para embutir. Essa tela só existe no fluxo completo (sem `screen`).

Ou seja: se o seu cadastro já coletou esses dados, o visitante nunca os digita de novo. Mande-os na primeira chamada, por `prefill`, ou depois por `identify`:

```js
// no mount da primeira tela
prefill: { name: "Ana", nationalId: "...", birthDate: "1990-01-02" }

// ou, em runtime, sobre um lead que já existe
Zarv("smartflow", {
  action: "identify",
  leadId,
  name: "Ana",
  nationalId: "...",
  birthDate: "1990-01-02",
});
```

O `complete` é o que cobra esses dados: ele responde **422 `basic_data_incomplete`** enquanto o lead não tiver nome, CPF e nascimento. Se você receber esse erro mesmo tendo mandado `prefill`, é sinal de que algum dos três não chegou.

## Fim do fluxo: onComplete e onDecision

Dois momentos distintos encerram um cadastro embutido:

1. **`onComplete({ leadId, status? })`** — o visitante chegou à tela final do funnel. A verificação (face, CNH, risco) pode ainda estar rodando; nesse caso `status` vem ausente. Dispara **uma única vez**.
2. **`onDecision({ leadId, status })`** — a decisão da verificação foi tomada: `approved`, `rejected` ou `manual` (revisão humana). Se a decisão já era conhecida quando o visitante chegou à tela final, dispara imediatamente após o `onComplete`; caso contrário, dispara assim que o backend decide (o funnel acompanha por polling e avisa o SDK).

Quem decide o que acontece no fim é **você**: redirecionar a própria página, fechar a modal, mostrar UI própria. Sem handler, o funnel mostra a tela de resultado dentro do iframe (ou redireciona para as URLs de resultado configuradas na campanha, quando existirem).

```js
Zarv("smartflow", {
  publicKey: "pk-XXXXXXXX",
  mode: "modal",
  onDecision: ({ leadId, status }) => {
    if (status === "approved") location.href = "/bem-vindo";
    else if (status === "manual") mostrarBannerEmAnalise();
    else location.href = "/nao-aprovado";
  },
});
```

Notas:

* Em `mode: 'modal'` com `onDecision` registrado, o `closeOnComplete` espera a **decisão** para fechar — fechar no `onComplete` derrubaria o iframe cujo polling é a fonte da decisão.
* O `onDecision` é o sinal de UX **no browser**; o canal autoritativo server-side continua sendo o [**webhook da campanha**](/sdk/webhooks) (use-o para efeitos de negócio, não o callback).
* Com o **autopilot desligado** na campanha, todo cadastro finaliza como `manual` — a aprovação/reprovação acontece depois, pela análise humana, e chega [num segundo evento de webhook](/sdk/webhooks#a-decisao-pode-chegar-em-duas-fases). No browser, o `onDecision` desses cadastros reporta `manual`: é o estado final que o visitante vê, não o desfecho.

## Contexto do lead

Para continuar um cadastro já iniciado, passe `leadId` com um lead existente. Se você **omitir** `leadId`, o SDK inicia uma auto-sessão e cria um lead automaticamente.

O `leadId` criado chega no callback [`onSession`](#varias-telas-no-seu-formulario), disparado assim que a sessão existe — antes de o iframe renderizar. É esse o momento de guardá-lo: os demais callbacks (`onStep`, `onComplete`) só chegam depois que o visitante interage, o que é tarde demais se você precisa montar a próxima tela no mesmo lead.

## Prefill de identidade

Use `prefill` para persistir dados no lead, preenchendo a próxima tela:

```js
Zarv("smartflow", {
  publicKey: "pk-XXXXXXXX",
  mount: "#zarv-smartflow",
  prefill: {
    name: "Ana",
    nationalId: "...",
    birthDate: "1990-01-02",
    phone: "+5511999998888", // SDK 0.40.0+
    email: "ana@example.com", // SDK 0.40.0+
  },
});
```

`phone` e `email` importam sobretudo nas telas isoladas `'phone'` e `'email'`: sem eles o visitante redigita um dado que você já tem, e pode digitar **um valor diferente** do que está no seu cadastro — quebrando a correspondência entre o que você guardou e o que foi verificado.

Prefill **não pula a verificação**: ele preenche o campo, então a tela de digitar é ignorada e o visitante cai direto na confirmação, que continua exigindo o código. Um telefone ou e-mail malformado é recusado na hora, com `invalid_prefill_phone` / `invalid_prefill_email`.

O prefill só faz sentido na **primeira** chamada de uma sessão. Depois que o lead existe, os dados já estão persistidos, e o backend nunca sobrescreve um campo já preenchido.

## Identify em runtime

Depois de iniciar uma sessão (mesmo sem `leadId`), você pode identificar o usuário e anexar o seu próprio `externalId`:

```js
Zarv("smartflow", {
  action: "identify",
  leadId, // opcional; obrigatório na prática se houver mais de uma tela na página
  externalId: "id-do-parceiro-123",
  name: "Ana",
  nationalId: "...",
  birthDate: "1990-01-02",
});
```

Essa chamada atualiza o lead: ela preenche apenas os campos que ainda estão vazios, enquanto o `externalId` é sempre setado. Assim, a próxima tela já vem preenchida com os dados informados.

Sem `leadId`, a chamada mira a **sessão ativa** — a última tela montada. Com uma única tela na página isso é o esperado; com várias, informe o `leadId` (obtido em `onSession`) para não acertar o lead errado. O campo `leadId` requer o SDK **0.40.0**+.

## Complete em runtime

Quando o seu fluxo termina, chame `complete` para **finalizar o cadastro**: ele anexa metadados que você coletou no seu lado e **dispara a verificação** do usuário na sessão ativa.

```js
Zarv("smartflow", {
  action: "complete",
  leadId, // opcional; veja a nota sobre sessão ativa em Identify em runtime
  metadata: { pedido: "ABC-123", plano: "pro" }, // anexado em metadata.custom no lead
  onComplete: ({ leadId, status }) => {
    // verificação disparada (a decisão chega depois via webhook da campanha)
  },
  onError: (err) => {
    if (err.error === "basic_data_incomplete") {
      // o usuário ainda não informou os dados básicos (nome / CPF / nascimento)
    }
  },
});
```

| Campo        | Tipo                           | Descrição                                                                                             |
| ------------ | ------------------------------ | ----------------------------------------------------------------------------------------------------- |
| `action`     | `'complete'`                   | Obrigatório.                                                                                          |
| `leadId`     | `string`                       | Lead alvo. Omitido, usa a sessão ativa (a última tela montada). SDK **0.40.0**+.                      |
| `metadata`   | `object`                       | Metadados adicionais do seu fluxo; persistidos sob `metadata.custom` no lead (vão também no webhook). |
| `onComplete` | `({ leadId, status }) => void` | Disparado quando a verificação é acionada com sucesso.                                                |
| `onError`    | `({ error, status }) => void`  | Disparado em erro.                                                                                    |

Comportamento:

* Se o usuário **não finalizou os dados básicos** (`IsBasicDataCompleted` é falso), o `complete` retorna erro `basic_data_incomplete` (HTTP 422) no `onError` — nada é publicado.
* A verificação é disparada conforme o estado do lead (ex.: após o face capture, a re-validação de liveness/face no ZarvID). É **idempotente**: um lead que já está em verificação/decidido não é re-enfileirado.
* A decisão final (aprovado / em revisão / reprovado) **não** volta por este `onComplete` — ela chega de forma assíncrona pelo **webhook da campanha** e, no browser, pelo callback [`onDecision`](#fim-do-fluxo-oncomplete-e-ondecision) do mount.

## Destruir o embed

Montar de novo no **mesmo container** já substitui o embed que estava lá — você não precisa limpar nada antes de remontar. O `destroy` é para o outro caso: quando a página **para de exibir** o embed e não vai remontar (uma SPA desmontando a rota que o hospeda, um modal que você fecha por conta própria, um passo do seu wizard que sai de cena).

```js
Zarv("smartflow", { action: "destroy" });
```

Ele remove o iframe (e a moldura do modal, se houver) **e desregistra o listener de `message`**. Essa segunda parte é o motivo de existir: tirar o iframe do DOM na mão deixa o listener registrado, e ele continua disparando os callbacks daquele mount para eventos de qualquer embed montado depois — o sintoma típico é `onStep` chegando duplicado.

| Campo    | Tipo        | Descrição                                                                                                    |
| -------- | ----------- | ------------------------------------------------------------------------------------------------------------ |
| `action` | `'destroy'` | Obrigatório.                                                                                                 |
| `mount`  | `string`    | O mesmo seletor com que o embed foi montado; derruba só aquele. Omitido, derruba **todos** os embeds ativos. |

```js
// só o embed daquele slot — os outros continuam de pé
Zarv("smartflow", { action: "destroy", mount: "#zarv-cnh" });
```

Comportamento:

* **Sem `mount`** derruba todos os embeds ativos. É o que uma página se desmontando quer, e é a única forma de fechar um **modal** — o modal não tem seletor próprio para nomear.
* Destruir algo que já não existe (seletor inexistente, `destroy` repetido, nada montado) é **no-op**, nunca exceção: isso roda enquanto a página está saindo e não pode ser o que quebra a desmontagem.
* **Não dispara `onClose`.** O `onClose` reporta um fechamento que o **visitante** fez (botão × ou clique no backdrop do modal); o `destroy` é a desmontagem silenciosa da página.
* A sessão ativa é liberada junto. Se você ainda vai chamar `identify`/`complete` naquele lead depois de destruir o embed, guarde o `leadId` do `onSession` e passe explicitamente.

Requer o SDK **0.42.0**+.

## Identificação do SDK

O SDK se identifica ao funnel com o parâmetro `sdk=<versão>` na URL do iframe (ex.: `sdk=0.38.0`). O funnel propaga esse parâmetro por toda a sessão e:

* registra `sdk_version` nos **metadados do lead** (junto de `utm_*`/`referrer`), permitindo segmentar cadastros por superfície de aquisição (SDK embed vs. funnel hosted) e por versão do SDK;
* marca os eventos de analytics do funnel com `sdk_version`;
* ativa o modo embed automaticamente (não é preciso nenhum parâmetro adicional).

Nada a configurar do seu lado — o parâmetro é adicionado e transportado automaticamente.

## Origens de embed (frame-ancestors)

Por padrão, o funnel permite ser embutido em `js.zarv.com` (a origem oficial do SDK) e nos domínios ativos do próprio flow.

Se um parceiro embutir o funnel em um host **diferente** do domínio do flow, é preciso adicionar essa origem em **"AllowedEmbedOrigins"** do flow (no admin). Sem isso, o navegador bloqueia o iframe via `frame-ancestors`.
