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 o comportamento que você precisa conhecer: uma captura por
token, sem loop e sem redirect — o resultado chega no evento e a navegação é sua.
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: início da aquisição de localização (antes de qualquer
rede; pode ser antecipada com warmUpLocation()) → 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 → bússola em background → espera pelo toque no botão de captura → processamento
do frame → espera curta pela localização, se ela ainda não chegou → upload para assinatura. O
ciclo cobre uma foto e para (ver
Uma foto por chamada).
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 antes de reabrir; depois de uploaded, reabra fora do handler — o componente
libera o ciclo ao terminar o fluxo, e o result não traz campo de conclusão.
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. Entrecaptureeuploadingo componente pode aguardar a localização por alguns segundos (no máximo 8), se ela ainda não chegou — a foto já é a do toque; o que espera é só o envio.uploaded— a assinatura concluiu; o resultado da foto está disponível.closed— o stream foi encerrado e a UI escondida.
A qualquer momento do ciclo — e antes de opened, se você chamou warmUpLocation() — o componente
emite location quando o estado da localização muda (ver O evento location).
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 | {} |
location |
EVENTS.LOCATION |
O estado da localização mudou (a qualquer momento; antes de opened se você chamou warmUpLocation()) |
{ status } ou { status, accuracy } |
O evento location
Seção intitulada “O evento location”O componente pede a localização ao navegador e informa em que estado ela está. Ele não exige localização para capturar: a captura leva a melhor posição recebida até o envio e segue mesmo sem nenhuma; a validação de localização acontece no servidor (ver Configuração).
camera.addEventListener(EVENTS.LOCATION, (e) => { const { status, accuracy } = e.detail; // accuracy só existe em 'available' (metros, como o navegador reporta) if (status === 'denied') mostrarDica('Permita a localização para uma vistoria completa.');});status |
Significado |
|---|---|
waiting |
Aquisição em andamento, sem posição ainda |
available |
Há posição; accuracy é a precisão em metros. Emitido de novo só quando a precisão melhora |
denied |
O usuário (ou o sistema) negou a localização |
unavailable |
Sem geolocalização no navegador, ou o navegador não conseguiu obter posição |
timeout |
A aquisição terminou sem posição |
idle |
Ainda não começou (só antes do primeiro open()/warmUpLocation()) |
O detail traz só esses campos — nunca coordenadas. O componente mostra o mesmo estado no badge
da tela de captura (“Obtendo GPS…”, “GPS ±Nm”, “Localização negada”, “Sem localização”).
camera.warmUpLocation() — antecipa o pedido de permissão e o aquecimento do GPS antes do
open() (por exemplo, ao carregar a página ou quando o usuário inicia o fluxo). É opcional e pode
ser chamado mais de uma vez; o open() faz o mesmo sozinho. Registre o listener de location
antes de chamá-lo, para não perder os primeiros estados. Uma posição recebida no aquecimento
vale para o open() que vier logo em seguida.
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. SobNEVERo componente não entrega artefato algum no evento: nem o JPEG, nem URL de manifesto. O sidecar do manifesto sai porGET /web/session-results/:token, pelo seu backend, logo após ouploaded— a rota não é garantida depois dos 30 minutos de validade da sessão. A imagem enviada pelo navegador passa por armazenamento temporário só para ser assinada e não é servida a ninguém (ver Configuração). Ver Resultados.Nota (2026-09-21). Desde 2026-09-20 a resposta do
POST /web/ingestentrega o JPEG assinado inline (camposigned_image_base64), em qualquer política de armazenamento. O componente<provvi-camera>publicado não expõe esse campo ao seu código — o que está acima continua valendo para a integração pelo componente. Quando o componente passar a expô-lo, esta página será atualizada. Contrato do campo na Referência da API Web.
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 no navegador. O componente informa o estado pelo evento location e segue; a captura leva a melhor posição recebida até o envio; a validação de localização acontece no servidor (ver abaixo). |
| 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. A negação de GPS não bloqueia no navegador, mas a captura pode ser recusada no servidor por localização (HTTP
422) — o resultado prático é: um usuário legítimo que negou a localização pode ter a foto recusada. Duas formas de se antecipar: chamecamera.warmUpLocation()assim que a página carregar (o pedido de permissão e o aquecimento do GPS acontecem antes de o usuário chegar ao botão) e reaja ao eventolocation— emdenied, oriente o usuário na sua UI; ou peça a permissão de localização na sua UI, antes de abrir a câmera. O nível da verificação é configurado por conta (Configuração da sua conta); 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. Cada token vale por uma captura bem-sucedida: depois dela a sessão fecha, e um novo open() com o mesmo token chega ao seu handler de error como SESSION_FETCH_FAILED (4001) com error.details.status === 410. Para N fotos, você cria N sessões no seu backend (um token por foto) e chama open({ token }) uma vez para cada. Falha antes da assinatura (permissão, GPS, 422, upload) não consome o token: reabra com ele, dentro dos 30 minutos de validade e do limite de 20 carregamentos da sessão. Se o retry vier com details.status === 410, a sessão não aceita mais captura — mas o 410 é o mesmo para sessão concluída e expirada. Antes de dar a foto por feita, confirme em GET /web/session-results/:token: status: "captured" ⇒ a captura saiu apesar do erro; expired ou pending ⇒ não houve captura, refaça com uma sessão nova.
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). Limite de tamanho e caracteres aceitos na Referência do Web SDK: fora deles a captura é recusada com 400.
// N fotos = N tokens, um por foto (cada um criado pelo seu backend)botaoFoto1.onclick = () => camera.open({ token: tokenFrente, overlay: { label: 'Frente do veículo' } });botaoFoto2.onclick = () => camera.open({ token: tokenTraseira, overlay: { label: 'Traseira' } });// open() não rejeita: sucesso chega em `uploaded`, falha em `error`. Não encadeie com await.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.close() e mostre sua própria tela. Use
remove() só ao sair da página: o componente é singleton e, depois de remove(), um novo open()
roda sobre um elemento fora do DOM (câmera ligada, sem interface, sem erro). A próxima foto, com
outro token, abra a partir de uma ação do usuário, fora do handler.
camera.addEventListener(EVENTS.UPLOADED, (e) => { const { result } = e.detail.capture; // { session_id, authenticated_jpeg_url } camera.close(); // fecha o overlay do SDK (remove() só ao sair da página) 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: novo token do seu backend e camera.open({ token: novoToken, overlay: { label } }), // disparado por uma ação do usuário — fora deste handler.});
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 });