Visão Geral
Este guia mostra como integrar o fluxo de verificação KYC da Gu1 em seus aplicativos móveis (Android, iOS, React Native, Flutter) e aplicações web (React, Vue, vanilla JS). A verificação é executada em um WebView/iframe apontando para uma URL segura gerada pelo seu backend.Este guia assume que você já configurou a integração do seu backend com a API da Gu1. Caso contrário, comece com a Visão Geral da API KYC.
Visão Geral da Arquitetura
Pontos-Chave:- Seu backend gera a URL de verificação (nunca exponha chaves de API no cliente)
- Seu frontend abre a URL no WebView/iframe
- A Gu1 envia webhooks para seu backend quando a verificação é concluída
- Seu frontend faz polling no seu backend para detectar a conclusão
Melhores Práticas de Segurança
CRÍTICO: Nunca exponha as credenciais da API da Gu1 ou IDs de workflow no código do lado do cliente. Sempre gere URLs de verificação a partir do seu backend seguro.
✅ FAZER
Gerar URLs sob demanda a partir do seu backend
Gerar URLs sob demanda a partir do seu backend
// ✅ CORRETO: Frontend solicita URL do SEU backend
const response = await fetch('https://yourapi.com/kyc/start', {
method: 'POST',
headers: {
'Authorization': `Bearer ${userToken}`, // Sua autenticação
'Content-Type': 'application/json'
},
body: JSON.stringify({
userId: '123'
})
});
const { verificationUrl } = await response.json();
// Abrir verificationUrl no WebView/iframe
Implementar TTL curto para URLs
Implementar TTL curto para URLs
Gere URLs de verificação que expirem rapidamente (15-30 minutos). Se o usuário não iniciar dentro desse tempo, gere uma nova.
// No seu backend
const validation = await gu1.createValidation({
// ... dados da entidade
});
// Armazenar com expiração
await redis.setex(
`kyc:${userId}`,
1800, // TTL de 30 minutos
validation.validation_url
);
Usar padrão Process-and-Purge
Usar padrão Process-and-Purge
Ao receber webhooks da Gu1:
- Extrair apenas os dados necessários
- Atualizar o status do usuário no seu BD
- Não armazenar dados sensíveis de verificação a longo prazo
// No seu manipulador de webhook
app.post('/webhooks/kyc', async (req, res) => {
const { event, payload } = req.body;
if (event === 'kyc.validation_approved') {
// Extrair apenas o que você precisa
await db.users.update({
where: { externalId: payload.entity.externalId },
data: {
kycStatus: 'approved',
kycVerifiedAt: new Date(),
// Não armazenar extractedData a menos que seja necessário
}
});
}
res.status(200).send('OK');
});
Validar assinaturas de webhook
Validar assinaturas de webhook
Sempre verifique se os webhooks são da Gu1 usando assinaturas HMAC.Saiba mais sobre segurança de webhooks →
❌ NÃO FAZER
Nunca codificar chaves de API ou IDs de workflow
Nunca codificar chaves de API ou IDs de workflow
// ❌ ERRADO: Expondo credenciais no código do cliente
const response = await fetch('https://api.gu1.ai/kyc/validations', {
headers: {
'x-api-key': 'gsk_live_abc123...', // NUNCA FAÇA ISSO
'x-workflow-id': 'workflow-id' // NUNCA FAÇA ISSO
}
});
- Chaves de API expostas em bundles de app podem ser extraídas
- Qualquer pessoa pode criar validações em seu nome
- Impossível rotacionar chaves comprometidas sem atualizações do app
Não reutilizar URLs de verificação
Não reutilizar URLs de verificação
// ❌ ERRADO: Armazenar URL permanentemente
localStorage.setItem('kycUrl', verificationUrl);
// Mais tarde...
window.open(localStorage.getItem('kycUrl')); // A URL pode ter expirado
Não confiar em validação do lado do cliente
Não confiar em validação do lado do cliente
// ❌ ERRADO: Confiar no cliente para reportar conclusão
function onVerificationComplete() {
// O usuário poderia falsificar isso
updateUserStatus('approved');
}
Integração Móvel
Android (Kotlin)
- Standard Android
- Jetpack Compose
import android.webkit.WebView
import android.webkit.WebViewClient
import androidx.appcompat.app.AppCompatActivity
class KYCActivity : AppCompatActivity() {
private lateinit var webView: WebView
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
webView = WebView(this).apply {
settings.apply {
javaScriptEnabled = true
domStorageEnabled = true
mediaPlaybackRequiresUserGesture = false // Para acesso à câmera
}
webViewClient = object : WebViewClient() {
override fun onPageFinished(view: WebView?, url: String?) {
// Página carregada
}
}
}
setContentView(webView)
// Solicitar URL de verificação do SEU backend
loadVerificationUrl()
}
private fun loadVerificationUrl() {
lifecycleScope.launch {
try {
val url = apiClient.requestKYCUrl(userId)
webView.loadUrl(url)
// Iniciar polling para conclusão
startStatusPolling()
} catch (e: Exception) {
// Tratar erro
showError("Failed to load verification")
}
}
}
private fun startStatusPolling() {
lifecycleScope.launch {
while (true) {
delay(3000) // Polling a cada 3 segundos
val status = apiClient.getKYCStatus(userId)
when (status) {
"approved" -> {
showSuccess()
finish()
return@launch
}
"rejected" -> {
showRejected()
finish()
return@launch
}
"pending", "in_progress" -> {
// Continuar polling
}
}
}
}
}
override fun onDestroy() {
webView.destroy()
super.onDestroy()
}
}
import androidx.compose.runtime.*
import androidx.compose.ui.viewinterop.AndroidView
import android.webkit.WebView
@Composable
fun KYCVerificationScreen(
userId: String,
onComplete: (status: String) -> Unit
) {
var verificationUrl by remember { mutableStateOf<String?>(null) }
var isLoading by remember { mutableStateOf(true) }
// Carregar URL de verificação
LaunchedEffect(userId) {
try {
val url = apiClient.requestKYCUrl(userId)
verificationUrl = url
isLoading = false
} catch (e: Exception) {
// Tratar erro
}
}
// Polling para status
LaunchedEffect(userId) {
while (true) {
delay(3000) // Polling a cada 3 segundos
val status = apiClient.getKYCStatus(userId)
when (status) {
"approved", "rejected" -> {
onComplete(status)
return@LaunchedEffect
}
}
}
}
if (isLoading) {
CircularProgressIndicator()
} else {
verificationUrl?.let { url ->
AndroidView(
factory = { context ->
WebView(context).apply {
settings.apply {
javaScriptEnabled = true
domStorageEnabled = true
mediaPlaybackRequiresUserGesture = false
}
loadUrl(url)
}
}
)
}
}
}
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.CAMERA" />
iOS (Swift)
- UIKit
- SwiftUI
import UIKit
import WebKit
class KYCViewController: UIViewController {
private var webView: WKWebView!
private let userId: String
private var pollingTimer: Timer?
init(userId: String) {
self.userId = userId
super.init(nibName: nil, bundle: nil)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func viewDidLoad() {
super.viewDidLoad()
// Configurar WebView
let configuration = WKWebViewConfiguration()
configuration.allowsInlineMediaPlayback = true
configuration.mediaTypesRequiringUserActionForPlayback = []
webView = WKWebView(frame: view.bounds, configuration: configuration)
webView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
view.addSubview(webView)
// Carregar URL de verificação
loadVerificationURL()
}
private func loadVerificationURL() {
Task {
do {
let urlString = try await apiClient.requestKYCURL(userId: userId)
guard let url = URL(string: urlString) else { return }
let request = URLRequest(url: url)
webView.load(request)
// Iniciar polling
startStatusPolling()
} catch {
showError("Failed to load verification")
}
}
}
private func startStatusPolling() {
pollingTimer = Timer.scheduledTimer(withTimeInterval: 3.0, repeats: true) { [weak self] _ in
guard let self = self else { return }
Task {
do {
let status = try await self.apiClient.getKYCStatus(userId: self.userId)
switch status {
case "approved":
self.handleApproved()
case "rejected":
self.handleRejected()
default:
break // Continuar polling
}
} catch {
// Tratar erro
}
}
}
}
private func handleApproved() {
pollingTimer?.invalidate()
// Mostrar sucesso e fechar
dismiss(animated: true)
}
private func handleRejected() {
pollingTimer?.invalidate()
// Mostrar mensagem de rejeição
}
deinit {
pollingTimer?.invalidate()
}
}
import SwiftUI
import WebKit
struct KYCVerificationView: View {
let userId: String
@State private var verificationURL: URL?
@State private var isLoading = true
@State private var status: String = "pending"
@Environment(\.dismiss) var dismiss
var body: some View {
ZStack {
if isLoading {
ProgressView("Loading verification...")
} else if let url = verificationURL {
WebView(url: url)
}
}
.onAppear {
loadVerificationURL()
startStatusPolling()
}
}
private func loadVerificationURL() {
Task {
do {
let urlString = try await apiClient.requestKYCURL(userId: userId)
verificationURL = URL(string: urlString)
isLoading = false
} catch {
// Tratar erro
}
}
}
private func startStatusPolling() {
Task {
while status == "pending" || status == "in_progress" {
try? await Task.sleep(nanoseconds: 3_000_000_000) // 3 segundos
do {
status = try await apiClient.getKYCStatus(userId: userId)
if status == "approved" || status == "rejected" {
dismiss()
return
}
} catch {
// Tratar erro
}
}
}
}
}
struct WebView: UIViewRepresentable {
let url: URL
func makeUIView(context: Context) -> WKWebView {
let configuration = WKWebViewConfiguration()
configuration.allowsInlineMediaPlayback = true
configuration.mediaTypesRequiringUserActionForPlayback = []
return WKWebView(frame: .zero, configuration: configuration)
}
func updateUIView(_ webView: WKWebView, context: Context) {
let request = URLRequest(url: url)
webView.load(request)
}
}
<key>NSCameraUsageDescription</key>
<string>We need camera access for identity verification</string>
<key>NSPhotoLibraryUsageDescription</key>
<string>We need photo library access for document upload</string>
Kotlin Multiplatform (KMP)
Perfeito para compartilhar lógica entre Android e iOS:- Common Code
- Android Implementation
- iOS Implementation
// commonMain/KYCManager.kt
expect class KYCManager {
fun openVerification(url: String)
fun startStatusPolling(userId: String, onComplete: (String) -> Unit)
}
// Lógica de negócio compartilhada
class KYCCoordinator(
private val apiClient: ApiClient,
private val kycManager: KYCManager
) {
suspend fun startVerification(userId: String) {
try {
val url = apiClient.requestKYCUrl(userId)
kycManager.openVerification(url)
kycManager.startStatusPolling(userId) { status ->
when (status) {
"approved" -> handleApproved()
"rejected" -> handleRejected()
}
}
} catch (e: Exception) {
// Tratar erro
}
}
private fun handleApproved() {
// Lógica de aprovação compartilhada
}
private fun handleRejected() {
// Lógica de rejeição compartilhada
}
}
// androidMain/KYCManager.kt
actual class KYCManager(private val activity: Activity) {
actual fun openVerification(url: String) {
val intent = Intent(activity, KYCWebViewActivity::class.java).apply {
putExtra("url", url)
}
activity.startActivity(intent)
}
actual fun startStatusPolling(userId: String, onComplete: (String) -> Unit) {
// Usar coroutines para polling
CoroutineScope(Dispatchers.Main).launch {
while (true) {
delay(3000)
val status = apiClient.getKYCStatus(userId)
if (status in listOf("approved", "rejected")) {
onComplete(status)
break
}
}
}
}
}
// iosMain/KYCManager.kt
actual class KYCManager {
actual fun openVerification(url: String) {
let vc = KYCViewController(urlString: url)
// Apresentar view controller
UIApplication.shared.windows.first?.rootViewController?
.present(vc, animated: true)
}
actual fun startStatusPolling(userId: String, onComplete: (String) -> Unit) {
Timer.scheduledTimer(withTimeInterval: 3.0, repeats: true) { timer in
Task {
let status = try await apiClient.getKYCStatus(userId: userId)
if status == "approved" || status == "rejected" {
onComplete(status)
timer.invalidate()
}
}
}
}
}
React Native
import React, { useEffect, useState } from 'react';
import { View, ActivityIndicator, StyleSheet } from 'react-native';
import { WebView } from 'react-native-webview';
interface KYCVerificationProps {
userId: string;
onComplete: (status: string) => void;
}
export const KYCVerification: React.FC<KYCVerificationProps> = ({
userId,
onComplete
}) => {
const [verificationUrl, setVerificationUrl] = useState<string | null>(null);
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
loadVerificationUrl();
const pollInterval = startStatusPolling();
return () => clearInterval(pollInterval);
}, [userId]);
const loadVerificationUrl = async () => {
try {
const response = await fetch(`https://yourapi.com/kyc/start`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${userToken}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({ userId })
});
const { verificationUrl } = await response.json();
setVerificationUrl(verificationUrl);
setIsLoading(false);
} catch (error) {
console.error('Failed to load KYC URL:', error);
setIsLoading(false);
}
};
const startStatusPolling = () => {
return setInterval(async () => {
try {
const response = await fetch(`https://yourapi.com/kyc/status/${userId}`, {
headers: {
'Authorization': `Bearer ${userToken}`
}
});
const { status } = await response.json();
if (status === 'approved' || status === 'rejected') {
onComplete(status);
}
} catch (error) {
console.error('Polling error:', error);
}
}, 3000); // Polling a cada 3 segundos
};
if (isLoading) {
return (
<View style={styles.loading}>
<ActivityIndicator size="large" />
</View>
);
}
if (!verificationUrl) {
return (
<View style={styles.error}>
<Text>Failed to load verification</Text>
</View>
);
}
return (
<WebView
source={{ uri: verificationUrl }}
style={styles.webview}
javaScriptEnabled={true}
domStorageEnabled={true}
mediaPlaybackRequiresUserAction={false} // Para câmera
allowsInlineMediaPlayback={true}
/>
);
};
const styles = StyleSheet.create({
webview: {
flex: 1
},
loading: {
flex: 1,
justifyContent: 'center',
alignItems: 'center'
},
error: {
flex: 1,
justifyContent: 'center',
alignItems: 'center'
}
});
- Adicione permissões de câmera ao
AndroidManifest.xmleInfo.plist - Instalar:
npm install react-native-webview
Flutter
import 'package:flutter/material.dart';
import 'package:webview_flutter/webview_flutter.dart';
import 'dart:async';
class KYCVerificationScreen extends StatefulWidget {
final String userId;
final Function(String) onComplete;
const KYCVerificationScreen({
Key? key,
required this.userId,
required this.onComplete,
}) : super(key: key);
@override
_KYCVerificationScreenState createState() => _KYCVerificationScreenState();
}
class _KYCVerificationScreenState extends State<KYCVerificationScreen> {
String? verificationUrl;
bool isLoading = true;
Timer? pollingTimer;
late WebViewController controller;
@override
void initState() {
super.initState();
_loadVerificationUrl();
_startStatusPolling();
}
@override
void dispose() {
pollingTimer?.cancel();
super.dispose();
}
Future<void> _loadVerificationUrl() async {
try {
final response = await http.post(
Uri.parse('https://yourapi.com/kyc/start'),
headers: {
'Authorization': 'Bearer $userToken',
'Content-Type': 'application/json',
},
body: jsonEncode({'userId': widget.userId}),
);
if (response.statusCode == 200) {
final data = jsonDecode(response.body);
setState(() {
verificationUrl = data['verificationUrl'];
isLoading = false;
});
// Inicializar WebView controller
controller = WebViewController()
..setJavaScriptMode(JavaScriptMode.unrestricted)
..setBackgroundColor(const Color(0x00000000))
..loadRequest(Uri.parse(verificationUrl!));
}
} catch (e) {
print('Error loading KYC URL: $e');
setState(() => isLoading = false);
}
}
void _startStatusPolling() {
pollingTimer = Timer.periodic(Duration(seconds: 3), (timer) async {
try {
final response = await http.get(
Uri.parse('https://yourapi.com/kyc/status/${widget.userId}'),
headers: {'Authorization': 'Bearer $userToken'},
);
if (response.statusCode == 200) {
final data = jsonDecode(response.body);
final status = data['status'];
if (status == 'approved' || status == 'rejected') {
timer.cancel();
widget.onComplete(status);
}
}
} catch (e) {
print('Polling error: $e');
}
});
}
@override
Widget build(BuildContext context) {
if (isLoading) {
return Scaffold(
body: Center(
child: CircularProgressIndicator(),
),
);
}
if (verificationUrl == null) {
return Scaffold(
body: Center(
child: Text('Failed to load verification'),
),
);
}
return Scaffold(
appBar: AppBar(
title: Text('Identity Verification'),
),
body: WebViewWidget(controller: controller),
);
}
}
dependencies:
webview_flutter: ^4.4.0
http: ^1.1.0
- Android: Adicione permissão de câmera ao
AndroidManifest.xml - iOS: Adicione descrição de uso de câmera ao
Info.plist
Integração Web
React
import React, { useEffect, useState, useRef } from 'react';
interface KYCVerificationProps {
userId: string;
onComplete: (status: string) => void;
}
export const KYCVerification: React.FC<KYCVerificationProps> = ({
userId,
onComplete
}) => {
const [verificationUrl, setVerificationUrl] = useState<string | null>(null);
const [isLoading, setIsLoading] = useState(true);
const iframeRef = useRef<HTMLIFrameElement>(null);
const pollingInterval = useRef<NodeJS.Timeout | null>(null);
useEffect(() => {
loadVerificationUrl();
startStatusPolling();
return () => {
if (pollingInterval.current) {
clearInterval(pollingInterval.current);
}
};
}, [userId]);
const loadVerificationUrl = async () => {
try {
const response = await fetch('/api/kyc/start', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${getAuthToken()}`
},
body: JSON.stringify({ userId })
});
const { verificationUrl } = await response.json();
setVerificationUrl(verificationUrl);
setIsLoading(false);
} catch (error) {
console.error('Failed to load KYC URL:', error);
setIsLoading(false);
}
};
const startStatusPolling = () => {
pollingInterval.current = setInterval(async () => {
try {
const response = await fetch(`/api/kyc/status/${userId}`, {
headers: {
'Authorization': `Bearer ${getAuthToken()}`
}
});
const { status } = await response.json();
if (status === 'approved' || status === 'rejected') {
if (pollingInterval.current) {
clearInterval(pollingInterval.current);
}
onComplete(status);
}
} catch (error) {
console.error('Polling error:', error);
}
}, 3000); // Polling a cada 3 segundos
};
if (isLoading) {
return (
<div className="flex items-center justify-center h-screen">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-500"></div>
</div>
);
}
if (!verificationUrl) {
return (
<div className="flex items-center justify-center h-screen">
<p className="text-red-500">Failed to load verification</p>
</div>
);
}
return (
<div className="w-full h-screen">
<iframe
ref={iframeRef}
src={verificationUrl}
className="w-full h-full border-0"
allow="camera; microphone"
title="KYC Verification"
/>
</div>
);
};
Vue 3
<template>
<div class="kyc-verification">
<div v-if="isLoading" class="loading">
<div class="spinner"></div>
<p>Loading verification...</p>
</div>
<div v-else-if="error" class="error">
<p>{{ error }}</p>
</div>
<iframe
v-else-if="verificationUrl"
:src="verificationUrl"
class="verification-iframe"
allow="camera; microphone"
title="KYC Verification"
></iframe>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted, onUnmounted } from 'vue';
interface Props {
userId: string;
}
const props = defineProps<Props>();
const emit = defineEmits<{
complete: [status: string];
}>();
const verificationUrl = ref<string | null>(null);
const isLoading = ref(true);
const error = ref<string | null>(null);
let pollingInterval: number | null = null;
onMounted(async () => {
await loadVerificationUrl();
startStatusPolling();
});
onUnmounted(() => {
if (pollingInterval) {
clearInterval(pollingInterval);
}
});
async function loadVerificationUrl() {
try {
const response = await fetch('/api/kyc/start', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${getAuthToken()}`
},
body: JSON.stringify({ userId: props.userId })
});
if (!response.ok) throw new Error('Failed to load verification URL');
const data = await response.json();
verificationUrl.value = data.verificationUrl;
} catch (err) {
error.value = 'Failed to load verification';
console.error(err);
} finally {
isLoading.value = false;
}
}
function startStatusPolling() {
pollingInterval = setInterval(async () => {
try {
const response = await fetch(`/api/kyc/status/${props.userId}`, {
headers: {
'Authorization': `Bearer ${getAuthToken()}`
}
});
const { status } = await response.json();
if (status === 'approved' || status === 'rejected') {
if (pollingInterval) clearInterval(pollingInterval);
emit('complete', status);
}
} catch (err) {
console.error('Polling error:', err);
}
}, 3000);
}
function getAuthToken(): string {
// Sua lógica de token de autenticação
return localStorage.getItem('authToken') || '';
}
</script>
<style scoped>
.kyc-verification {
width: 100%;
height: 100vh;
}
.loading,
.error {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100%;
}
.spinner {
width: 50px;
height: 50px;
border: 4px solid #f3f3f3;
border-top: 4px solid #3498db;
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.verification-iframe {
width: 100%;
height: 100%;
border: none;
}
.error {
color: #e74c3c;
}
</style>
Plain HTML/JavaScript (Vanilla)
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>KYC Verification</title>
<style>
body, html {
margin: 0;
padding: 0;
height: 100%;
font-family: Arial, sans-serif;
}
#loading {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100vh;
}
.spinner {
width: 50px;
height: 50px;
border: 4px solid #f3f3f3;
border-top: 4px solid #3498db;
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
#verification-iframe {
width: 100%;
height: 100vh;
border: none;
display: none;
}
#error {
display: none;
text-align: center;
padding: 20px;
color: #e74c3c;
}
</style>
</head>
<body>
<div id="loading">
<div class="spinner"></div>
<p>Loading verification...</p>
</div>
<div id="error">
<p>Failed to load verification. Please try again.</p>
</div>
<iframe
id="verification-iframe"
allow="camera; microphone"
title="KYC Verification"
></iframe>
<script>
const userId = 'USER_ID_HERE'; // Substituir com ID de usuário real
let pollingInterval;
async function loadVerificationUrl() {
try {
const response = await fetch('/api/kyc/start', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${getAuthToken()}`
},
body: JSON.stringify({ userId })
});
if (!response.ok) throw new Error('Failed to load');
const { verificationUrl } = await response.json();
// Mostrar iframe e ocultar loading
document.getElementById('loading').style.display = 'none';
const iframe = document.getElementById('verification-iframe');
iframe.src = verificationUrl;
iframe.style.display = 'block';
// Iniciar polling para status
startStatusPolling();
} catch (error) {
console.error('Error:', error);
document.getElementById('loading').style.display = 'none';
document.getElementById('error').style.display = 'block';
}
}
function startStatusPolling() {
pollingInterval = setInterval(async () => {
try {
const response = await fetch(`/api/kyc/status/${userId}`, {
headers: {
'Authorization': `Bearer ${getAuthToken()}`
}
});
const { status } = await response.json();
if (status === 'approved') {
clearInterval(pollingInterval);
handleApproved();
} else if (status === 'rejected') {
clearInterval(pollingInterval);
handleRejected();
}
} catch (error) {
console.error('Polling error:', error);
}
}, 3000); // Polling a cada 3 segundos
}
function handleApproved() {
alert('Verification approved!');
window.location.href = '/dashboard'; // Redirecionar
}
function handleRejected() {
alert('Verification rejected. Please try again.');
window.location.href = '/kyc-retry'; // Redirecionar
}
function getAuthToken() {
// Obter token de autenticação do localStorage, cookie, etc.
return localStorage.getItem('authToken');
}
// Iniciar carregamento quando a página carregar
window.addEventListener('DOMContentLoaded', loadVerificationUrl);
// Limpeza ao descarregar a página
window.addEventListener('beforeunload', () => {
if (pollingInterval) {
clearInterval(pollingInterval);
}
});
</script>
</body>
</html>
Detecção de Conclusão do Fluxo
O WebView/iframe não pode notificar diretamente seu app quando a verificação é concluída. A UI de verificação da Gu1 é executada em isolamento por razões de segurança.
Padrão Recomendado: Polling ao Backend
Seu frontend faz polling no seu backend, que recebe webhooks da Gu1: Exemplo de endpoint do backend:// Exemplo Node.js/Express
app.get('/api/kyc/status/:userId', authenticate, async (req, res) => {
const { userId } = req.params;
const user = await db.users.findOne({ id: userId });
res.json({
status: user.kycStatus, // 'pending', 'in_progress', 'approved', 'rejected'
verifiedAt: user.kycVerifiedAt,
lastUpdated: user.kycLastUpdated
});
});
- JavaScript
- Kotlin
- Swift
async function pollStatus(userId) {
const interval = setInterval(async () => {
const response = await fetch(`/api/kyc/status/${userId}`);
const { status } = await response.json();
if (status === 'approved' || status === 'rejected') {
clearInterval(interval);
handleComplete(status);
}
}, 3000); // 3 segundos
}
fun pollStatus(userId: String) {
lifecycleScope.launch {
while (true) {
delay(3000)
val status = apiClient.getKYCStatus(userId)
if (status in listOf("approved", "rejected")) {
handleComplete(status)
break
}
}
}
}
func pollStatus(userId: String) {
Timer.scheduledTimer(withTimeInterval: 3.0, repeats: true) { timer in
Task {
let status = try await apiClient.getKYCStatus(userId: userId)
if status == "approved" || status == "rejected" {
timer.invalidate()
handleComplete(status)
}
}
}
}
Alternativa: WebSockets (Avançado)
Para melhor UX, use WebSockets para enviar atualizações em vez de polling:// Backend (exemplo Socket.IO)
io.on('connection', (socket) => {
socket.on('subscribe-kyc', (userId) => {
socket.join(`kyc:${userId}`);
});
});
// Quando o webhook chega
app.post('/webhooks/kyc', async (req, res) => {
const { event, payload } = req.body;
// Atualizar banco de dados
await updateUserStatus(payload);
// Notificar clientes conectados
io.to(`kyc:${payload.entity.externalId}`).emit('kyc-update', {
status: payload.status
});
res.status(200).send('OK');
});
// Frontend
const socket = io();
socket.emit('subscribe-kyc', userId);
socket.on('kyc-update', ({ status }) => {
handleComplete(status);
});
Manipulação de Webhooks
Seu backend recebe webhooks da Gu1 quando a verificação é concluída.Guia de Integração de Webhooks
Veja o guia completo de integração de webhooks com exemplos de código, segurança e estruturas de payload
Eventos-Chave de Webhook
| Evento | Quando Disparado | Ação |
|---|---|---|
kyc.validation_approved | Usuário verificado com sucesso | Atualizar status do usuário para verified, habilitar recursos |
kyc.validation_rejected | Verificação falhou | Atualizar status para rejected, opcionalmente permitir nova tentativa |
kyc.validation_abandoned | Usuário saiu sem concluir | Marcar como incomplete, enviar e-mail de lembrete |
kyc.validation_expired | Sessão expirou | Permitir ao usuário solicitar nova URL |
app.post('/webhooks/gu1/kyc', async (req, res) => {
// Verificar assinatura (ver guia de webhooks)
const { event, payload } = req.body;
const userId = payload.entity.externalId;
switch (event) {
case 'kyc.validation_approved':
await db.users.update({
where: { id: userId },
data: {
kycStatus: 'approved',
kycVerifiedAt: new Date(),
isVerified: true
}
});
break;
case 'kyc.validation_rejected':
await db.users.update({
where: { id: userId },
data: {
kycStatus: 'rejected',
isVerified: false
}
});
break;
}
res.status(200).send('OK');
});
Exemplo de Fluxo Completo
Vamos ver um exemplo completo de ponta a ponta:1. Backend: Gerar URL
// POST /api/kyc/start
app.post('/api/kyc/start', authenticate, async (req, res) => {
const { userId } = req.body;
const user = await db.users.findOne({ id: userId });
// Garantir que a entidade pessoa existe na Gu1 (criar ou obter por externalId/taxId)
let entityId = user.gu1EntityId;
if (!entityId) {
const entityRes = await gu1.post('/entities', {
type: 'person',
externalId: user.id,
name: user.name,
taxId: user.taxId,
countryCode: user.countryCode || 'US'
});
entityId = entityRes.entity?.id ?? entityRes.id;
await db.users.update({ where: { id: userId }, data: { gu1EntityId: entityId } });
}
// Criar validação KYC (entityId + integrationCode)
const validation = await gu1.post('/kyc/validations', {
entityId,
integrationCode: 'global_gueno_validation_kyc'
});
await db.users.update({
where: { id: userId },
data: {
kycValidationId: validation.id,
kycStatus: 'pending'
}
});
res.json({
verificationUrl: validation.providerSessionUrl
});
});
2. Frontend: Abrir Verificação
// Usuário clica em "Verify Identity"
async function startVerification() {
const { verificationUrl } = await fetch('/api/kyc/start', {
method: 'POST',
body: JSON.stringify({ userId: currentUser.id })
}).then(r => r.json());
// Abrir no WebView/iframe
openVerificationUI(verificationUrl);
// Iniciar polling
pollForCompletion();
}
3. Backend: Receber Webhook
// POST /webhooks/gu1/kyc
app.post('/webhooks/gu1/kyc', async (req, res) => {
const { event, payload } = req.body;
if (event === 'kyc.validation_approved') {
await db.users.update({
where: { externalId: payload.entity.externalId },
data: {
kycStatus: 'approved',
isVerified: true
}
});
}
res.status(200).send('OK');
});
4. Frontend: Detectar Conclusão
async function pollForCompletion() {
const interval = setInterval(async () => {
const { status } = await fetch(`/api/kyc/status/${userId}`)
.then(r => r.json());
if (status === 'approved') {
clearInterval(interval);
showSuccess('Identity verified!');
} else if (status === 'rejected') {
clearInterval(interval);
showError('Verification failed');
}
}, 3000);
}
Testes
Ambiente Sandbox
Use o modo sandbox para testes:// Backend
const gu1 = new Gu1Client({
apiKey: process.env.GU1_SANDBOX_API_KEY,
environment: 'sandbox' // Usar sandbox para testes
});
- Nenhum documento real é necessário
- Você pode simular diferentes resultados
- Webhooks ainda disparam normalmente
Testando Diferentes Resultados
Para testar cenários de rejeição/expiração, use diferentes dados de teste no modo sandbox.Solução de Problemas
WebView mostra página em branco
WebView mostra página em branco
Possíveis causas:
- URL expirou (gerar uma nova)
- JavaScript desabilitado no WebView
- Problemas de conectividade de rede
- Habilitar JavaScript:
settings.javaScriptEnabled = true - Verificar se a URL é válida e não expirou
- Testar URL em navegador normal primeiro
Câmera não funciona no WebView
Câmera não funciona no WebView
Possíveis causas:iOS:Web:
- Permissões de câmera faltando
- Reprodução de mídia requer gesto do usuário
<uses-permission android:name="android.permission.CAMERA" />
configuration.mediaTypesRequiringUserActionForPlayback = []
<iframe allow="camera; microphone" />
Polling nunca detecta conclusão
Polling nunca detecta conclusão
Possíveis causas:
- Webhook não recebido pelo backend
- Verificação de assinatura de webhook falhou
- Banco de dados não está sendo atualizado
- Verificar logs de webhook no painel da Gu1
- Verificar implementação de assinatura de webhook
- Adicionar logging ao manipulador de webhook
- Testar endpoint de webhook manualmente
Usuário preso na verificação
Usuário preso na verificação
Possíveis causas:
- Iluminação fraca para fotos de documentos
- Tipo de documento não suportado
- Problemas técnicos
- Fornecer instruções claras antes de começar
- Mostrar exemplos de fotos boas vs ruins
- Implementar timeout (15-20 minutos)
- Permitir ao usuário sair e tentar novamente
Resumo de Melhores Práticas
Segurança em Primeiro Lugar
- Nunca expor chaves de API no cliente
- Sempre gerar URLs a partir do backend
- Verificar assinaturas de webhook
- Usar HTTPS em todos os lugares
Experiência do Usuário
- Mostrar estados de carregamento
- Fornecer instruções claras
- Tratar erros com elegância
- Permitir nova tentativa em caso de falha
Confiabilidade
- Implementar polling com intervalos razoáveis
- Tratar falhas de rede
- Definir timeouts apropriados
- Testar todas as plataformas completamente
Conformidade
- Não armazenar dados sensíveis de verificação
- Usar padrão Process-and-Purge
- Respeitar políticas de retenção de dados
- Documentar sua integração
Próximos Passos
Visão Geral da API KYC
Aprenda sobre a API KYC e integração do backend
Integração de Webhooks
Configure webhooks para receber resultados de verificação
Guia de Fluxo Completo
Veja o fluxo completo de verificação KYC
Guia de Segurança
Implemente segurança de webhooks e verificação HMAC