Passo 3 · Ciclo de captura e eventos
Com o componente embutido (Passo 2) e o token da sessão em mãos (Passo 1), você abre
a câmera com camera.open() e reage aos eventos que o componente emite ao longo do
ciclo de captura. Esta página cobre a chamada open(), cada evento e seu payload real,
as permissões que o fluxo pede e os dois comportamentos automáticos que você precisa
conhecer: o loop multi-foto e o redirect ao terminar.
Tier de garantia. Cada foto é assinada server-side com ICP-Brasil A1 (fraud deterrence). O Web SDK não ancora proveniência em hardware do dispositivo — GPS e sensores são telemetria, não prova de origem. Para cadeia de prova com âncora de hardware device-side, use o SDK mobile.
camera.open(options)
Seção intitulada “camera.open(options)”Inicia o ciclo de captura na instância retornada por useProvviCamera(). A chamada dispara,
em sequência e de forma automática: validação da licença → busca da sessão → pedido de
permissões (câmera e sensores) → abertura da câmera com a guia de enquadramento → aquisição
de GPS e bússola em background → espera pelo toque no botão de captura → processamento do
frame → upload para assinatura. Em sessões multi-foto, o ciclo se repete sozinho (ver
Multi-foto).
camera.open({ token, // obrigatório — vindo do SEU backend (Passo 1) customData: { ref: 'anuncio-123' },});| Parâmetro | Tipo | Obrigatório | Descrição |
|---|---|---|---|
token |
string | Sim | Token de sessão criado pelo seu backend. Sem ele, o componente emite error com código 5002 (TOKEN_REQUIRED). |
licenseKey |
string | Não* | License (pvv_*). Opcional aqui se já foi passada em useProvviCamera(). Se faltar nos dois lugares, o componente emite error com código 8001 (LICENSE_KEY_MISSING). |
overlay |
object | Não | Máscara-guia de enquadramento, sobrescreve a de useProvviCamera(). Contrato completo em O componente. |
customData |
any | Não | Dados arbitrários seus. Atenção: não são ecoados de volta nos eventos (ver abaixo). |
open() é no-op quando o componente já está ocupado. Se um ciclo de captura já está em
andamento, uma nova chamada a open() é simplesmente ignorada — não há erro, não há reset.
Aguarde closed (ou uploaded com complete: true) antes de reabrir.
Erros de open() (licença/token) e todos os erros de rede chegam pelo evento error, não
como exceção. A lista de códigos e o mapeamento para ação do usuário está em
Erros e bloqueios.
O ciclo
Seção intitulada “O ciclo”Um ciclo de captura bem-sucedido de uma foto emite, em ordem:
opened → capture → uploading → uploaded → (closed)opened— câmera e sensores ativos; a UI de captura está visível.capture— o usuário tocou no botão; o frame foi capturado e processado (antes do upload).uploading— o upload para assinatura ICP-Brasil começou.uploaded— a assinatura concluiu; o resultado da foto está disponível.closed— o stream foi encerrado e a UI escondida.
Qualquer falha ao longo do caminho emite error em vez de seguir.
Eventos
Seção intitulada “Eventos”O componente emite CustomEvent padrão do DOM. Registre com addEventListener usando as
constantes de EVENTS (recomendado) ou os nomes literais.
| Evento | Constante | Quando | e.detail |
|---|---|---|---|
opened |
EVENTS.OPENED |
Câmera e sensores prontos | {} |
capture |
EVENTS.CAPTURE |
Frame capturado, antes do upload | { capture } |
uploading |
EVENTS.UPLOADING |
Upload iniciado | { capture } |
uploaded |
EVENTS.UPLOADED |
Assinatura concluída | { capture } — com capture.result |
error |
EVENTS.ERROR |
Falha em qualquer etapa | { capture, error } |
closed |
EVENTS.CLOSED |
Câmera encerrada | {} |
O objeto capture
Seção intitulada “O objeto capture”Presente em capture, uploading, uploaded e error:
{ id: 'uuid-v4', // ID da captura nesta sessão status: 'UPLOADED', // um dos STATUSES (abaixo) capturedAt: '2026-07-05T12:05:30Z', // ISO 8601 result: { /* ... */ } // presente apenas em `uploaded` (ver abaixo)}O objeto result (só em uploaded)
Seção intitulada “O objeto result (só em uploaded)”Quando a assinatura conclui, capture.result traz o resultado da foto. Ele só aparece
no evento uploaded, e apenas quando há um registro de captura — sempre teste antes de ler:
{ session_id: 'sess_abc123', authenticated_jpeg_url: 'https://…' // JPEG assinado (presigned) — pode ser null}
authenticated_jpeg_urlpode virnull. Contas configuradas para não armazenar o JPEG (zero-knowledge,store_asset_policy = NEVER) não retêm o asset — onullaqui é esperado, não um erro. O manifesto vai embutido no JPEG autenticado (não há URL de manifesto separada); em modoNEVER, o componente não entrega um artefato inline. Ver Resultados.
customData não é ecoado
Seção intitulada “customData não é ecoado”O customData que você passa em open() não volta em nenhum e.detail. Ele é filtrado
dos eventos. Para correlacionar uma captura ao seu próprio registro, use o session_id do
result (ou o mapeamento token ↔ reference_id que você já tem do Passo 1) — não dependa de
customData retornar.
STATUSES
Seção intitulada “STATUSES”capture.status percorre os valores de STATUSES:
| Status | Significado |
|---|---|
NEW |
Captura criada, ainda não processada |
READY |
Frame capturado e pronto para upload |
UPLOADING |
Upload/assinatura em andamento |
UPLOADED |
Assinatura concluída |
ERROR |
Falha nesta captura |
Permissões e consequências
Seção intitulada “Permissões e consequências”O fluxo pede três coisas ao navegador. Só uma é obrigatória.
| Permissão | Obrigatória? | Se negada / indisponível |
|---|---|---|
| Câmera | Sim | O ciclo falha com error código 3001 (STREAM_OPEN_FAILED). Sem câmera não há captura. |
| Localização (GPS) | Não | Não bloqueia. A captura segue; o payload registra gps_lat/gps_lon como 0 e gps_accuracy como 9999. |
| Bússola (heading) | Não | No iOS o navegador pede um gesto do usuário; há um botão para pular. A captura segue sem heading. |
GPS negado × contas em
enforce. Como a negação de GPS não bloqueia no navegador, mas uma conta configurada para exigir verificação de localização pode rejeitar a captura no servidor (HTTP422), o resultado prático é: um usuário legítimo que negou a localização — ou está atrás de VPN — pode ter a foto recusada. Oriente o usuário a permitir a localização. Detalhes do comportamento em Configuração da sua conta e o tratamento do422em Erros e bloqueios.
Uma foto por chamada (você orquestra a quantidade)
Seção intitulada “Uma foto por chamada (você orquestra a quantidade)”O Web SDK é single-photo: cada open() captura uma foto, emite uploaded e para — sem loop automático. Para N fotos, você chama open({ token }) N vezes no mesmo token.
O rótulo de orientação de cada foto vai no overlay: open({ overlay: { label: 'Chassi' } }) — apresentado ao usuário e assinado no manifesto como photo_label (client-attested, modelo fraud_deterrence).
// N fotos = N chamadas de open() no mesmo tokenawait camera.open({ token, overlay: { label: 'Frente do veículo' } });// (no seu handler de uploaded, ao concluir uma, abra a próxima:)await camera.open({ token, overlay: { label: 'Traseira' } });Resultado inline — o SDK não navega
Seção intitulada “Resultado inline — o SDK não navega”Ao concluir a captura, o componente emite uploaded com o resultado inline no próprio evento
(e.detail.capture.result = { session_id, authenticated_jpeg_url }) e para. O SDK é
transparente: não redireciona nem navega. A navegação pós-captura é sua — no handler de
uploaded, feche o overlay do componente com camera.remove() e mostre sua própria tela (ou abra
a próxima foto no mesmo token).
camera.addEventListener(EVENTS.UPLOADED, (e) => { const { result } = e.detail.capture; // { session_id, authenticated_jpeg_url } camera.remove(); // fecha o overlay do SDK mostrarTelaDeSucesso(result); // sua própria navegação / UI});Exemplo — wiring dos eventos
Seção intitulada “Exemplo — wiring dos eventos”O mínimo útil: instanciar o componente, escutar os eventos e abrir a captura com o token do
seu backend.
import { useProvviCamera, EVENTS } from '@provvi/web-sdk';
const camera = useProvviCamera({ licenseKey: 'pvv_live_SuaChaveAqui' });
camera.addEventListener(EVENTS.OPENED, () => { console.log('Câmera pronta');});
camera.addEventListener(EVENTS.CAPTURE, (e) => { console.log('Frame capturado:', e.detail.capture.id);});
camera.addEventListener(EVENTS.UPLOADING, () => { console.log('Enviando para assinatura ICP-Brasil…');});
camera.addEventListener(EVENTS.UPLOADED, (e) => { const { result } = e.detail.capture; if (!result) return; console.log('Foto assinada:', result.session_id, result.authenticated_jpeg_url); // Para outra foto: chame camera.open({ token, overlay: { label } }) de novo no mesmo token.});
camera.addEventListener(EVENTS.ERROR, (e) => { console.error(`Erro ${e.detail.error.code}: ${e.detail.error.message}`);});
camera.addEventListener(EVENTS.CLOSED, () => { console.log('Câmera encerrada');});
// token obtido do SEU backend (Passo 1) — chame open() uma única vezcamera.open({ token });