Contrato do resultado — Face Local SDK#
SDK 0.5+ · schemaVersion: "1.0" · tipo exportado: VerificationResult.
Este documento descreve o retorno da API runFaceLocalVerification, não o
relatório interno da API anterior. Os detalhes são destinados ao app integrador;
não precisam aparecer no onboarding do usuário.
1. O que “aprovado” significa#
approved: true significa que uma captura nativa passou pela prova de vida e
encontrou uma foto local, com uma única face, EXIF elegível do mesmo modelo de
aparelho e similaridade igual ou superior ao limiar configurado.
Não confirma identidade civil, nome, documento, titularidade do celular nem autenticidade do EXIF. Uma pessoa pode aparecer em fotos de outro aparelho do mesmo modelo. Metadados também podem ser copiados ou editados.
Não há upload, consulta remota de rostos, cadastro em banco biométrico ou seleção manual de imagem de referência. Os templates são internos ao nativo, temporários e nunca retornam no JSON.
2. Status e aprovação#
status | approved | Significado |
|---|---|---|
approved | true | Primeiro match elegível; a busca para imediatamente |
not_approved | false | Prova de vida recusada, ou busca elegível concluída sem match |
inconclusive | null | Não há elementos suficientes para decidir |
cancelled | null | O usuário ou a integração cancelou a tentativa |
error | null | Permissão, ambiente, evidência ou outra operação impediu a conclusão |
Nunca converta null para “reprovado”. Use status no controle de fluxo.
not_approved significa que a tentativa não atingiu os critérios, não que a
pessoa é uma impostora ou que o celular não lhe pertence.
Motivos comuns:
reason | Interpretação |
|---|---|
match-found | Há comparação aprovada |
no-match | Comparações válidas concluídas abaixo do limiar |
liveness-failed | A captura não foi aprovada; não existe referência exportável |
no-eligible-photos | Nenhuma candidata elegível |
photos-missing-metadata | Faltam os campos EXIF obrigatórios |
different-device-model | Fotos candidatas informam outro fabricante/modelo |
no-single-face | Não foi obtida uma comparação válida com uma única face |
search-limit | O limite foi atingido e ainda há fotos não examinadas |
unreadable-photos | A leitura ficou incompleta |
comparison-failed | O motor não concluiu comparações necessárias |
photo-full-access-required | O sistema concedeu acesso limitado ou não informou escopo completo |
photo-permission | Acesso às fotos recusado |
cancelled | Tentativa cancelada |
reference-expired | A referência deixou de ser válida |
evidence-unavailable | Binário nativo anterior à API de exportação |
evidence-invalid | Fotos, sessão, score ou metadados não correspondem ao resultado |
cleanup-failed | Não foi possível confirmar a limpeza |
Erros adicionais usam o código nativo sanitizado, em minúsculas e com hífens, em
reason. errorCode preserva a forma maiúscula, por exemplo EVIDENCE_EXPIRED.
Não use mensagens de erro como lógica de negócio; códigos desconhecidos também
devem bloquear aprovação. Não são incluídos stack traces nem mensagens internas.
Uma foto ilegível não impede um match válido encontrado depois. Mas erro de integridade, expiração, cancelamento, falha da exportação ou da limpeza aborta a tentativa inteira, sem aprovação parcial.
3. Similaridade não é acurácia#
Os métodos usados são evaluateFaceSimiByFeature no Android e
evaluateSimilarity(f1:f2:) no iOS. O adaptador recebe um score de similaridade
do motor, não uma estimativa calibrada da probabilidade de identidade.
| Campo | Descrição |
|---|---|
faceMatch.similarity | Score original da comparação selecionada; null sem comparação válida |
faceMatch.threshold | Limiar realmente aplicado, padrão 0.83 |
faceMatch.similarityPercent | Índice de apresentação entre 0 e 100; null sem score |
faceMatch.thresholdPercent | Limiar na mesma escala de apresentação |
faceMatch.percentageIsProbability | Sempre false |
faceMatch.selectedComparison | matched, best-compared ou none |
O índice é calculado assim:
Math.round(Math.max(0, Math.min(1, similarity)) * 10000) / 100O adaptador aceita scores finitos de -1 a 1. Valores negativos são mantidos
no score original e apresentados como zero no índice. A conversão não altera a
decisão, que sempre usa similarity >= threshold, sem arredondamento.
Por exemplo, 0.82996 aparece como 83 no índice, mas continua abaixo de 0.83.
Exemplo ilustrativo, não medição realizada: 0.91 produz índice 91 e passa
um limiar 0.83. Isso não significa “91% de certeza”, “91% de acurácia” nem
“9% de risco de fraude”.
Acurácia é uma medida de desempenho em um conjunto rotulado. Sua avaliação exige
pares da mesma pessoa e de pessoas diferentes, separação entre calibração e teste,
falsos matches, falsas recusas e condições de uso representativas. Uma taxa de
benchmark isolada não é a acurácia deste fluxo nem de uma foto.
O limiar padrão é um valor inicial e não foi calibrado para este
produto ou para todos os aparelhos. Não há campo accuracy fictício no retorno.
4. Qual foto e qual comparação são retornadas#
O contrato expõe no máximo duas cópias temporárias, não a biblioteca inteira:
photos.liveness: imagem da mesma captura nativa aprovada que gerou a referência. Não é outra selfie tirada depois e não pode ser fornecida pela integração.photos.compared: a imagem da galeria efetivamente analisada na comparação selecionada. Com aprovação, é a primeira que passou. Sem match, é a de maior score entre as comparações concluídas; empates mantêm a primeira.- Sem comparação válida,
photos.comparedénull. - Falha na prova de vida, cancelamento ou erro: ambas são
null. - Um resultado inconclusivo pode conter o par da melhor comparação já concluída; isso não o transforma em uma aprovação nem em uma rejeição.
faceMatch, exif e photos.compared sempre se referem ao mesmo par selecionado.
O EXIF não é de uma foto enquanto o score e a imagem são de outra.
O nativo valida o vínculo da sessão e o integrador recebe um objeto filtrado,
sem nomes de arquivos originais, identificadores da galeria ou lista de fotos.
Cada foto presente possui:
| Campo | Descrição |
|---|---|
uri | file:///..., em cache privado do próprio app; não é URL pública |
mimeType | image/jpeg |
width, height | Dimensões da cópia, em pixels |
sizeBytes | Tamanho do JPEG, limitado a 8 MiB |
sha256 | Hash dos bytes da cópia JPEG; 64 caracteres hexadecimais |
representation | normalized-jpeg |
base64 | Presente somente ao solicitar format: "base64"; sem prefixo data: |
São cópias normalizadas/recomprimidas das imagens já utilizadas na inferência, não cópias byte a byte do original. A exportação não relê um arquivo da galeria que poderia ter mudado. Orientação é normalizada; EXIF original, GPS e demais metadados não são embutidos no JPEG exportado.
O score foi calculado sobre a imagem utilizada pelo motor antes da recompressão de exportação. Refazer o match sobre os JPEGs pode produzir um score numericamente diferente. O hash não é assinatura do fabricante, prova da origem da câmera, hash do EXIF original ou atestado externo de integridade.
5. EXIF materializado#
exif é null quando não houve comparação válida selecionada. Quando presente:
| Campo | Descrição |
|---|---|
source | original-photo-bytes: leitura nativa dos mesmos bytes decodificados para o motor |
comparison | matched ou best-compared, coerente com o par selecionado |
fields.Make | Fabricante registrado no EXIF original |
fields.Model | Modelo registrado no EXIF original |
fields.DateTimeOriginal | Data original como texto EXIF, sem inventar fuso horário |
expected | Fabricante/modelo da captura e eventual código de hardware verificado |
manufacturerMatches, modelMatches | Resultado da regra de correspondência |
matchRule | exact-model-or-verified-hardware-alias |
editable | Sempre true: EXIF pode ser alterado |
provesDeviceOrigin | Sempre false |
Os três campos são obrigatórios. O filtro normaliza somente espaços e caixa; não aceita modelo da mesma família por aproximação. Um código de hardware alternativo precisa de equivalência explicitamente verificada no módulo nativo. O EXIF nunca ensina ao SDK qual seria o modelo do aparelho.
DateTimeOriginal é uma presença obrigatória, não uma validação forense de
cronologia ou autenticidade. Datas compartilhadas/editadas podem ser falsas.
Não são retornados GPS, localização, número de série, MakerNote nem o EXIF completo.
6. Device Fingerprint: perfil, não identidade única#
O nome deviceFingerprint descreve, neste contrato, um perfil do modelo:
kind: "model-profile"
make: fabricante usado na captura
model: nome/código exato resolvido nativamente
hardwareModel?: código real do sistema, quando houver alias Android verificado
platform: android ou ios
osVersion: versão informada pelo sistema, ou null se indisponível
uniqueDevice: false
provesOwnership: falseO objeto só é retornado após uma captura aprovada, vinculada ao aparelho atual. No Android, fabricante/código vêm do sistema e de equivalências verificadas. No iPhone, o modelo é resolvido localmente pelo DeviceKit. Modelo desconhecido é recusado; nunca vira simplesmente “iPhone” ou “Android”.
Não coletamos IDFA, IDFV, IMEI, MAC, número de série ou nome personalizado para este contrato. Não construímos um identificador de rastreamento. Milhares de unidades compartilham o mesmo perfil. Portanto:
Modelcorrespondente não prova que a foto veio daquela unidade física.- Ter fotos parecidas no álbum não prova propriedade do celular.
- Acrescentar a versão do sistema não torna o perfil único.
- Esse campo não substitui App Attest, Play Integrity ou identidade civil.
7. Auditoria da decisão#
| Campo | Descrição |
|---|---|
checks.livenessPassed | true após captura validada, false em recusa explícita, null sem conclusão |
checks.faceMatchPassed | true no positivo, false na busca concluída sem match, null nos demais casos |
checks.exifModelMatched | Resultado para a comparação selecionada; null se não houve uma |
decision.policy | local-face-continuity-v1 |
decision.liveness | Score real, mínimo exclusivo 0.85, método passive-active e origem native-camera; ou null |
decision.engine | Provedor, versão do motor e plataforma; ou null se indisponível |
decision.requirements | Câmera com liveness, permissão completa, EXIF do modelo atual, face única e limiar configurado |
decision.comparedPhotos | Quantidade de pares com uma face e similaridade válida |
decision.nativeAttempts | Chamadas de comparação nativa, incluindo ausência de face e falhas |
decision.stoppedOnFirstMatch | true somente quando retornou aprovação |
decision.exclusions | Contagens agregadas por motivo, sem listar o conteúdo do álbum |
A prova de vida exige também sequência de câmera e checagens locais de integridade; não é decidida apenas pelo valor escalar. O score de liveness também não é uma probabilidade calibrada.
As contagens só descrevem o relatório efetivamente concluído. Em aborto/erro que não produziu relatório final, podem ser zero mesmo que alguma tentativa tenha ocorrido; não são um log permanente de processamento.
verificationId é um identificador aleatório de sessão, não de pessoa ou aparelho.
completedAt é uma data ISO 8601 UTC baseada no relógio local, não em fonte de
tempo atestada. processing: "on-device" descreve o caminho desta integração.
identityVerified: false evita interpretar o resultado como verificação civil.
8. Exemplo do formato#
Exemplo sintético e abreviado, com valores ilustrativos, nunca utilizável
como resultado biométrico real. O retorno real inclui os demais campos
documentados acima e definidos em VerificationResult.
{
"schemaVersion": "1.0",
"verificationId": "identificador-aleatorio-da-sessao",
"status": "approved",
"approved": true,
"reason": "match-found",
"errorCode": null,
"checks": {
"livenessPassed": true,
"faceMatchPassed": true,
"exifModelMatched": true
},
"faceMatch": {
"similarity": 0.91,
"threshold": 0.83,
"similarityPercent": 91,
"thresholdPercent": 83,
"percentageIsProbability": false,
"selectedComparison": "matched"
},
"processing": "on-device",
"identityVerified": false
}Não valide o resultado por um percentual arredondado, presença de uma foto, campo EXIF isolado ou texto exibido. Consuma o estado final e trate o JSON inteiro como dado sensível, não como conteúdo para logs ou analytics.
9. Integridade e limites#
Os templates são removidos antes do retorno. As fotos opcionais têm ciclo de vida separado, descrito em INTEGRACAO.md. Cancelamento, falha de validação ou de limpeza nunca libera um resultado aprovado.
A captura não aceita imagem externa; a comparação confere EXIF em nativo; o resultado associa sessão, imagem, metadados, hash e score. Essas medidas reduzem troca acidental ou manipulação conhecida, mas não tornam confiável um processo hostil. Root/jailbreak oculto, hooks, câmera/driver adulterado ou um app modificado podem contornar checagens locais.
O JSON não é assinado nem atestado remotamente. Não aceite um objeto enviado por um cliente como prova irrefutável para um servidor. Testes unitários e compilação não medem acurácia ou resistência real a ataques. O iOS foi testado em iPhone. Para integrar, gere o build com Mac/Xcode e valide o comportamento de rede, armazenamento e biometria do seu app em aparelho físico.