Pular para o conteúdo

Passo 4 · Erros e bloqueios

Toda falha de captura chega ao seu código de duas formas: como um erro no evento error do componente <provvi-camera> (Tabela A) ou, por trás dele, como um erro HTTP de um dos endpoints da API (Tabela B). Esta página é orientada a ação: para cada caso, o que fazer no seu app ou o que orientar o usuário.

Alguns bloqueios (frames de câmera ao vivo, recaptura, localização inválida) são gates de dissuasão de fraude (fraud deterrence). Lembre que a garantia do fluxo web é assinatura server-side ICP-Brasil A1 sobre os bytes recebidos — não há âncora de hardware device-side. Esses gates elevam o custo do ataque; não substituem a cadeia de prova com hardware do SDK mobile.

Trate primeiro por classe (instanceof) para o caso amplo, depois por código (error.code) para o granular. O objeto do erro traz error.code, error.message (o texto do servidor, quando há), error.details e error.toJSON() (sem stack trace, seguro para telemetria).

import {
CODES,
ProvviBrowserError,
ProvviLicenseError,
ProvviStreamError,
} from '@provvi/web-sdk';
camera.addEventListener('error', (e) => {
const { error } = e.detail; // ProvviError com .code, .message e .details
// 1) Por classe — trata o caso amplo
if (error instanceof ProvviBrowserError) {
mostrarQrParaCelular(); // 2002 — abra no celular
return;
}
if (error instanceof ProvviLicenseError) {
// error.details.reason: INVALID_KEY | REVOKED | EXPIRED |
// VOLUME_EXCEEDED | CLIENT_SUSPENDED | ORIGIN_NOT_ALLOWED
mostrarErroLicenca(error.details?.reason);
return;
}
if (error instanceof ProvviStreamError) {
pedirPermissaoCamera(); // 3001 — permita a câmera
return;
}
// 2) Por código — trata o granular
switch (error.code) {
case CODES.TOKEN_REQUIRED: // 5002 — bug de integração
console.error('open() foi chamado sem token');
break;
case CODES.SESSION_FETCH_FAILED: // 4001 — sessão inválida, ou acesso não validado (403 sem block_reason)
if (error.details?.status === 403 &&
error.message === 'Não foi possível validar o acesso. Recarregue a página pelo link recebido e tente de novo.') {
// acesso não validado: oriente a recarregar a página pelo link recebido; se persistir, acione o suporte.
// NÃO recrie a sessão em laço — cada nova sessão repete o mesmo 403.
mostrar(error.message);
break;
}
recriarSessao();
break;
case CODES.INGEST_FAILED: // 4002 — mapeie pelo HTTP
tratarIngest(error.details?.status, error.message);
break;
default:
reportarTelemetria(error.toJSON?.() ?? error);
}
});

O código INGEST_FAILED (4002) embrulha os erros HTTP da captura: o status real fica em error.details.status. Mapeie-o para a ação do usuário (Tabela B, seção POST /web/ingest):

function tratarIngest(status, mensagem) {
switch (status) {
case 400: // câmera ao vivo, rótulo da foto, coordenadas ou imagem — o texto em `mensagem` distingue
mostrar(mensagem);
// câmera ao vivo ⇒ refaça a captura apontando para o objeto real (o componente envia os
// frames extras sozinho); rótulo ⇒ bug de integração, corrija o `overlay.label`;
// coordenadas ⇒ bug de integração (não esperado pelo componente): envie a posição como o
// navegador a reporta; imagem ⇒ bug de integração (não esperado pelo componente): envie os
// bytes que `captureFrames` produz, sem convertê-los
break;
case 403: // license, origem, relógio, nonce, sessão inválida ou acesso não validado (sem block_reason) — o texto em `mensagem` distingue
mostrar('Não foi possível validar esta captura: ' + mensagem);
// origem ⇒ domínio não habilitado para esta licença: fale com a Provvi e NÃO repita a
// requisição; sessão carregada de outra origem ⇒ conclua a captura na página em que a
// sessão foi aberta, ou crie uma sessão nova e abra-a já nessa página;
// relógio ⇒ acertar data e hora; nonce ⇒ recarregar a página; sessão inválida ⇒ nova sessão;
// licença ⇒ mesma ação da Tabela A, 8002; acesso não validado (sem block_reason) ⇒ recarregar a
// página pelo link recebido e tentar de novo; se persistir, acionar o suporte — não repetir em laço
break;
case 413: // foto acima do limite por imagem — o limite vem no texto de `mensagem`
mostrar('A foto ficou acima do tamanho aceito. Vamos tentar de novo.');
camera.open({ token }); // a sessão admite novas tentativas; o componente não retenta sozinho
break;
case 404:
case 410: // sessão inválida ou expirada — crie outra
recriarSessao();
break;
case 409: // não foi possível processar — refaça a captura
mostrar('Não foi possível processar esta captura. Tente novamente.');
break;
case 422: // recaptura ou localização — objeto físico + permitir GPS
mostrar('Fotografe o objeto físico e permita o acesso à localização.');
break;
case 502: // falha do lado da Provvi — a captura NÃO foi entregue
mostrar('Não foi possível concluir a captura. Vamos tentar de novo.');
// Recarregue a sessão (open({ token })) e refaça a captura — o componente tira uma foto
// nova. O `block_reason` diz se foi o serviço de assinatura, o armazenamento da prova ou o
// registro; a conduta é a mesma. Ver Tabela B.
break;
case 503: // transitório — repita com backoff
mostrar('Serviço temporariamente indisponível. Vamos tentar de novo em instantes.');
// Se a mensagem pedir para recarregar a sessão, repetir o mesmo envio não resolve:
// chame open({ token }) de novo e refaça a captura.
break;
default:
mostrar('Não foi possível concluir a captura.');
}
}

Estes são os erros com throw-site real hoje. Cada um chega em e.detail.error.

code Nome Quando ocorre O que fazer
2002 BROWSER_NOT_SUPPORTED Navegador fora da allowlist. Só Safari mobile ou Chrome mobile passam; desktop, Firefox e Chrome/Edge/Opera no iOS são bloqueados. Use camera.isUserAgentSupported() antes de abrir e conduza o usuário no seu fluxo quando não for suportado (ex.: orientá-lo a abrir no celular).
3001 STREAM_OPEN_FAILED Falha ao abrir a câmera — permissão negada ou câmera em uso por outro app. A câmera é obrigatória. Peça ao usuário para permitir a câmera e fechar apps que a estejam usando; ofereça repetir.
5002 TOKEN_REQUIRED open() foi chamado sem token. Bug de integração — passe o token da sessão criada no backend (Passo 1).
8001 LICENSE_KEY_MISSING licenseKey não foi informada em useProvviCamera() nem em open(). Bug de integração — informe a license key pvv_* do frontend.
8002 LICENSE_INVALID Licença rejeitada na validação. O motivo está em error.details.reason. Depende do reason: EXPIRED/VOLUME_EXCEEDED → renovar/ampliar; REVOKED/CLIENT_SUSPENDED → contatar a Provvi; ORIGIN_NOT_ALLOWED → domínio não autorizado (ver Passo 6).
8003 LICENSE_NETWORK_ERROR Falha de rede ao validar a licença. Verifique a conexão do usuário e ofereça repetir.
4001 SESSION_FETCH_FAILED Não foi possível carregar a sessão. O status HTTP está em error.details.status: 403 sessão ilegível, origem não habilitada, licença recusada ou acesso não validado (o texto de error.message distingue — o componente não repassa block_reason; no último caso a ação é recarregar a página pelo link recebido e tentar de novo, e, se persistir, acionar o suporte — sem recriar a sessão), 404 não encontrada, 410 expirada ou já usada, 429 carregamentos demais, 503 indisponibilidade transitória. 403 com block_reason web_origin_not_allowed ou web_origin_allowlist_empty: domínio não habilitado para esta licença — fale com a Provvi e não repita a requisição nem recrie sessões em laço (ver Passo 6). 403 de acesso não validado (mensagem Não foi possível validar o acesso…): oriente o usuário a recarregar a página pelo link recebido e tentar de novo; se persistir, acione o suporte — não recrie a sessão em laço. Demais 403, 404, 410 e 429: crie uma nova sessão no backend e reabra o componente. 503: repita open({ token }) em instantes — não gaste uma sessão nova. Detalhe na Tabela B, seção GET /web/sessions/:token.
4002 INGEST_FAILED Falha ao enviar a captura. Duas origens: (a) o upload da imagem para o armazenamento temporário falhou — mensagem "Falha de rede no upload da imagem" (sem details.status; o navegador não obteve resposta legível) ou "Upload da imagem falhou (N)" (details.status = N, o status devolvido pelo armazenamento); (b) o servidor recusou a captura — o status HTTP está em error.details.status. (a) chame open({ token }) de novo uma vez: o componente recarrega a sessão e recebe uma URL de upload nova (a URL vale 5 minutos; a sessão admite 20 carregamentos no total, contando o primeiro). Se falhar de novo já na primeira captura a partir do seu domínio, a origem pode não estar habilitada — fale com a Provvi, não repita em laço. (b) mapeie details.status pela Tabela B, seção POST /web/ingest.
— reservados (não ocorrem hoje) 1000, 3002, 5001, 6001, 6002, 6003, 7001 existem em CODES mas não têm throw-site atual. Não trate como comportamento; cubra-os apenas no default de telemetria.

Os corpos abaixo são os literais retornados pela API. O componente chama GET /web/sessions e POST /web/ingest internamente e os expõe via os códigos 4001/4002 (acima). A criação de sessão você mesmo faz no backend (Passo 1).

POST /web/sessions — criação da sessão (backend)

Seção intitulada “POST /web/sessions — criação da sessão (backend)”
HTTP Corpo O que fazer
400 { "error": "Payload inválido: ..." }, { "error": "license_key ausente ou malformada", "block_reason": "web_license_key_malformed" } ou { "error": "...", "block_reason": "web_reference_id_invalid" } Corpo ilegível, license_key fora do formato (pvv_live_/pvv_stag_/pvv_sand_ + 32–33 caracteres) ou reference_id fora do contrato (ASCII, 1–512 bytes, sem espaço nas pontas). Corrija o payload no seu backend — não é transitório.
401 { "error": "API key inválida ou ausente" } (variações: "...ou revogada", "...apenas pvv_api_* aceito") Confira o header x-api-key: pvv_api_... do seu backend. Nunca envie a x-api-key ao frontend.
403 { "error": "License inválida para esta API key" } A license_key do corpo não pertence a esta x-api-key. Use a license emitida para a mesma conta.
403 { "error": "License e API key pertencem a ambientes diferentes", "block_reason": "web_license_environment_mismatch" } A license é sua, mas de outro ambiente (sandbox × produção) que a x-api-key. Use a license e a api key do mesmo ambiente.
429 { "error": "Limite de criação de sessões atingido..." } Reduza a taxa de criação de sessões e aplique backoff. Cada sessão é única — não recrie desnecessariamente.
500 { "error": "Erro na validação de API Key" } ou { "error": "Erro na validação da license" } Dado inconsistente do lado da Provvi ao validar a x-api-key ou a license. Não é transitório: não repita em laço — fale com a Provvi.
503 { "error": "Serviço temporariamente indisponível, tente novamente", "block_reason": "web_upstream_unavailable" } Transitório (um serviço interno da Provvi indisponível; fail-closed). Repita a criação com backoff exponencial. Não trate como falha permanente.

GET /web/sessions/:token — chamado pelo componente (chega como 4001)

Seção intitulada “GET /web/sessions/:token — chamado pelo componente (chega como 4001)”
HTTP Corpo O que fazer
403 { "error": "Sessão inválida — recrie a sessão.", "block_reason": "web_session_corrupt" } A sessão não pôde ser lida pelo servidor e não se recupera. Crie uma sessão nova no backend.
403 { "error": "Origem não autorizada para esta licença — fale com a Provvi para habilitar este domínio.", "block_reason": "web_origin_not_allowed" } ou "web_origin_allowlist_empty" O domínio de onde a página foi servida não está habilitado para esta licença (web_origin_allowlist_empty = a licença ainda não tem domínio algum registrado). Não repita a requisição e não recrie sessões em laço: registre o domínio com a Provvi antes de publicar (Passo 6). A sessão continua válida: habilitado o domínio, a mesma sessão pode ser carregada de novo.
403 { "error": "Sessão inválida — recrie a sessão.", "block_reason": "web_license_missing" } A sessão não carrega licença utilizável. Crie uma sessão nova no backend, com a license_key no corpo do create.
403 { "error": "License inválida — verifique a licença.", "block_reason": "web_license_invalid" } A licença amarrada à sessão não é reconhecida. Confirme a license_key usada no create; se ela é a correta, fale com a Provvi — não repita em laço.
403 { "error": "Não foi possível validar o acesso. Recarregue a página pelo link recebido e tente de novo." } — sem block_reason O servidor não validou o acesso: o pedido não se apresentou como um navegador suportado. Pelo <provvi-camera> não é esperado (ele barra o navegador antes, com 2002). Oriente o usuário a recarregar a página pelo link recebido e tentar de novo; se persistir, acione o suporte. Não repita em laço nem recrie a sessão.
404 { "error": "Sessão não encontrada" } Token inválido ou nunca emitido. Crie uma nova sessão no backend.
410 { "error": "Sessão expirada ou já utilizada" } Sessões são single-use e têm validade limitada. Gere uma sessão nova.
429 { "error": "Limite de tentativas atingido para esta sessão" } A página foi recarregada vezes demais. Oriente o usuário a recomeçar com uma sessão nova.
503 { "error": "Serviço temporariamente indisponível, tente novamente", "block_reason": "web_upstream_unavailable" } ou "web_presign_unavailable" Transitório (inclui a emissão da URL de upload). Ofereça repetir em instantes: open({ token }) de novo.

POST /web/ingest — chamado pelo componente (chega como 4002, details.status)

Seção intitulada “POST /web/ingest — chamado pelo componente (chega como 4002, details.status)”
HTTP Corpo O que fazer
400 { "error": "Imagem estática detectada — a câmera deve estar ao vivo durante a captura.", "block_reason": "web_liveness_static" } Os frames extras do envio eram iguais entre si — tela parada, vídeo congelado ou imagem injetada. Oriente o usuário a apontar para o objeto real e capturar pela câmera; o componente coleta e envia os frames extras sozinho.
400 { "error": "Frames de liveness insuficientes — a câmera deve estar ao vivo durante a captura.", "block_reason": "web_liveness_insufficient" } O envio trouxe menos de dois frames extras. Pelo <provvi-camera> não ocorre (ele envia dois). Se você monta a própria UI com as primitivas exportadas, envie extra_frame_hashes conforme a Referência da API Web.
400 { "error": "extra_frame_hashes fora da forma (SHA-256 hex).", "block_reason": "web_liveness_malformed" } Algum hash extra não é um SHA-256 em hexadecimal (64 caracteres). Ocorre quando a aplicação monta os frames extras por conta própria; o componente oficial envia dois frames válidos. Não é ação do usuário.
400 { "error": "extra_frame_hashes acima do teto.", "block_reason": "web_liveness_too_many" } Foram enviados mais hashes extras do que o aceito (limite na Referência da API Web). Ocorre quando a aplicação monta os frames extras por conta própria; o componente oficial envia dois frames válidos.
400 { "error": "label inválido — até 64 bytes, sem caracteres de controle/formato nem <>&\"'`.", "block_reason": "web_label_invalid" } O rótulo da foto (overlay.label, assinado como photo_label) está acima de 64 bytes ou usa caracteres não aceitos. Bug de integração: corrija o texto do rótulo — refazer a captura não resolve.
400 { "error": "Localização inválida." } As coordenadas enviadas não são válidas. Com o <provvi-camera> isso não é esperado. Se você monta a própria UI com as primitivas exportadas, envie gps_lat, gps_lon e gps_accuracy como o navegador os reporta, sem transformá-los. Bug de integração: corrija o payload — refazer a captura não resolve.
400 { "error": "Imagem ausente ou inválida" } A imagem enviada não pôde ser aceita (ausente, ilegível ou fora do que o SDK produz). Com o <provvi-camera> isso não é esperado. Se você monta a própria UI com as primitivas exportadas, envie os bytes que captureFrames produz, sem convertê-los nem reprocessá-los. Bug de integração: corrija o payload — refazer a captura não resolve. Se repetir em todas as capturas de um aparelho, fale com a Provvi — não repita em laço.
400 { "error": "photo_order não corresponde à foto pendente desta sessão.", "block_reason": "web_photo_order_mismatch" } O envio não corresponde à foto que a sessão espera. Pelo componente <provvi-camera> isto não ocorre: a sessão web é de uma foto e o componente envia a ordem que ela informa. Se aparecer, o envio não veio do componente — crie uma sessão nova e reabra o componente.
403 { "error": "..." } — licença revalidada (revogada/suspensa/expirada) A licença deixou de ser válida durante a sessão. Trate como licença inválida (ver Tabela A, 8002).
403 { "error": "Origem não autorizada para esta licença — fale com a Provvi para habilitar este domínio.", "block_reason": "web_origin_not_allowed" } ou "web_origin_allowlist_empty" O domínio de onde a página foi servida não está habilitado para esta licença (web_origin_allowlist_empty = a licença ainda não tem domínio algum registrado). Não repita a requisição e não recrie sessões em laço: registre o domínio com a Provvi (Passo 6). A sessão continua válida: habilitado o domínio, a mesma sessão pode ser carregada de novo.
403 { "error": "Sessão carregada de outra origem — recarregue a sessão.", "block_reason": "web_origin_session_mismatch" } A sessão foi carregada em um domínio e a captura enviada de outro. O vínculo é definido no carregamento e não muda: conclua a captura na mesma página em que a sessão foi aberta, ou crie uma sessão nova no backend e abra-a já na página de onde o usuário vai capturar. Repetir o envio da mesma sessão de outra origem devolve o mesmo erro.
403 { "error": "Relógio do dispositivo fora de sincronia..." } Oriente o usuário a verificar a data e a hora do dispositivo e capturar de novo.
403 { "error": "..." } — nonce ausente/inválido (block_reason: web_nonce_missing, web_nonce_malformed ou web_nonce_replay) Oriente o usuário a recarregar a página para renovar a sessão. web_nonce_malformed (valor fora da forma) ocorre quando a aplicação monta a requisição por conta própria; o componente oficial envia o valor recebido ao carregar a sessão.
403 { "error": "Sessão inválida — recrie a sessão.", "block_reason": "web_session_corrupt" } A sessão não pôde ser lida pelo servidor e não se recupera. Crie uma sessão nova no backend e reabra o componente. Se o erro se repetir em todas as capturas, não recrie em laço: fale com a Provvi.
403 { "error": "Não foi possível validar o acesso. Recarregue a página pelo link recebido e tente de novo." } — sem block_reason O servidor não validou o acesso: o envio não se apresentou como um navegador suportado. Pelo <provvi-camera> não é esperado (ele barra o navegador antes, com 2002). Oriente o usuário a recarregar a página pelo link recebido e tentar de novo; se persistir, acione o suporte. Não repita em laço nem recrie a sessão.
404 { "error": "Sessão não encontrada" } Sessão inexistente. Crie uma nova.
404 { "error": "Imagem não encontrada — refaça o upload" } O servidor não achou a imagem enviada (a sessão continua válida). Chame open({ token }) de novo para repetir o upload.
409 { "error": "Sessão já utilizada" } Reuso de sessão já concluída. Gere uma nova.
409 { "error": "Esta captura não pôde ser processada.", "block_reason": "web_duplicate" } Corpo opaco por design (a sub-causa não é revelada). Oriente o usuário a repetir a captura; se persistir, use uma sessão nova.
410 { "error": "Sessão expirada" } Sessão fora de validade. Gere uma nova.
413 { "error": "Imagem acima do tamanho máximo aceito (... bytes).", "block_reason": "web_asset_too_large", "bytes": ..., "max_asset_bytes": ... } A foto excede o limite por imagem. O limite vem no texto da própria mensagem, que o componente entrega em error.message junto de details.status = 413; os campos bytes e max_asset_bytes do corpo não são repassados pelo componente. O <provvi-camera> captura o frame inteiro do stream de vídeo, sem recorte nem redimensionamento, e não expõe opção de resolução (quem monta a própria UI com as primitivas exportadas escolhe o stream). Chame open({ token }) de novo para o usuário repetir a captura; se o erro se repetir num dispositivo, fale com a Provvi.
422 { "error": "Imagem rejeitada — possível recaptura", "recapture_risk": ... } Foto de tela/foto-de-foto detectada. Oriente o usuário a fotografar o objeto físico, sem telas no enquadramento.
422 { "error": "Não foi possível validar a localização da captura." } A localização da captura não pôde ser validada. Oriente o usuário a permitir o acesso à localização e refazer a captura. O nível da verificação de localização é configurado por conta (Passo 6 · Configuração).
502 { "error": "Serviço de assinatura indisponível — refaça a captura.", "block_reason": "web_signer_unavailable" }, { "error": "A assinatura foi recusada — refaça a captura.", "block_reason": "web_signer_rejected" } ou { "error": "Resposta do serviço de assinatura inválida — refaça a captura.", "block_reason": "web_signer_response_invalid" } Falha do lado da Provvi ao assinar: nada foi entregue e a captura não conta como assinada. Com web_signer_rejected nada foi assinado. Com web_signer_unavailable e web_signer_response_invalid a assinatura pode ter ocorrido do lado da Provvi sem chegar ao seu app, e o mesmo frame pode ser recusado como duplicata (409, web_duplicate) por alguns minutos — a nova captura deve ser um frame novo, não o reenvio. Chame open({ token }) de novo para recarregar a sessão e o usuário refaz a captura. Se web_signer_rejected se repetir em todas as capturas de um domínio, não repita em laço: fale com a Provvi.
502 { "error": "Serviço de armazenamento da prova indisponível — refaça a captura.", "block_reason": "web_prova_store_unavailable" }, { "error": "Falha ao gerar o acesso à prova — refaça a captura.", "block_reason": "web_ingest_presign_failed" }, { "error": "Serviço temporariamente indisponível — refaça a captura.", "block_reason": "web_ingest_presign_config_failed" } ou { "error": "Serviço de registro indisponível — refaça a captura.", "block_reason": "web_hash_store_unavailable" } Um serviço interno da Provvi (armazenamento da prova, acesso a ela ou registro anti-replay) indisponível — nada entregue. Recarregue a sessão (open({ token })) e refaça a captura, com backoff.
502 { "error": "Falha ao ler a imagem enviada — recarregue a sessão e refaça o upload.", "block_reason": "web_raw_read_failed" } O upload chegou, mas a leitura da imagem falhou do lado da Provvi. Repetir o mesmo envio não resolve: chame open({ token }) para recarregar a sessão e refaça a captura.
502 { "error": "Imagem assinada acima do tamanho entregável — reduza a resolução da captura.", "block_reason": "web_signed_payload_too_large" } A resposta com o JPEG assinado passaria do tamanho entregável — nada persistido. Não é esperado com o limite atual por imagem (o 413 acima chega antes). Se ocorrer: o <provvi-camera> não expõe opção de resolução — quem monta a própria UI escolhe o stream —, então refaça e fale com a Provvi.
502 { "error": "Serviço temporariamente indisponível — recarregue a sessão e refaça a captura.", "block_reason": "web_ledger_client_id_unauthoritative" } A licença amarrada à sessão não trouxe o dado que registra a captura (configuração do lado da Provvi) — nada assinado. Recarregue a sessão e refaça; se persistir, fale com a Provvi. Não é ação do usuário.
502 { "error": "Serviço temporariamente indisponível — recarregue a sessão e refaça a captura.", "block_reason": "web_ledger_write_failed" } A captura foi assinada e não entregue: o registro probatório não pôde ser gravado. A mesma imagem não é aceita de novo (volta como 409). Chame open({ token }): o componente tira uma foto nova. Ofereça ao usuário refazer a captura; não há como recuperar a anterior.
502 { "error": "Erro interno ao montar a resposta.", "block_reason": "web_response_serialization_failed" } Erro interno da Provvi ao montar a resposta — não é transitório do seu lado. Repita uma vez com open({ token }); se persistir, fale com a Provvi.
503 { "error": "Serviço temporariamente indisponível — tente novamente.", "block_reason": "web_nonce_store_unavailable" } Transitório, do lado da Provvi (fail-closed: a captura não foi assinada). Repita com backoff exponencial.
503 { "error": "Serviço temporariamente indisponível — recarregue a sessão e tente novamente.", "block_reason": "web_raw_store_unavailable" } Transitório, do lado da Provvi (fail-closed). Aqui repetir o mesmo envio não resolve: chame open({ token }) para recarregar a sessão e refaça a captura, com backoff.

Os contratos HTTP completos (payloads, campos e envelopes) estão na Referência da API Web; a API do componente e os códigos, na Referência do Web SDK.


← Ciclo de captura · Próximo → Resultados