Ir al contenido

Quickstart · KYC en 15 minutos

En 15 minutos vas a ejecutar un onboarding KYC completo contra el sandbox de Digital Jungle, usando datos de prueba. Al final el subject queda en status=approved y recibís un evento subject.verified por webhook.

Opción recomendada · @bjungle/sdk (TypeScript)

Sección titulada «Opción recomendada · @bjungle/sdk (TypeScript)»

Para integraciones nuevas usá el SDK oficial: tipado completo, retries con backoff, Idempotency-Key automático en POSTs y errores mapeados (401, 402, 429, etc.) a clases tipadas.

Ventana de terminal
npm install @bjungle/sdk
import { BjungleClient } from '@bjungle/sdk';
const dj = new BjungleClient({
apiKey: process.env.DJ_API_KEY!, // 'dj_test_...' del paso 1
baseUrl: 'https://api.qa.bjungle.net',
});

A lo largo del quickstart cada paso muestra primero la versión SDK y abajo la equivalente con cURL/fetch para quienes integran en otro lenguaje.

EndpointMétodoPlanoCosto (sandbox)
POST /signupTenant nuevoPúblico0
POST /v1/flows/seed-defaultCrea + publica onboarding defaultTenant0
POST /v1/flows/sessionsInicia sesiónTenant0
POST /v1/kyc/subjects/{id}/presign-uploadURL presigned S3Tenant0
PUT <s3_upload_url>Subida directa a S3Público (firmado)0
POST /v1/flows/sessions/{id}/advanceAvanza pasoTenantacumula 80 al face_match
GET /v1/flows/sessions/{id}Lee estado finalTenant0

Total ~5 llamadas + 2 PUTs a S3. En producción esto cobra 80 Açaí (60 % menos si se reusa wallet existente — ver Wallet SSI).


Andá a /registro?plan=sandbox y completá el formulario. La respuesta incluye plaintext api_key y webhook_secret — copialos a un secret manager o .env ahora, no se vuelven a mostrar.

Alternativa por API:

Ventana de terminal
curl -X POST https://api.qa.bjungle.net/signup \
-H "Content-Type: application/json" \
-d '{
"plan": "sandbox",
"slug": "tu-fintech",
"display_name":"Tu Fintech",
"owner_email": "dev@tufintech.co"
}'

Exportalas como env vars para los siguientes pasos:

Ventana de terminal
export DJ_BASE=https://api.qa.bjungle.net
export DJ_API_KEY=dj_test_...
export DJ_WEBHOOK_SECRET=...

Paso 2 · Crear el flow onboarding default (2 min)

Sección titulada «Paso 2 · Crear el flow onboarding default (2 min)»

bmonkey ofrece un atajo idempotente que crea y publica el flow cashpaya-style (consent → document → face_match → sarlaft) en una sola llamada:

const flow = await dj.bmonkey.flows.seedDefault();
// { id, code: 'default', type: 'onboarding', status: 'published', version: 1 }

Respuesta 201:

{
"id": "f1b2c3d4-…",
"code": "default",
"type": "onboarding",
"status": "published",
"version": 1
}

const session = await dj.bmonkey.flows.createSession({
type: 'onboarding',
code: 'default',
subject_ref: 'user-quickstart-001',
context: {
document_type: 'CC',
document_number: '1023456789',
country: 'CO',
email: 'camila@tufintech.co',
phone: '+573001234567',
},
});
// { id, token, subject_id, current_step, status, ... }

Capturá session.id, session.session_token y session.subject_id — los vas a usar en los siguientes pasos.


Paso 4 · Subir documento + selfie a S3 (3 min)

Sección titulada «Paso 4 · Subir documento + selfie a S3 (3 min)»

bmonkey usa uploads directos a S3 con presigned URLs — los bytes nunca pasan por nuestro backend. Para cada blob necesitás:

  1. Pedir un upload URL con POST /v1/kyc/subjects/{subject_id}/presign-upload.
  2. Hacer PUT al upload_url con el body crudo del archivo.
  3. Guardar el stored_uri (queda referenciado automáticamente en la sesión).
async function uploadKycAsset(subjectId: string, purpose: 'doc_front' | 'doc_back' | 'selfie', file: Blob) {
// 1. presign
const { url, key } = await dj.bmonkey.subjects.presignUpload(subjectId, {
purpose,
content_type: file.type || 'image/jpeg',
});
// 2. PUT directo a S3 (fuera del SDK — el SDK firma el presign, no el upload)
await fetch(url, { method: 'PUT', body: file });
return key; // stored_uri equivalente
}
const docFrontURI = await uploadKycAsset(session.subject_id, 'doc_front', docFrontFile);
const selfieURI = await uploadKycAsset(session.subject_id, 'selfie', selfieFile);

GET /v1/flows/sessions/{id} te dice qué step viene. Cada advance consume el input correspondiente y mueve el cursor. El orden del flow default es:

#step_typeInput esperado
1consent{ "accepted": true }
2document_upload{ "doc_front_uri": "...", "doc_back_uri": "..." }
3face_match{ "selfie_uri": "...", "liveness_mode": "basic" }
4sarlaft{} (corre automático contra bhawk)
await dj.bmonkey.flows.advance(session.id, { input: { accepted: true } });
await dj.bmonkey.flows.advance(session.id, { input: { doc_front_uri: docFrontURI } });
await dj.bmonkey.flows.advance(session.id, {
input: { selfie_uri: selfieURI, liveness_mode: 'basic' },
});
const final = await dj.bmonkey.flows.advance(session.id, { input: {} });
console.log(final.status, final.last_outcome);

El SDK ya maneja 429/503 con retry + backoff exponencial, así que no necesitás tu propio while para reintentar.

Cada respuesta trae:

{
"id": "...",
"status": "in_progress",
"current_step": "face_match",
"last_outcome": "advance",
"context": { /* respuestas acumuladas */ }
}

status final es completed (todos los steps OK), failed (alguno rechazó), manual_review (SARLAFT pidió review).


const final = await dj.bmonkey.flows.getSession(session.id);
console.log(final.status === 'completed' ? 'KYC OK' : final.last_outcome);

Respuesta:

{
"id": "...",
"subject_id": "...",
"status": "completed",
"current_step": null,
"last_outcome": "advance",
"completed_at": "2026-06-05T14:00:00Z"
}

En paralelo recibís el webhook subject.verified (ver siguiente sección).


Registrá tu endpoint con PUT /tenants/{id}/webhook-endpoints/bmonkey.

Payload de ejemplo:

{
"event_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"tenant_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"module": "bmonkey",
"event_type": "subject.verified",
"occurred_at":"2026-06-05T14:00:00Z",
"payload": {
"subject_id": "550e8400-e29b-41d4-a716-446655440000",
"external_ref": "user-quickstart-001",
"kyc_status": "approved",
"document_type": "CC",
"document_country": "CO",
"subject_age_years": 28,
"restrictive_list_hits": []
}
}

Verificá la firma HMAC antes de procesar:

import { createHmac, timingSafeEqual } from 'node:crypto';
function verifyWebhook(rawBody, signatureHeader, secret) {
const expected = createHmac('sha256', secret).update(rawBody).digest('hex');
const provided = signatureHeader.replace(/^sha256=/, '');
return (
expected.length === provided.length &&
timingSafeEqual(Buffer.from(expected), Buffer.from(provided))
);
}

Habilitá SARLAFT

El flow default ya corre SARLAFT, pero seguro querés tus propias reglas (PEP, umbrales por país, etc.).

Quickstart →

Firma electrónica

Sellá contratos PDF con KMS hash + auditoría.

Quickstart →

Wallet SSI · reuso de KYC

Cuando otro tenant ya verificó al usuario, podés reusar con consent y pagar 60 % menos.

Concepto →

OIDC Sign-in with bjungle

Authorization Code + PKCE contra bmonkey como IdP.

Concepto →

El SDK mapea cada non-2xx a una clase de error. Capturalas en lugar de chequear status numérico:

import {
UnauthorizedError,
InsufficientAcaiError,
UnprocessableEntityError,
RateLimitError,
BjungleApiError,
} from '@bjungle/sdk';
try {
await dj.bmonkey.flows.advance(session.id, { input: { selfie_uri: selfieURI } });
} catch (err) {
if (err instanceof InsufficientAcaiError) {
// 402 — balance agotado. Cargá créditos en /precios.
} else if (err instanceof UnprocessableEntityError) {
// 422 — KYC rechazado (liveness/face-match falló). Ver err.problem.
} else if (err instanceof UnauthorizedError) {
// 401 — rotá la API key.
} else if (err instanceof RateLimitError) {
// 429 — el SDK ya reintentó hasta maxRetries.
} else if (err instanceof BjungleApiError) {
console.error(err.status, err.problem); // RFC 9457 problem+json
} else {
throw err;
}
}
SíntomaCausa típicaFix
401 invalid-api-keyHeader X-API-Key faltante o key incorrectaVerificá que copiaste el plaintext, no el hash
402 insufficient-acaiBalance agotado (sandbox: testeo muy alto)Cargá créditos en /precios
409 state-conflict en seed-defaultYa existe flow onboarding defaultIgnorá — significa que estás listo
400 en advance con input vacíoEl step actual exige input (revisá current_step)Consultá la tabla de la sección Paso 5
422 kyc-rejectedLiveness o face-match fallóSubí selfie/documento con mejor calidad, o ver rejection_reason
429 rate-limit-exceededMás de 120 req/minEsperá al X-RateLimit-Reset (el SDK ya reintenta hasta maxRetries)
Webhook no llegaTu endpoint no devuelve 2xxProbá manual con curl + ver delivery en /webhook-deliveries