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+.
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
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
| Campo | Obrigatório | Padrão | Notas |
|---|---|---|---|
publishableKey | sim | — | qv_pub_... ou qv_pubt_... |
apiBase | não | https://api.xxuxe.online | Sobrescreva apenas para uma implantação regional |
userRef | não | — | Seu id de usuário. Ecoado de volta no webhook |
country | não | — | ISO 3166-1 alpha-2 (GT, MX, BR, …) |
documentType | não | — | Se for null, o SDK pergunta ao usuário |
submittedFullName | não | — | Comparado de forma difusa com o documento no servidor |
requireDocBack | não | segue documentType | Veja abaixo |
locale | não | VeridiaLocale.en | en / es / pt |
activeLiveness | não | false | Roda 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 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.passport→false. Passaportes são de página única.- Qualquer outro
documentType→true. documentTypenull →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.
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 Dart | wireValue | Quando |
|---|---|---|
cameraDenied | camera_denied | Usuário recusou a permissão de câmera |
cameraUnavailable | camera_unavailable | Nenhuma câmera utilizável no dispositivo |
noFaceInSelfie | no_face_in_selfie | A detecção facial no dispositivo não achou nada |
blurryImage | blurry_image | A checagem de nitidez falhou |
uploadFailed | upload_failed | Um PUT não teve sucesso |
apiUnreachable | api_unreachable | Falha de rede ao alcançar a Veridia |
invalidApiKey | invalid_api_key | Chave ausente, malformada ou revogada |
insufficientCredits | insufficient_credits | Saldo do tenant esgotado (HTTP 402) |
rateLimited | rate_limited | HTTP 429 |
userCancelled | user_cancelled | Usuário desistiu do fluxo |
internalError | internal_error | Qualquer 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 Runner → Build Settings → iOS Deployment Target → 15.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.
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
- Webhooks — como o veredicto de fato chega até você
- Widget — o equivalente deste SDK no navegador
- SDK JavaScript · SDK Python · SDK PHP