Pular para o conteúdo

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.


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.


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. Entre capture e uploading o 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.


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 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.

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)
}

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_url pode vir null. Contas configuradas para não armazenar o JPEG (zero-knowledge, store_asset_policy = NEVER) não retêm o asset — o null aqui é esperado, não um erro. Sob NEVER o componente não entrega artefato algum no evento: nem o JPEG, nem URL de manifesto. O sidecar do manifesto sai por GET /web/session-results/:token, pelo seu backend, logo após o uploaded — 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/ingest entrega o JPEG assinado inline (campo signed_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.

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.


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

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: chame camera.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 evento location — em denied, 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 do 422 em 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.

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
});

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 vez
camera.open({ token });

← O componente · Próximo → Erros e bloqueios