Bmonkey · Contrato de integración
This content is not available in your language yet.
Este contrato describe cómo tu sistema habla con Bmonkey — la llave maestra (mon-key) del ecosistema. Incluye operaciones, costos en Açaí, payloads JSON, snippets en cURL/TypeScript/Go y los eventos que recibirás por webhook.
Operaciones disponibles
Sección titulada «Operaciones disponibles»| Operación | Endpoint | Plano | Costo |
|---|---|---|---|
| Crear plantilla de formulario | POST /v1/form-templates | Control | 0 Açaí |
| Crear submission KYC | POST /v1/kyc/submissions | Data | 80 Açaí (con biometría) / 30 Açaí (sin) |
| Verificar identidad existente | POST /v1/subjects/{id}/reverify | Data | 50 Açaí |
| Consultar subject | GET /v1/subjects/{id} | Data | 1 Açaí |
| Listar submissions | GET /v1/submissions | Data | 1 Açaí |
Autenticación (OIDC — modelo MAU + extras)
Sección titulada «Autenticación (OIDC — modelo MAU + extras)»El cobro de auth sigue el estándar Auth0 / Cognito: una cuota fija por usuario activo en el mes (MAU) que cubre logins ilimitados con sesión válida, más extras por las operaciones que consumen infraestructura real (envío de SMS, biometría, etc.).
| Operación | Endpoint | Costo | Proveedor |
|---|---|---|---|
| MAU — primer login del mes por usuario (cualquier método) | POST /v1/auth/login | 4 Açaí | Bmonkey IdP |
| Logins adicionales del mismo MAU en el mismo mes | — | 0 Açaí | — |
| MFA SMS OTP enviado | POST /v1/auth/mfa/sms | 6 Açaí | AWS SNS |
| MFA Email OTP enviado | POST /v1/auth/mfa/email | 1 Açaí | AWS SES |
| MFA TOTP / Push verificado | POST /v1/auth/mfa/totp o /push | 0 Açaí | FCM / APNs |
| Step-up biométrico (eleva sesión a LoA3) | POST /v1/auth/biometric | 5 Açaí | AWS Rekognition |
| Inspeccionar / cerrar sesión | GET / DELETE /v1/auth/sessions/{id} | 1 Açaí | — |
| Emisión libre de token OIDC (servicio M2M) | POST /v1/oauth/token | 1 Açaí | — |
Wallet de identidad portable (SSI)
Sección titulada «Wallet de identidad portable (SSI)»Plano del comercio (X-API-Key):
| Operación | Endpoint | Costo |
|---|---|---|
| Discover si existe wallet activo para un documento | POST /v1/wallet/discover | 1 Açaí |
| Solicitar presentación (dispara consent del usuario) | POST /v1/wallet/request-presentation | 5 Açaí |
| Recibir presentation aprobada (webhook → reuso completo) | webhook bmonkey.presentation.granted | 32 Açaí (vs 80 KYC desde cero) |
Plano del usuario (Bearer wallet session JWT):
| Operación | Endpoint | Costo |
|---|---|---|
| Listar grants activos del wallet del usuario | GET /v1/wallet/me/grants | 0 Açaí |
| Revocar un grant | DELETE /v1/wallet/me/grants/{id} | 0 Açaí |
| Ver balance Açaí del usuario | GET /v1/wallet/me/balance | 0 Açaí |
| Ver historial de movimientos | GET /v1/wallet/me/ledger | 0 Açaí |
| Listar passkeys enroladas | GET /v1/wallet/me/passkeys | 0 Açaí |
| Revocar una passkey | DELETE /v1/wallet/me/passkeys/{id} | 0 Açaí |
| Iniciar registro WebAuthn | POST /v1/wallet/webauthn/register/begin | 0 Açaí |
| Confirmar registro WebAuthn | POST /v1/wallet/webauthn/register/finish | 0 Açaí |
Plano público (consent token + WebAuthn):
| Operación | Endpoint | Costo |
|---|---|---|
| Ver detalles del consent | GET /consent/{token} | 0 Açaí |
| Aprobar consent (emite presentation + crédita 5 Açaí al usuario) | POST /consent/{token}/approve | (cobrado al solicitante = 32) |
| Rechazar consent | POST /consent/{token}/reject | 0 Açaí |
| Iniciar login con passkey | POST /v1/wallet/webauthn/login/begin | 0 Açaí |
| Finalizar login con passkey | POST /v1/wallet/webauthn/login/finish | 0 Açaí |
Flujo end-to-end (happy path)
Sección titulada «Flujo end-to-end (happy path)»- Crear plantilla desde el admin o por API (
POST /v1/form-templates). - Tu app llama
POST /v1/kyc/submissionscon las respuestas del formulario y la selfie + documento en base64 o URLs presignadas. - Bmonkey responde
202consubmission_idy unsubject_idprovisional. - Worker procesa biometría + validación documental en background.
- Recibes webhook
bmonkey.subject.verified(o.rejected) firmado con HMAC. - Verificas la firma y guardas el resultado en tu sistema.
1. Crear submission KYC
Sección titulada «1. Crear submission KYC»Request
Sección titulada «Request»POST /v1/kyc/submissions HTTP/1.1Host: api.digital-jungle.bjungle.comX-API-Key: <api_key>Content-Type: application/jsonIdempotency-Key: 5f8d1c3e-1234-... # recomendado{ "template_code": "persona-natural-v1", "external_ref": "client-90234", "answers": { "tipo_documento": "CC", "numero_documento": "1023456789", "primer_nombre": "Camila", "primer_apellido": "Rodríguez", "fecha_nacimiento": "1992-04-18", "direccion": "Calle 100 #15-22", "ciudad": "Bogotá" }, "biometric": { "selfie_url": "https://uploads.tu-app.com/abc.jpg", "document_url": "https://uploads.tu-app.com/cc.jpg" }}Response — 202 Accepted
Sección titulada «Response — 202 Accepted»{ "submission_id": "01J8FXY2K7HZWQ7M9P0G5R3T4N", "subject_id": "0c4b5b32-3a89-4a45-9c2c-1a8c47e91234", "status": "received", "acai_debited": 80}Webhook resultado — bmonkey.subject.verified
Sección titulada «Webhook resultado — bmonkey.subject.verified»{ "event_id": "5f8d1c3e-3b00-4d6e-8a9d-5e8c8b5f1234", "tenant_id": "8a7b6c5d-1234-5678-9abc-def012345678", "module": "bmonkey", "event_type": "bmonkey.subject.verified", "occurred_at": "2026-05-25T14:00:00Z", "payload": { "subject_id": "0c4b5b32-3a89-4a45-9c2c-1a8c47e91234", "submission_id": "01J8FXY2K7HZWQ7M9P0G5R3T4N", "external_ref": "client-90234", "decision": "verified", "biometric_score": 0.97, "liveness_score": 0.99, "document_match": true, "attributes": { "full_name": "Camila Rodríguez", "document_id": "1023456789", "document_type": "CC" } }}Snippets
Sección titulada «Snippets»curl -X POST https://api.digital-jungle.bjungle.com/v1/kyc/submissions \ -H "X-API-Key: $BMONKEY_KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: $(uuidgen)" \ -d @submission.jsonimport { BmonkeyClient } from '@bjungle/bmonkey-client';import { randomUUID } from 'node:crypto';
const bmonkey = new BmonkeyClient({ apiKey: process.env.BMONKEY_KEY! });
const res = await bmonkey.kyc.submit( { template_code: 'persona-natural-v1', external_ref: 'client-90234', answers: { /* ... */ }, biometric: { selfie_url, document_url }, }, { headers: { 'Idempotency-Key': randomUUID() } },);
console.log(res.submission_id);package main
import ( "context" "github.com/bjungle/sdk-go/bmonkey" "github.com/google/uuid")
func main() { client := bmonkey.NewClient(os.Getenv("BMONKEY_KEY")) res, err := client.KYC.Submit(context.Background(), &bmonkey.SubmissionInput{ TemplateCode: "persona-natural-v1", ExternalRef: "client-90234", Answers: map[string]any{ /* ... */ }, Biometric: &bmonkey.Biometric{SelfieURL: selfieURL, DocumentURL: docURL}, }, bmonkey.WithIdempotencyKey(uuid.NewString())) if err != nil { log.Fatal(err) } fmt.Println(res.SubmissionID)}2. Verificar firma del webhook
Sección titulada «2. Verificar firma del webhook»Cada webhook llega con header X-Bjungle-Signature: sha256=<hex>. La firma es
el HMAC-SHA256 del body crudo con el webhook_secret del tenant.
import { createHmac, timingSafeEqual } from 'node:crypto';
export function verify(rawBody: string, signatureHeader: string, secret: string): boolean { const expected = createHmac('sha256', secret).update(rawBody).digest('hex'); const provided = signatureHeader.replace(/^sha256=/, ''); return timingSafeEqual(Buffer.from(expected), Buffer.from(provided));}func verify(body []byte, sigHeader, secret string) bool { expected := hmac.New(sha256.New, []byte(secret)) expected.Write(body) sig := strings.TrimPrefix(sigHeader, "sha256=") return hmac.Equal([]byte(hex.EncodeToString(expected.Sum(nil))), []byte(sig))}import hmac, hashlib
def verify(raw_body: bytes, sig_header: str, secret: bytes) -> bool: expected = hmac.new(secret, raw_body, hashlib.sha256).hexdigest() provided = sig_header.removeprefix("sha256=") return hmac.compare_digest(expected, provided)3. Autenticación con niveles de aseguramiento
Sección titulada «3. Autenticación con niveles de aseguramiento»Bmonkey emite tokens OIDC estándar con el claim acr (Authentication
Context Class Reference) indicando el LoA alcanzado en la sesión. Tu app
valida acr antes de autorizar acciones sensibles y hace step-up si falta nivel.
POST /v1/auth/login HTTP/1.1Host: api.digital-jungle.bjungle.comX-API-Key: <api_key>Content-Type: application/json{ "subject_ref": "client-90234", "credentials": { "username": "camila@correo.co", "password": "***" }, "required_loa": "LoA2", "redirect_uri": "https://app.cashpaya.co/oauth/callback"}Si las credenciales son válidas pero el LoA exigido pide MFA, la respuesta no es un token sino un challenge:
{ "challenge_id": "ch_01J8H1...", "next_step": "mfa", "available_factors": ["sms", "totp", "push"], "expires_at": "2026-05-25T14:05:00Z", "acai_debited": 1}Completar challenge MFA
Sección titulada «Completar challenge MFA»POST /v1/auth/mfa/challenge HTTP/1.1{ "challenge_id": "ch_01J8H1...", "factor": "totp", "code": "284913" }Response — token OIDC final:
{ "access_token": "eyJhbGciOiJSUzI1NiIs...", "id_token": "eyJhbGciOiJSUzI1NiIs...", "token_type": "Bearer", "expires_in": 3600, "acr": "LoA2", "amr": ["pwd", "totp"], "session_id": "ses_01J8H2..."}El claim acr viaja firmado dentro del JWT. Tu backend lo lee y exige
step-up cuando un endpoint requiere LoA3:
if (decodedJwt.acr !== 'LoA3' && action === 'transfer') { return redirectStepUp(decodedJwt.session_id, 'LoA3');}4. Wallet de identidad — reuso de KYC
Sección titulada «4. Wallet de identidad — reuso de KYC»Permite que un tenant nuevo reuse una verificación que otro tenant ya hizo, con consentimiento del usuario y pagando 60 % menos. Ver el concepto completo en Wallet de identidad.
Discover (¿existe wallet?)
Sección titulada «Discover (¿existe wallet?)»POST /v1/wallet/discover HTTP/1.1{ "document_type": "CC", "document_id": "1023456789" }Response (no expone claims, solo metadatos):
{ "wallet_exists": true, "active_loa": "LoA3", "expires_at": "2027-03-12T15:00:00Z", "origin_tenant_known": true}Solicitar presentation
Sección titulada «Solicitar presentation»POST /v1/wallet/request-presentation HTTP/1.1{ "document_type": "CC", "document_id": "1023456789", "required_claims": ["full_name", "document", "address"], "required_loa": "LoA3", "purpose": "Onboarding en Tenant B para producto microcrédito", "notify_via": ["push", "email"], "expires_in_min": 30}Response 202 Accepted:
{ "request_id": "req_01J8H5...", "status": "awaiting_consent", "acai_debited": 5}Webhook — usuario aprobó
Sección titulada «Webhook — usuario aprobó»{ "event_id": "5f8d1c3e-7a01-4d6e-8a9d-5e8c8b5fffff", "tenant_id": "8a7b6c5d-1234-5678-9abc-def012345678", "module": "bmonkey", "event_type": "bmonkey.presentation.granted", "occurred_at":"2026-05-25T14:01:30Z", "payload": { "request_id": "req_01J8H5...", "presentation_id":"prs_01J8H6...", "subject_id": "0c4b5b32-3a89-4a45-9c2c-1a8c47e91234", "acr": "LoA3", "issued_at": "2026-03-12T15:00:00Z", "claims": { "full_name": "Camila Rodríguez", "document": "1023456789", "address": "Calle 100 #15-22, Bogotá" }, "valid_until": "2026-06-24T14:01:30Z", "acai_debited": 32, "revenue_share": { "origin_tenant_credit": 12 } }}Webhook — usuario rechazó
Sección titulada «Webhook — usuario rechazó»{ "event_type": "bmonkey.presentation.denied", "payload": { "request_id": "req_01J8H5...", "reason": "user_denied", "fallback_recommended": "kyc_from_scratch", "acai_debited": 5 }}Si el usuario rechaza, no se debitan los 32 Açaí — solo los 5 del request. Tu app debe ofrecer el KYC clásico como camino alternativo.
Eventos emitidos
Sección titulada «Eventos emitidos»| Evento | Cuándo |
|---|---|
bmonkey.subject.created | Submission recibida, antes de procesar |
bmonkey.subject.verified | KYC aprobado (biometría OK, documento válido) |
bmonkey.subject.rejected | KYC rechazado (con reason específico) |
bmonkey.auth.login.succeeded | Sesión OIDC abierta (con acr indicando LoA) |
bmonkey.auth.login.failed | Intento fallido (incluye razón: bad_password / mfa_expired) |
bmonkey.auth.session.revoked | Logout o expiración |
bmonkey.presentation.requested | Un tenant solicitó reuso del wallet |
bmonkey.presentation.granted | Usuario aprobó — credenciales entregadas |
bmonkey.presentation.denied | Usuario rechazó la solicitud de reuso |
bmonkey.wallet.grant.revoked | Usuario revocó un acceso vigente |
Cada evento se entrega al endpoint registrado en /tenants/{id}/webhook-endpoints/bmonkey
y vive bajo el subject NATS interno bmonkey.subject.<event> para integración entre apps.
Errores
Sección titulada «Errores»Bmonkey devuelve application/problem+json (RFC 7807):
{ "type": "https://digital-jungle.bjungle.com/errors/biometric-low-score", "title": "Biometric score below threshold", "status": 422, "detail": "Liveness score 0.42 < umbral 0.70 del tenant", "instance": "/v1/kyc/submissions/01J8FXY...", "errors": [ { "path": "biometric.liveness_score", "detail": "0.42 < 0.70" } ]}| Status | Causa típica |
|---|---|
400 Bad Request | Payload malformado o template_code inexistente |
401 Unauthorized | X-API-Key ausente o inválida |
402 Payment Required | Sin Açaí disponibles ni capacidad de overage |
409 Conflict | Idempotency-Key repetida con payload distinto |
422 Unprocessable | Validación de negocio (umbrales biométricos, documento inválido) |
429 Too Many Requests | Rate limit del plan excedido |