Ir al contenido

Quickstart · SARLAFT con bhawk

bhawk es el módulo de cumplimiento del ecosistema. Te permite crear sets de reglas SARLAFT versionadas, cruzar contra OpenSanctions (OFAC, ONU, PEP, EU_SANCTIONS, FATF) y operar el workbench de casos para review/reject.

En este quickstart vas a:

  1. Seed una validation con plantillas SARLAFT estándar.
  2. Publicar la validation.
  3. Probarla con dry-run (sin cobrar).
  4. Correr un screening real (POST /v1/screenings) que persiste y debita 8 Açaí.
  5. Gestionar los casos abiertos.

Ventana de terminal
export DJ_BASE=https://bhawk.qa.bjungle.net
export DJ_API_KEY=dj_test_... # misma key que bmonkey

bhawk corre en su propio host (bhawk.qa.bjungle.net), pero usa la misma API key del tenant.

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

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

El SDK enruta cada módulo a su host por separado a partir de baseUrl o moduleUrls. Para integraciones nuevas usá el SDK — tipa request + response, mapea 409/402/422 a errores con clase y agrega Idempotency-Key automático.

Ventana de terminal
npm install @bjungle/sdk
import { BjungleClient } from '@bjungle/sdk';
const dj = new BjungleClient({
apiKey: process.env.DJ_API_KEY!,
baseUrl: 'https://api.qa.bjungle.net',
moduleUrls: {
bhawk: 'https://bhawk.qa.bjungle.net', // mismo tenant, host distinto
},
});

Una validation es un grupo versionado de reglas. Ciclo de vida: draft → published → deprecated. Solo los drafts se pueden mutar.

const validation = await dj.bhawk.validations.create({
code: 'default',
name: 'SARLAFT por defecto',
description: 'Reglas baseline para personas naturales CO',
});
// { id, code, status: 'draft', version: 1, ... }

Capturá validation.id para el próximo paso.


bhawk trae un catálogo de plantillas SARLAFT canónicas (PEP, OFAC hit, edad mínima, países de alto riesgo, etc.):

Ventana de terminal
curl -X POST "$DJ_BASE/v1/rules/seed-defaults" \
-H "X-API-Key: $DJ_API_KEY" \
-H "Content-Type: application/json" \
-d "{
\"validation_id\": \"$VALIDATION_ID\",
\"subject_type\": \"natural\"
}"

Inspeccioná el catálogo crudo si querés filtrar antes de seedear:

Ventana de terminal
curl "$DJ_BASE/v1/templates?subject_type=natural&group=sarlaft" \
-H "X-API-Key: $DJ_API_KEY" | jq .

subject_type puede ser natural | legal | both. Las plantillas vienen pre-configuradas con severities (approve | review | reject) y scores.


const published = await dj.bhawk.validations.publish(validation.id);
// { id, status: 'published', version: 1, ... }

Esto hace dos cosas atómicamente:

  • Transiciona la validation a published.
  • Auto-deprecates cualquier validation publicada anterior con el mismo code.

A partir de acá POST /v1/screenings con validation_code: "default" la usa.


POST /v1/screenings/dry-run evalúa cualquier validation (draft, published, deprecated) sin persistir y sin cobrar Açaí. Ideal para tests en CI o para “probar este caso edge antes de publicar”.

const dryRun = await dj.bhawk.screenings.dryRun({
validation_code: 'default',
subject_ref: 'user-quickstart-001',
subject_type: 'natural',
payload: {
full_name: 'Camila Rodríguez',
birth_date: '1992-04-18',
document_type: 'CC',
document_number: '1023456789',
country_code: 'CO',
subject_age: 33,
kyc_status: 'approved',
amount: 5000000,
currency: 'COP',
},
});
console.log(dryRun.decision, dryRun.findings);

Respuesta:

{
"decision": "approve",
"score": 0,
"findings": [],
"list_hits": []
}

Probá con un caso PEP — cambia full_name por el de un PEP conocido (el screener consulta OpenSanctions/Yente):

{
"decision": "review",
"score": 60,
"findings": [
{
"rule_code": "pep",
"node_id": "a1",
"decision": "review",
"score": 60,
"reason": "Subject identificado como PEP — DDR requerida."
}
],
"list_hits": ["PEP"]
}

Idéntico body al dry-run pero contra POST /v1/screenings (sin validation_id — usa la publicada del validation_code que pasés, default "default"). Debita 8 Açaí y persiste la evaluación en bhawk.risk_evaluations.

import { InsufficientAcaiError } from '@bjungle/sdk';
try {
const screening = await dj.bhawk.screenings.create({
validation_code: 'default',
subject_ref: 'user-quickstart-001',
subject_type: 'natural',
payload: {
full_name: 'Camila Rodríguez',
birth_date: '1992-04-18',
document_type: 'CC',
document_number: '1023456789',
country_code: 'CO',
subject_age: 33,
kyc_status: 'approved',
},
});
// screening.decision: 'approve' | 'review' | 'reject'
} catch (err) {
if (err instanceof InsufficientAcaiError) {
// 402 — balance < 8 Açaí. Bloquear + notificar ops.
} else {
throw err;
}
}

subject_ref es opaco — el ID que tu sistema asigna al usuario. bhawk lo guarda para correlación pero no comparte foreign keys con bmonkey (ver gotcha #5 de CLAUDE.md). Eso te deja libre de elegir el shape que quieras.

Decisiones agregadas: reject > review > approve. Si alguna regla dispara reject, el screening completo es reject.

Cuando la decisión es review o reject, bhawk auto-abre un caso en el workbench.


Paso 6 · Workbench — listar y resolver casos

Sección titulada «Paso 6 · Workbench — listar y resolver casos»
// Listar casos abiertos
const page = await dj.bhawk.cases.list({ status: 'open', limit: 50 });
console.log(page.items.length, 'casos abiertos');
// Asignar a un analista
await dj.bhawk.cases.assign(caseId, { assignee: 'compliance@tufintech.co' });
// Resolver
await dj.bhawk.cases.resolve(caseId, {
decision: 'approve',
reason: 'PEP confirmado pero perfil de bajo riesgo',
});
// Escalar
await dj.bhawk.cases.escalate(caseId, { reason: 'monto excede umbral DDR' });

Endpoints del caso:

VerboPathAcción
GET/v1/cases?status=Listar (cursor pagination)
GET/v1/cases/statsKPIs (count por estado)
GET/v1/cases/export?from=&to=CSV ROS
GET/v1/cases/{id}Detalle + timeline auditable
POST/v1/cases/{id}/assignAsignar a analista
POST/v1/cases/{id}/commentComentar
POST/v1/cases/{id}/resolveResolver con `approve
POST/v1/cases/{id}/escalateEscalar a supervisor
POST/v1/cases/{id}/reopenReabrir resuelto

El flow default de bmonkey ya incluye un step sarlaft que llama a bhawk vía S2S internamente. Si querés que tu flow customizado lo haga:

Ventana de terminal
# 1. Clonar el flow default para editar
curl -X POST "$BMONKEY_BASE/v1/flows/$FLOW_ID/clone" -H "X-API-Key: $DJ_API_KEY"
# 2. PUT steps con sarlaft en la posición que quieras
curl -X PUT "$BMONKEY_BASE/v1/flows/$NEW_FLOW_ID/steps" \
-H "X-API-Key: $DJ_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"steps": [
{ "step_type": "consent", "required": true, "config": {} },
{ "step_type": "document_upload", "required": true, "config": {} },
{ "step_type": "face_match", "required": true, "config": {} },
{ "step_type": "sarlaft", "required": true, "config": {"validation_code":"default"} }
]
}'
# 3. Publicar
curl -X POST "$BMONKEY_BASE/v1/flows/$NEW_FLOW_ID/publish" -H "X-API-Key: $DJ_API_KEY"

El validation_code por default es "default". Si querés correr distintas reglas según país, podés tener default, default-mx, default-ar y referenciar la apropiada al crear la sesión.


Cada regla es un grafo de Condition + Action nodes con edges dirigidas. Semantics AND: una action dispara cuando todas sus conditions entrantes son true. Para OR usá actions separadas.

{
"code": "alto-monto-pais-riesgo",
"severity": "review",
"score": 50,
"subject_type": "natural",
"definition": {
"nodes": [
{ "id":"c1", "type":"condition", "predicate":"amount_gte", "value": 10000000 },
{ "id":"c2", "type":"condition", "predicate":"country_in", "value": ["VE","KP","IR"] },
{ "id":"a1", "type":"action", "decision":"review", "score": 50,
"reason":"Monto alto desde país de riesgo — DDR requerida." }
],
"edges": [
{ "from":"c1", "to":"a1" },
{ "from":"c2", "to":"a1" }
]
}
}

Predicados disponibles (parcial):

  • Identidad: subject_age_gte, subject_age_lt, country_in, country_not_in
  • Listas: on_restrictive_list (codes: OFAC | ONU | PEP | EU_SANCTIONS | FATF)
  • Transacción: amount_gte, currency_in
  • KYC: kyc_status_in

El catálogo completo + schema vive en shared-libs/go-bjungle-bhawk-evaluator.


SíntomaCausa típicaFix
Screening siempre devuelve approve sin hitsOpenSanctions sidecar no configurado en sandboxEsperable en QA; para prod, contactar para activar
409 en publishOtra validation con mismo code ya está publicadaEs el flujo correcto — la auto-deprecada queda en histórico
400 validation-not-draft al editar reglaEstás editando una validation publicadaCloná → editás el draft → publish
402 insufficient-acai en /v1/screeningsBalance < 8 AçaíSandbox arranca con 1000 simulados
Análisis con dry-run difiere de screening realOpenSanctions hits dependen del momento (catálogo se actualiza)Esperado; dry-run es snapshot del momento

Reglas avanzadas

Concepto completo del motor de reglas, predicados y semantics.

Concepto →

API Reference Bhawk

Spec OpenAPI 3.1 completo, interactivo con Scalar.

Abrir →