> ## Documentation Index
> Fetch the complete documentation index at: https://api-docs.omni-ward.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Iframe e WebView

> Exiba a verificação dentro do seu site ou app, sem SDK, e acompanhe o andamento pelos eventos da página.

A `url` devolvida por [`POST /v1/sessions`](https://api-docs.omni-ward.com/api-reference/criar-sessao) abre a verificação completa: formulários, documento, selfie e prova de vida. Você pode exibi-la num iframe, numa WebView, num popup ou por redirecionamento. Nenhum desses caminhos exige SDK.

> [!WARNING]
>
> Os eventos da página servem para atualizar a sua tela, como fechar o iframe ou mostrar "aguarde". Eles não provam o resultado. Para decidir, use o [webhook](https://api-docs.omni-ward.com/webhooks.md)`verification.completed` ou consulte [`GET /v1/sessions/{id}`](https://api-docs.omni-ward.com/api-reference/consultar-sessao) no seu back-end.

## Iframe

### 1. Crie a sessão com o seu site em `allowedOrigins`

```bash theme={null}
curl --request POST 'https://api.omni-ward.com/v1/sessions' \
  --header "Authorization: Bearer $OMNI_WARD_API_KEY" \
  --header 'Content-Type: application/json' \
  --data '{
    "workflowId": "9a2c…",
    "allowedOrigins": ["https://app.exemplo.com"],
    "redirectUrl": "https://app.exemplo.com/verificacao/fim"
  }'
```

`allowedOrigins` define quais sites podem exibir a verificação em iframe. Use só esquema, domínio e porta, sem caminho (`https://app.exemplo.com`). Se a lista ficar vazia, qualquer site que tenha o link pode exibir. Recomendamos preencher.

### 2. Coloque o iframe na página

```html theme={null}
<iframe
  id="omni-ward"
  src="https://verify.omni-ward.com/v/…"
  allow="camera; microphone; fullscreen"
  title="Verificação de identidade"
  style="border: 0; width: 100%; max-width: 480px; height: 760px"
></iframe>
```

| Requisito | Por quê |
| - | - |
| `allow="camera; microphone"` | Sem ele, o navegador bloqueia a câmera dentro do iframe, e a etapa de documento, selfie ou prova de vida não abre. |
| Seu site em HTTPS | O navegador só libera a câmera em páginas seguras. Em desenvolvimento, `http://localhost` funciona. |
| Altura de pelo menos 640 px | A página é feita para celular. Num espaço menor, os botões ficam fora da tela. |
| Sem `sandbox`, ou com `allow-scripts allow-same-origin allow-forms allow-top-navigation-by-user-activation` | A página precisa de JavaScript e de armazenamento local para continuar de onde a pessoa parou. A última permissão é para o botão de voltar ao seu site. |

Não reaproveite o mesmo iframe para outra sessão. Para uma nova verificação, crie outra sessão e troque o `src`.

### 3. Escute os eventos

A página manda mensagens para a janela que contém o iframe com `window.parent.postMessage`. Antes de usar uma mensagem, confira a origem dela e o campo `source`:

```javascript theme={null}
const verifyOrigin = new URL(sessionUrl).origin; // https://verify.omni-ward.com
const frame = document.getElementById('omni-ward');
let finished = false;

window.addEventListener('message', (e) => {
  if (e.origin !== verifyOrigin || e.source !== frame.contentWindow) return;
  if (e.data?.source !== 'omni-ward') return;

  switch (e.data.type) {
    case 'verification.started':
      mostrarProgresso();
      break;
    case 'verification.step_changed':
      atualizarProgresso(e.data.step); // ex.: "selfie_capture"
      break;
    case 'verification.completed':
    case 'verification.failed':
    case 'verification.cancelled':
    case 'verification.expired':
      if (finished) return; // pode chegar mais de uma vez
      finished = true;
      frame.remove();
      buscarResultadoNoBackend(); // decida pelo back-end, não pelo evento
      break;
  }
});
```

### 4. Fim do fluxo

Ao terminar, a página mostra um botão "Voltar para" seguido do nome da sua empresa se a sessão tiver `redirectUrl`. Dentro do iframe, esse botão abre o endereço na janela principal, não dentro do iframe. Se preferir controlar a saída você mesmo, não envie `redirectUrl` e use o evento terminal para fechar o iframe.

## Eventos da página

Toda mensagem tem este formato:

```json theme={null}
{ "source": "omni-ward", "type": "verification.step_changed", "step": "selfie_capture" }
```

| `type` | Quando chega | Campos extras |
| - | - | - |
| `verification.started` | A pessoa abriu o link e a sessão começou neste aparelho. Não chega de novo se ela recarregar a página. | — |
| `verification.step_changed` | A pessoa concluiu uma etapa e a página passou para a próxima. | `step`: a etapa que aparece agora (tabela abaixo). Vem vazio quando não há mais etapas e a sessão vai para processamento. |
| `verification.completed` | A sessão foi processada até o fim. | — |
| `verification.failed` | O processamento não terminou por uma falha. | — |
| `verification.cancelled` | A sessão foi cancelada pela sua empresa. | — |
| `verification.expired` | A sessão passou do prazo (`ttlMinutes`). | — |

Os quatro últimos são **terminais**: depois deles, a sessão não muda mais. Eles podem chegar mais de uma vez, porque a página reenvia o evento a cada atualização de estado. Trate só o primeiro.

`verification.completed` quer dizer que a verificação terminou, não que a pessoa foi aprovada. A decisão fica em `outcome` e `reviewState`, na consulta da sessão.

As mensagens não trazem o `id` da sessão nem dados da pessoa. Associe o evento à sessão pelo iframe ou pela tela que você abriu.

### Etapas (`step`)

| Valor | Tela |
| - | - |
| `privacy_consent` | Aviso de privacidade |
| `personal_info` | Dados da pessoa |
| `business_info` | Dados da empresa |
| `document_capture` | Documento com foto |
| `proof_of_address_capture` | Comprovante de endereço |
| `cnpj_card_capture` | Cartão CNPJ |
| `social_contract_capture` | Contrato social |
| `selfie_capture` | Selfie |
| `liveness` | Prova de vida |

Workflows novos podem trazer etapas que não estão nesta lista. Se `step` vier com um valor que você não conhece, ignore e continue.

## WebView

A WebView carrega a `url` como página principal, então `allowedOrigins` não se aplica. Você precisa liberar a câmera no app e, para receber os eventos, expor um canal que a página procura.

A página manda cada evento como texto JSON, no mesmo formato acima, por estes canais (os que existirem):

| Plataforma | Canal que a página chama |
| - | - |
| iOS (WKWebView) | `window.webkit.messageHandlers.omniWard.postMessage(json)` |
| Android (WebView) | `window.OmniWardAndroid.postMessage(json)` |
| React Native (`react-native-webview`) | `window.ReactNativeWebView.postMessage(json)` |

### iOS (WKWebView)

No `Info.plist`, adicione `NSCameraUsageDescription` (e `NSMicrophoneUsageDescription` se o workflow usar áudio).

```swift theme={null}
import WebKit

final class VerificationViewController: UIViewController, WKScriptMessageHandler, WKUIDelegate, WKNavigationDelegate {
  private let url: URL           // session.url, vinda do seu back-end
  private var webView: WKWebView!
  private var finished = false

  init(url: URL) { self.url = url; super.init(nibName: nil, bundle: nil) }
  required init?(coder: NSCoder) { fatalError() }

  override func viewDidLoad() {
    super.viewDidLoad()
    let config = WKWebViewConfiguration()
    config.allowsInlineMediaPlayback = true                 // câmera dentro da página, não em tela cheia
    config.mediaTypesRequiringUserActionForPlayback = []
    config.userContentController.add(self, name: "omniWard") // canal dos eventos
    webView = WKWebView(frame: view.bounds, configuration: config)
    webView.uiDelegate = self
    webView.navigationDelegate = self
    view.addSubview(webView)
    webView.load(URLRequest(url: url))
  }

  // Libera a câmera só para a página da verificação (iOS 15+).
  func webView(_ webView: WKWebView, requestMediaCapturePermissionFor origin: WKSecurityOrigin,
               initiatedByFrame frame: WKFrameInfo, type: WKMediaCaptureType,
               decisionHandler: @escaping (WKPermissionDecision) -> Void) {
    decisionHandler(origin.host == url.host ? .grant : .deny)
  }

  func userContentController(_ controller: WKUserContentController, didReceive message: WKScriptMessage) {
    guard message.frameInfo.securityOrigin.host == url.host,
          let json = (message.body as? String)?.data(using: .utf8),
          let event = try? JSONSerialization.jsonObject(with: json) as? [String: Any],
          event["source"] as? String == "omni-ward",
          let type = event["type"] as? String else { return }
    let terminal = ["verification.completed", "verification.failed", "verification.cancelled", "verification.expired"]
    if terminal.contains(type) && !finished {
      finished = true
      dismiss(animated: true) // e busque o resultado no seu back-end
    }
  }
}
```

### Android (WebView)

No `AndroidManifest.xml`, declare `android.permission.CAMERA` e peça a permissão em tempo de execução **antes** de abrir a tela.

```kotlin theme={null}
class VerificationActivity : AppCompatActivity() {
  private val terminal = setOf("verification.completed", "verification.failed", "verification.cancelled", "verification.expired")
  private var finished = false

  override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    val url = intent.getStringExtra("url")!!   // session.url, vinda do seu back-end
    val verifyHost = Uri.parse(url).host
    val webView = WebView(this)
    setContentView(webView)

    webView.settings.apply {
      javaScriptEnabled = true
      domStorageEnabled = true                 // a página guarda o progresso no armazenamento local
      mediaPlaybackRequiresUserGesture = false
    }
    // Libera a câmera só para a página da verificação.
    webView.webChromeClient = object : WebChromeClient() {
      override fun onPermissionRequest(request: PermissionRequest) {
        if (request.origin.host == verifyHost) request.grant(request.resources) else request.deny()
      }
    }
    // Canal dos eventos. Confira o host: a interface fica visível para qualquer página carregada.
    webView.addJavascriptInterface(object {
      @JavascriptInterface
      fun postMessage(json: String) = runOnUiThread {
        if (Uri.parse(webView.url ?: "").host != verifyHost) return@runOnUiThread
        val event = JSONObject(json)
        if (event.optString("source") != "omni-ward") return@runOnUiThread
        if (event.optString("type") in terminal && !finished) {
          finished = true
          finish() // e busque o resultado no seu back-end
        }
      }
    }, "OmniWardAndroid")
    webView.loadUrl(url)
  }
}
```

### React Native

Use `react-native-webview`, ou o componente pronto em [React Native](https://api-docs.omni-ward.com/sdk/react-native.md), que já faz isto:

```jsx theme={null}
<WebView
  source={{ uri: sessionUrl }}
  javaScriptEnabled
  domStorageEnabled
  allowsInlineMediaPlayback
  mediaPlaybackRequiresUserAction={false}
  mediaCapturePermissionGrantType="grantIfSameHostElsePrompt"
  onMessage={(e) => {
    if (new URL(e.nativeEvent.url).origin !== new URL(sessionUrl).origin) return;
    const event = JSON.parse(e.nativeEvent.data);
    if (event.source !== 'omni-ward') return;
    // mesmos tipos de evento da tabela acima
  }}
/>
```

### Voltar para o app ao terminar

Em WebView, você pode usar um esquema do seu app como `redirectUrl`, por exemplo `meuapp://verificacao/fim`. Intercepte essa navegação (`decidePolicyFor` no iOS, `shouldOverrideUrlLoading` no Android, `onShouldStartLoadWithRequest` no React Native) para fechar a tela. Os eventos terminais já bastam para isso, então o `redirectUrl` é opcional.

## Comportamento nos dois casos

* **Continuar depois:** o progresso fica salvo. Se a pessoa fechar e abrir o mesmo link no mesmo aparelho em até 1 hora, ela volta para a etapa onde parou. Depois disso, gere outro link com [`POST /v1/sessions/{id}/link`](https://api-docs.omni-ward.com/api-reference/gerar-novo-link).
* **Trocar de aparelho:** nas etapas de câmera, a página oferece um QR code para continuar no celular. A partir daí, os eventos de etapa (`verification.step_changed`) saem no celular. O seu iframe ou WebView continua acompanhando e recebe o evento terminal enquanto estiver aberto. Se ele for fechado antes, o resultado chega só pelo back-end.
* **Sem câmera ao vivo:** a página oferece enviar uma foto do aparelho. A prova de vida exige câmera ao vivo.

## Problemas comuns

| Sintoma | Causa provável |
| - | - |
| "Este site não está autorizado a exibir esta verificação." | O site que exibe o iframe não está em `allowedOrigins`, ou a origem foi escrita com caminho ou barra no fim. |
| A câmera não abre no iframe | Falta `allow="camera; microphone"` no iframe, o site não está em HTTPS, ou o seu site envia um cabeçalho `Permissions-Policy` que bloqueia a câmera (por exemplo `camera=()`). Nesse caso, libere com `camera=(self "https://verify.omni-ward.com")`. |
| A câmera não abre na WebView | Falta a permissão nativa (`NSCameraUsageDescription` ou `CAMERA`) ou a liberação na WebView (`requestMediaCapturePermissionFor` ou `onPermissionRequest`). |
| A página em branco na WebView | JavaScript ou armazenamento local (`domStorageEnabled`) desligados. |
| Nenhum evento chega | No iframe, confira se o filtro usa a origem de `session.url`. Na WebView, confira o nome do canal: `omniWard` (iOS) ou `OmniWardAndroid` (Android). |


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.