Esta guía te muestra cómo integrar el flujo de verificación KYC de Gu1 en tus aplicaciones móviles (Android, iOS, React Native, Flutter) y aplicaciones web (React, Vue, vanilla JS). La verificación se ejecuta en un WebView/iframe que apunta a una URL segura generada por tu backend.
Esta guía asume que ya has configurado la integración de tu backend con la API de Gu1. Si no es así, comienza con la Descripción General de la API KYC.
CRÍTICO: Nunca expongas las credenciales de la API de Gu1 o los IDs de workflow en tu código del lado del cliente. Siempre genera las URLs de verificación desde tu backend seguro.
Genera URLs de verificación que expiren rápidamente (15-30 minutos). Si el usuario no inicia dentro de ese tiempo, genera una nueva.
Copy
Ask AI
// En tu backendconst validation = await gu1.createValidation({ // ... datos de entidad});// Almacenar con expiraciónawait redis.setex( `kyc:${userId}`, 1800, // TTL de 30 minutos validation.validation_url);
Usar patrón Process-and-Purge
Al recibir webhooks de Gu1:
Extraer solo los datos necesarios
Actualizar el estado del usuario en tu BD
No almacenar datos sensibles de verificación a largo plazo
Copy
Ask AI
// En tu manejador de webhookapp.post('/webhooks/kyc', async (req, res) => { const { event, payload } = req.body; if (event === 'kyc.validation_approved') { // Extraer solo lo que necesitas await db.users.update({ where: { externalId: payload.entity.externalId }, data: { kycStatus: 'approved', kycVerifiedAt: new Date(), // No almacenar extractedData a menos que sea necesario } }); } res.status(200).send('OK');});
Nunca codificar en duro claves de API o IDs de workflow
Copy
Ask AI
// ❌ INCORRECTO: Exponiendo credenciales en código del clienteconst response = await fetch('https://api.gu1.ai/kyc/validations', { headers: { 'x-api-key': 'gsk_live_abc123...', // NUNCA HAGAS ESTO 'x-workflow-id': 'workflow-id' // NUNCA HAGAS ESTO }});
Por qué es peligroso:
Las claves de API expuestas en bundles de app pueden ser extraídas
Cualquiera puede crear validaciones en tu nombre
Imposible rotar claves comprometidas sin actualizar la app
No reutilizar URLs de verificación
Copy
Ask AI
// ❌ INCORRECTO: Almacenar URL permanentementelocalStorage.setItem('kycUrl', verificationUrl);// Más tarde...window.open(localStorage.getItem('kycUrl')); // La URL podría haber expirado
En su lugar: Siempre solicita una URL nueva desde tu backend cuando sea necesario.
No confiar en validación del lado del cliente
Copy
Ask AI
// ❌ INCORRECTO: Confiar en que el cliente reporte la finalizaciónfunction onVerificationComplete() { // El usuario podría falsificar esto updateUserStatus('approved');}
En su lugar: Solo confía en webhooks recibidos por tu backend.
import UIKitimport WebKitclass 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) // Cargar URL de verificación 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 { // Manejar error } } } } private func handleApproved() { pollingTimer?.invalidate() // Mostrar éxito y cerrar dismiss(animated: true) } private func handleRejected() { pollingTimer?.invalidate() // Mostrar mensaje de rechazo } deinit { pollingTimer?.invalidate() }}
Copy
Ask AI
import SwiftUIimport WebKitstruct 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 { // Manejar error } } } 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 { // Manejar error } } } }}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) }}
Permisos (Info.plist):
Copy
Ask AI
<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>
Perfecto para compartir lógica entre Android e iOS:
Common Code
Android Implementation
iOS Implementation
Copy
Ask AI
// commonMain/KYCManager.ktexpect class KYCManager { fun openVerification(url: String) fun startStatusPolling(userId: String, onComplete: (String) -> Unit)}// Lógica de negocio compartidaclass 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) { // Manejar error } } private fun handleApproved() { // Lógica de aprobación compartida } private fun handleRejected() { // Lógica de rechazo compartida }}
Copy
Ask AI
// androidMain/KYCManager.ktactual 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 } } } }}
Copy
Ask AI
// iosMain/KYCManager.ktactual class KYCManager { actual fun openVerification(url: String) { let vc = KYCViewController(urlString: url) // Presentar 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() } } } }}
El WebView/iframe no puede notificar directamente a tu app cuando la verificación se completa. La UI de verificación de Gu1 se ejecuta en aislamiento por razones de seguridad.
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}
Copy
Ask AI
fun pollStatus(userId: String) { lifecycleScope.launch { while (true) { delay(3000) val status = apiClient.getKYCStatus(userId) if (status in listOf("approved", "rejected")) { handleComplete(status) break } } }}
Copy
Ask AI
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) } } }}