Pular para o conteúdo principal

SDK Flutter

dependencies:
veridia_sdk:
git:
url: https://github.com/EielCorp/veridia-sdk-flutter.git
ref: v0.4.3

O pacote é veridia_sdk, versão atual 0.4.3. Dart 3.10.3+, Flutter 3.38.4+.

Não está no pub.dev

A Veridia é uma plataforma empresarial e o SDK é licenciado por cliente, então o pacote é distribuído a partir de um repositório privado em vez de ser publicado abertamente. Só esta documentação é pública.

Envie seu usuário do GitHub ao seu contato na Veridia — a mesma pessoa que emitiu sua chave de API — e liberamos acesso de leitura. Depois disso o trecho acima resolve com um flutter pub get normal.

Fixe uma tag, nunca um branch. ref: main volta a resolver a cada pub get e moveria a sua integração sem mudança de versão. Em CI use a forma SSH, para que nenhum token acabe no arquivo:

url: git@github.com:EielCorp/veridia-sdk-flutter.git
O piso do Flutter é 3.38.4, e é rígido

Versões anteriores desta página diziam Flutter 3.27 / Dart 3.6. Isso subestimava em cerca de um ano: o plugin camera hoje exige Dart 3.10.3 / Flutter 3.38.4, então no Flutter 3.27–3.37 o pub get falha com um erro de resolução transitiva que não nomeia a causa real. Nada degrada em silêncio — ou resolve, ou não resolve.

Este é o único SDK da Veridia que captura imagens. Os outros três são clientes de API. Ele cuida da permissão de câmera, da captura, das checagens de qualidade, da detecção facial no dispositivo e do upload, e te entrega um verificationId quando o pipeline foi iniciado.

Integração rápida

import 'package:veridia_sdk/veridia_sdk.dart';

VeridiaFlow(
config: const VeridiaConfig(
publishableKey: 'qv_pub_your_key_here',
userRef: 'your_internal_user_id',
country: 'GT',
documentType: DocumentType.dni,
locale: VeridiaLocale.es,
),
onComplete: (result) {
// A verificação foi SUBMETIDA. Isto não é um veredicto.
sendToYourBackend(result.verificationId);
},
onError: (error) {
reportToUser(error.code, error.message);
},
)

Essa é a integração inteira. VeridiaFlow é um widget Flutter comum — navegue até ele, empilhe-o como rota em tela cheia, embuta-o em uma aba.

Ele é de uso único. Descarte-o após a conclusão e construa uma nova instância se o usuário precisar tentar de novo.

VeridiaFlow

VeridiaFlow({
required VeridiaConfig config,
void Function(VeridiaResult result)? onComplete,
void Function(VeridiaError error)? onError,
ThemeData? theme,
})

VeridiaConfig

CampoObrigatórioPadrãoNotas
publishableKeysimqv_pub_... ou qv_pubt_...
apiBasenãohttps://api.xxuxe.onlineSobrescreva apenas para uma implantação regional
userRefnãoSeu id de usuário. Ecoado de volta no webhook
countrynãoISO 3166-1 alpha-2 (GT, MX, BR, …)
documentTypenãoSe for null, o SDK pergunta ao usuário
submittedFullNamenãoComparado de forma difusa com o documento no servidor
requireDocBacknãosegue documentTypeVeja abaixo
localenãoVeridiaLocale.enen / es / pt
activeLivenessnãofalseRoda o desafio diante da câmera. Veja abaixo

Nove campos, e essa é a superfície inteira. accentColor não está aqui — o tema vai pelo parâmetro theme do VeridiaFlow, não pela config.

activeLiveness

Desligado por padrão. Colocando true, depois da selfie o SDK roda um desafio curto: o servidor emite uma sequência de poses imprevisível, o SDK captura quatro rajadas de frames contra uma âncora, e o servidor verifica se as respostas batem com o que ele pediu. O app não julga nada por conta própria — captura e envia.

O que isso compra é interatividade: a pessoa reagiu em tempo real a uma sequência que ninguém poderia conhecer de antemão, que é exatamente o que um vídeo pré-gravado ou uma imagem injetada não conseguem fazer. Custa ao usuário cerca de quinze segundos.

O que ele NÃO prova

O desafio não estabelece que o que está diante da câmera tem profundidade, e a Veridia não o trata como se estabelecesse. Um caso que cruza o limiar de aprovação apenas graças ao desafio é encaminhado para revisão humana em vez de ser aprovado sozinho.

Ligar activeLiveness portanto alonga o fluxo e, na margem, manda mais casos para revisão, não menos. Ligue quando quiser essa evidência extra no registro e puder absorver isso; deixe desligado se a sua prioridade é um funil curto.

O mesmo desafio está disponível no widget web.

DocumentType é dni, passport, driversLicense, nationalId, other. O enum Dart é camelCase; ele serializa para o snake_case da API (drivers_license, national_id) por você.

requireDocBack

Se você deixar null, o SDK deriva o valor:

  • documentType: DocumentType.passportfalse. Passaportes são de página única.
  • Qualquer outro documentTypetrue.
  • documentType null → true.

Ou seja, o padrão é ligado para tudo exceto passaporte. Se você esperava um fluxo de duas capturas (frente + selfie) e recebeu três telas, é por isso. Defina requireDocBack: false explicitamente para forçar o desligamento.

VeridiaResult

class VeridiaResult {
String verificationId; // "vf_abc..."
VerificationStatus status; // queued / processing / completed
VerificationVerdict? verdict; // null neste ponto
}

onComplete dispara quando as imagens foram enviadas e o submit foi aceito. O pipeline ainda não rodou. verdict é null, e status não diz nada sobre a pessoa — ele é o estado do job.

Você não consegue ler o veredicto pelo app

GET /v1/verify/{id} exige uma chave secreta. VeridiaConfig aceita apenas uma publishableKey, deliberadamente.

O único jeito de fazer o polling funcionar de dentro do app seria embarcar uma chave qv_sec_* no binário — e um APK ou IPA é desempacotado em minutos. A chave extraída não lê apenas o resultado daquele usuário: ela lê os desfechos de KYC de todos os clientes do seu tenant, incluindo os campos de identidade extraídos. Trate qualquer sugestão de fazer polling a partir de um cliente mobile como um erro.

Envie result.verificationId para o seu próprio backend e resolva o desfecho lá: receba o webhook, ou chame GET /v1/verify/{id} no servidor com sua chave secreta.

O webhook também é o único canal que carrega o desfecho de um caso revisado por um humano, que pode chegar muito depois de o usuário ter fechado seu app.

onComplete: (result) async {
// Seu servidor registra o verificationId associado a este usuário e espera
// pelo webhook. Nada sobre o desfecho é decidido aqui.
await api.post('/kyc/started', {
'verificationId': result.verificationId,
'userId': currentUser.id,
});
showPendingScreen();
}

Defina userRef na config e o webhook o ecoa de volta, o que te poupa desse mapeamento. Se você não definir, o verificationId é o único correlator e você precisa guardá-lo por conta própria.

VeridiaError

class VeridiaError implements Exception {
VeridiaErrorCode code;
String message;
Map<String, Object?>? detail;
}

detail é preenchido quando o erro se originou na API, e em um caso do lado do cliente: um cameraDenied levantado depois que o sistema parou de perguntar carrega {'permanently_denied': true}. É o caso em que o SDK oferece Abrir Ajustes em vez de uma nova tentativa, porque no iOS o diálogo do sistema aparece uma única vez por instalação.

Onze códigos de erro, batendo um a um com o contrato do widget web:

Enum DartwireValueQuando
cameraDeniedcamera_deniedUsuário recusou a permissão de câmera
cameraUnavailablecamera_unavailableNenhuma câmera utilizável no dispositivo
noFaceInSelfieno_face_in_selfieA detecção facial no dispositivo não achou nada
blurryImageblurry_imageA checagem de nitidez falhou
uploadFailedupload_failedUm PUT não teve sucesso
apiUnreachableapi_unreachableFalha de rede ao alcançar a Veridia
invalidApiKeyinvalid_api_keyChave ausente, malformada ou revogada
insufficientCreditsinsufficient_creditsSaldo do tenant esgotado (HTTP 402)
rateLimitedrate_limitedHTTP 429
userCancelleduser_cancelledUsuário desistiu do fluxo
internalErrorinternal_errorQualquer coisa não classificada

code.wireValue devolve a string em snake_case, que é a forma para logar e para comparar com os códigos do widget web.

Dois destes são muito mais comuns que os demais em produção e são fáceis de esquecer de tratar: cameraDenied (um pedido de permissão que o usuário recusou, muitas vezes em definitivo) e userCancelled (o usuário apertou voltar). Nenhum dos dois é falha da sua integração, e ambos precisam de um caminho de UI de verdade — um fluxo de KYC abandonado é o desfecho mais frequente de qualquer funil de onboarding.

Classes de exceção tipadas também são exportadas — CameraDeniedException, UserCancelledException, UploadFailedException, RateLimitException, AuthenticationException, PaymentException, entre outras — se você prefere capturar em vez de ramificar.

Retentativas HTTP não são aplicadas automaticamente dentro do fluxo de captura. As falhas afloram por onError para você decidir o que fazer.

Configuração de plataforma

Android

Em android/app/build.gradle (ou .kts):

android {
compileSdk 36
defaultConfig {
// Deixe no padrão do Flutter. No Flutter 3.38+ resolve para 24 e um
// valor menor é reescrito de qualquer forma.
targetSdk 36
}
}

O piso real do Android é API 24 (Android 7.0). Versões anteriores desta página diziam 21; as dependências realmente permitem 21, mas o Flutter 3.38+ coloca minSdk em 24 por padrão e reescreve valores menores, então 24 é o que você de fato publica, a menos que sobrescreva de propósito.

Nada mais é necessário — nenhuma configuração de ML Kit ou CameraX da sua parte. Este é o caminho contra o qual compilamos um APK de release real, com R8 incluído, e que atende ao requisito de page-size de 16 KB do Google Play.

Em android/app/src/main/AndroidManifest.xml, dentro de <manifest>:

<uses-permission android:name="android.permission.CAMERA"/>
<uses-permission android:name="android.permission.INTERNET"/>
<uses-feature android:name="android.hardware.camera" android:required="true"/>

iOS

Três coisas, e nenhuma é opcional — é aqui que as integrações travam.

1. Suba o deployment target nos dois lugares

O platform do Podfile e o IPHONEOS_DEPLOYMENT_TARGET do projeto Xcode são dois ajustes diferentes, e o CocoaPods compara os dois. Se você subir só o Podfile, o pod install para com:

The platform of the target `Runner` (iOS 13.0) is not compatible with
`GoogleMLKit/FaceDetection`, which requires iOS 15.5

No Xcode: selecione o target RunnerBuild SettingsiOS Deployment Target15.5. Faça isso também no nível do projeto, e confira todas as configurações, inclusive Profile. O template do Flutter inicia apps novos bem abaixo disso, então este passo vale para praticamente qualquer app existente.

2. ios/Podfile

# O Google ML Kit se recusa a instalar abaixo de 15.5, e ele entra por DOIS
# pods distintos — então subir um único plugin não basta para rederivar este
# número:
# google_mlkit_face_detection 0.13.2 -> GoogleMLKit/FaceDetection ~> 9.0.0
# google_mlkit_commons 0.11.1 -> MLKitVision ~> 10.0.0
# Versões anteriores desta página diziam 12.0; com 12.0 o `pod install` aborta
# e o app nunca compila.
platform :ios, '15.5'

post_install do |installer|
installer.pods_project.targets.each do |target|
flutter_additional_ios_build_settings(target)

target.build_configurations.each do |config|
# Inclua a permissão de câmera na compilação do permission_handler. No
# iOS TODAS as permissões são compiladas de fora por padrão, e sem esta
# macro Permission.camera.request() devolve "denied" sem exibir nenhum
# diálogo do sistema — o fluxo morre em `cameraDenied` e parece que o
# usuário recusou.
config.build_settings['GCC_PREPROCESSOR_DEFINITIONS'] ||= [
'$(inherited)',
'PERMISSION_CAMERA=1',
]

# Alguns pods transitivos ainda apontam para 12.0 e falhariam com o ML Kit.
config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '15.5'
end
end
end

O iPhone mais antigo suportado é portanto o 6s / SE (1ª geração). O 5s, o 6 e o 6 Plus param no iOS 12 e não conseguem rodar o ML Kit.

iOS está documentado, ainda não verificado em dispositivo

O caminho do Android é verificado compilando e rodando um APK de release real. A configuração de iOS é derivada dos manifestos de dependências e do app de exemplo deste repositório, e não foi compilada nem executada em um iPhone por nós. Tudo nesta página está apoiado em um podspec ou um plist que lemos; nada está apoiado em uma compilação que tenha acontecido.

Se você encontrar atrito no iOS, avise — preferimos encontrá-lo junto com você a deixar você encontrá-lo sozinho.

3. ios/Runner/Info.plist

<key>NSCameraUsageDescription</key>
<string>Precisamos de acesso à sua câmera para verificar sua identidade
(foto do documento + selfie).</string>

A revisão da App Store rejeita textos genéricos como "Acesso à câmera necessário". Seja específico sobre o caso de uso. Esse texto é lido pelo seu usuário, então localize-o em <idioma>.lproj/InfoPlist.strings — a interface do SDK fala espanhol e português, mas o diálogo de permissão do sistema usa o seu bundle, não o nosso, e sem isso aparecerá em inglês diante de um usuário que fala português.

Manifesto de privacidade. Desde maio de 2024 a Apple exige que todo app iOS publique um PrivacyInfo.xcprivacy declarando o uso de APIs sensíveis. O SDK da Veridia é Dart puro e não publica um framework próprio, então o manifesto pertence ao seu app, não ao SDK — é sua responsabilidade escrevê-lo e mantê-lo atualizado, e a Veridia não fornece um. Veja a documentação de manifesto de privacidade da Apple.

Duas coisas que só mordem na hora do upload

Nenhuma afeta a compilação, e as duas são mais fáceis de resolver agora do que durante um release.

ITSAppUsesNonExemptEncryption. Se estiver ausente, o App Store Connect faz uma pergunta de conformidade de exportação em todo upload, TestFlight incluído. A Veridia é acessada apenas por HTTPS, que é isento:

<key>ITSAppUsesNonExemptEncryption</key>
<false/>

iPad com uma única orientação é rejeitado. Se o seu target declara suporte a iPad (TARGETED_DEVICE_FAMILY = "1,2", o padrão do Flutter) enquanto UISupportedInterfaceOrientations~ipad permite apenas retrato, o upload falha na validação com ITMS-90474: a multitarefa do iPad exige as quatro orientações. Ou você suporta as quatro no iPad, ou tira o iPad da família de dispositivos. Nosso app de exemplo é somente iPhone, de propósito — a interface de captura enquadra um documento e um rosto contra uma guia vertical fixa, e nenhum layout de iPad foi olhado.

NSMicrophoneUsageDescription. O SDK nunca grava áudio — todo CameraController que ele cria passa enableAudio: false. Mas o plugin camera linka a API de áudio da Apple de qualquer forma, e a varredura de upload da Apple lê o binário, não o grafo de chamadas, então o aviso pode aparecer em um app que nunca abre o microfone. Declarar a chave não custa nada: nenhum diálogo é exibido, porque nada pede.

O que roda no dispositivo

  • Permissão de câmera e captura
  • Redução para no máximo 1600×1200 (nunca amplia)
  • Codificação JPEG com qualidade 85
  • Nitidez, por três operadores independentes — variância laplaciana, Tenengrad (Sobel) e Brenner — aprovada por votação, não por um único número
  • Checagem de brilho por luminância média
  • Detecção facial no dispositivo para a selfie, via Google ML Kit
  • A captura de prova de vida ativa, se você habilitou
  • Upload das imagens

Os limiares de nitidez diferem por superfície, e é por isso que uma selfie que reprovaria como documento ainda passa. Um documento precisa passar em dois dos três operadores; uma selfie em apenas um, e em níveis bem mais baixos. Rostos são legitimamente mais suaves que texto impresso, e segurar o telefone com o braço estendido não é o mesmo que fotografar um cartão sobre a mesa — um único limiar para os dois rejeita usuários reais.

Para o documento os operadores ainda são executados de novo sobre a região do documento em vez do frame inteiro. Fazer média sobre uma foto que é majoritariamente mesa dilui um cartão perfeitamente nítido até reprová-lo.

Todo o resto — OCR, parsing de MRZ, comparação facial, detecção de reflexo e moiré, comparação difusa de nome, pontuação de confiança e o veredicto — roda no servidor. O SDK não faz nenhuma alegação própria de antifraude por spoofing; o backend é a fonte da verdade para approved / review / rejected.

Uploads

As imagens não vão para armazenamento de objetos pré-assinado. Cada slot de upload retornado pelo init aponta para um endpoint da Veridia autenticado por X-Veridia-Upload-Token, uma credencial de vida curta por verificação carregada nos próprios headers do slot. O SDK repassa esses headers literalmente, e é isso que faz o upload dar certo.

Isso só importa para você se estiver escrevendo regras de firewall de saída: os bytes vão para api.xxuxe.online, não para um host de armazenamento. Você ainda pode ver "presigned R2 upload" no README do próprio pacote ou no diagrama de fluxo — esse texto está desatualizado; o código está correto.

Fluxo

idle
↓ usuário aperta Iniciar
requestingCamera permissão + câmera traseira

captureDocFront ↔ reviewDocFront refazer / confirmar
↓ (se requireDocBack)
captureDocBack ↔ reviewDocBack refazer / confirmar
↓ (troca para a câmera frontal)
captureSelfie ↔ reviewSelfie refazer / confirmar
↓ (so se activeLiveness: true)
challenge ancora, e depois poses do servidor

uploading PUT de cada imagem

submitting POST /v1/verify/submit

done ✓ onComplete dispara

Erro de qualquer ponto → estado de erro + onError

Verificação de webhook

O pacote exporta um WebhookVerifier, mas um webhook é entregue a um servidor, não a um telefone: um app não tem URL estável e não pode guardar o segredo de assinatura. Verifique webhooks no seu backend com o SDK JavaScript, Python ou PHP, ou com o esquema HMAC documentado na linguagem em que ele rodar.

Para onde ir agora