# 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: ```ts Math.round(Math.max(0, Math.min(1, similarity)) * 10000) / 100 ``` O 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**: ```text 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: false ``` O 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: - `Model` correspondente 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`. ```json { "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](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.