Skip to main content

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

// ✅ 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
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
);
Ao receber webhooks da Gu1:
  1. Extrair apenas os dados necessários
  2. Atualizar o status do usuário no seu BD
  3. 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');
});
Sempre verifique se os webhooks são da Gu1 usando assinaturas HMAC.Saiba mais sobre segurança de webhooks →

❌ NÃO FAZER

// ❌ 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
  }
});
Por que é perigoso:
  • 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
// ❌ ERRADO: Armazenar URL permanentemente
localStorage.setItem('kycUrl', verificationUrl);

// Mais tarde...
window.open(localStorage.getItem('kycUrl')); // A URL pode ter expirado
Em vez disso: Sempre solicite uma URL nova do seu backend quando necessário.
// ❌ ERRADO: Confiar no cliente para reportar conclusão
function onVerificationComplete() {
  // O usuário poderia falsificar isso
  updateUserStatus('approved');
}
Em vez disso: Confie apenas em webhooks recebidos pelo seu backend.

Integração Móvel

Android (Kotlin)

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()
    }
}
Permissões (AndroidManifest.xml):
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.CAMERA" />

iOS (Swift)

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()
    }
}
Permissões (Info.plist):
<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:
// 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
    }
}

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'
  }
});
Permissões:
  • Adicione permissões de câmera ao AndroidManifest.xml e Info.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),
    );
  }
}
Dependências (pubspec.yaml):
dependencies:
  webview_flutter: ^4.4.0
  http: ^1.1.0
Permissões:
  • 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
  });
});
Polling do frontend (todas as plataformas):
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
}

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

EventoQuando DisparadoAção
kyc.validation_approvedUsuário verificado com sucessoAtualizar status do usuário para verified, habilitar recursos
kyc.validation_rejectedVerificação falhouAtualizar status para rejected, opcionalmente permitir nova tentativa
kyc.validation_abandonedUsuário saiu sem concluirMarcar como incomplete, enviar e-mail de lembrete
kyc.validation_expiredSessão expirouPermitir ao usuário solicitar nova URL
Manipulador mínimo de webhook:
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 });

  // Chamar API da Gu1
  const validation = await gu1.post('/kyc/validations', {
    entity: {
      externalId: user.id,
      name: user.name,
      taxId: user.taxId,
      type: 'person'
    },
    workflowId: process.env.GU1_WORKFLOW_ID
  });

  // Armazenar ID de validação
  await db.users.update({
    where: { id: userId },
    data: {
      kycValidationId: validation.id,
      kycStatus: 'pending'
    }
  });

  res.json({
    verificationUrl: validation.validation_url
  });
});

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
});
No modo sandbox:
  • 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

Possíveis causas:
  • URL expirou (gerar uma nova)
  • JavaScript desabilitado no WebView
  • Problemas de conectividade de rede
Soluções:
  • Habilitar JavaScript: settings.javaScriptEnabled = true
  • Verificar se a URL é válida e não expirou
  • Testar URL em navegador normal primeiro
Possíveis causas:
  • Permissões de câmera faltando
  • Reprodução de mídia requer gesto do usuário
Soluções:Android:
<uses-permission android:name="android.permission.CAMERA" />
iOS:
configuration.mediaTypesRequiringUserActionForPlayback = []
Web:
<iframe allow="camera; microphone" />
Possíveis causas:
  • Webhook não recebido pelo backend
  • Verificação de assinatura de webhook falhou
  • Banco de dados não está sendo atualizado
Soluções:
  • 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
Possíveis causas:
  • Iluminação fraca para fotos de documentos
  • Tipo de documento não suportado
  • Problemas técnicos
Soluções:
  • 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