Guía de integración
Verifica la identidad de tus usuarios en tres pasos: crea una sesión desde tu servidor, muestra el flujo al usuario y recibe el resultado por webhook. La referencia completa de endpoints está en Swagger (http://localhost:3001/docs).
Tu backend Brickflow API Tu usuario (navegador)
| | |
|-- POST /public/sessions (x-api-key) ------------>|
|<-- { id, url, expiresAt } ----| |
| | |
|-- entrega "url" (redirección o widget) -------->|
| |<-- flujo alojado /v/<token> (consentimiento,
| | datos, documento, prueba de vida)
| | |
|<== Webhook firmado: verification.completed / in_review
|-- (alternativa) GET /public/sessions/:id -->|Todas las llamadas a la API usan tu API key en el encabezado x-api-key y deben hacerse desde tu servidor. Las URLs de este documento apuntan al entorno local; en producción usa tu dominio.
Campos: externalRef (tu identificador), firstName, lastName, documentType, documentNumber. La respuesta incluye url, el enlace del flujo alojado, que vence en expiresAt. Cada sesión consume un crédito; con 0 créditos recibes 402.
curl
curl -X POST https://apibio.brickflows.com/api/v1/public/sessions \
-H "x-api-key: $BRICKFLOW_API_KEY" \
-H "Content-Type: application/json" \
-d '{"externalRef":"pedido-1234","firstName":"Ana","lastName":"Pérez"}'
# 201
# { "id": "…", "status": "pending",
# "url": "http://localhost:3000/v/<token>", "expiresAt": "…" }Node.js
// Node 18+ (solo en tu servidor)
const res = await fetch('https://apibio.brickflows.com/api/v1/public/sessions', {
method: 'POST',
headers: {
'x-api-key': process.env.BRICKFLOW_API_KEY,
'Content-Type': 'application/json'
},
body: JSON.stringify({ externalRef: 'pedido-1234' })
});
if (res.status === 402) throw new Error('Sin créditos');
if (!res.ok) throw new Error('Error ' + res.status);
const session = await res.json(); // { id, status, url, expiresAt }Python
import os, requests
res = requests.post(
"https://apibio.brickflows.com/api/v1/public/sessions",
headers={"x-api-key": os.environ["BRICKFLOW_API_KEY"]},
json={"externalRef": "pedido-1234"},
timeout=10,
)
if res.status_code == 402:
raise RuntimeError("Sin créditos")
res.raise_for_status()
session = res.json() # id, status, url, expiresAtOpción A: redirección (la más simple)
Redirige al usuario a session.url (o envíaselo por correo/SMS). El flujo funciona en móvil y escritorio y es la opción más fiable para la cámara.
Opción B: widget embebido
Muestra el flujo en un modal sobre tu página, sin dependencias. Solo acepta URLs http(s)://…/v/<token> y únicamente escucha mensajes del origen de esa URL. El widget nunca recibe datos personales ni el token: solo el nombre del paso y el estado final.
<script src="http://localhost:3000/widget/brickflow-verify.js"></script>
<script>
// "url" la obtiene tu servidor en el paso 1 y la entrega a esta página.
BrickflowVerify.open({
url: sessionUrl,
locale: 'es',
onEvent: function (e) { console.log(e.type, e); },
onComplete: function (r) {
// r = { sessionId?, status }. NO es la decisión final:
// confía en el webhook / consulta a tu backend.
},
onClose: function () {}
});
</script>// Mensajes postMessage del flujo alojado (versión 1). Nunca incluyen token,
// datos personales ni imágenes.
{ type: 'brickflow:ready' }
{ type: 'brickflow:step', step: 'consent' | 'data' | 'document' | 'back' | 'camera' | 'done' }
{ type: 'brickflow:completed', status: 'processing' | 'in_review' | 'approved' | 'declined' }
{ type: 'brickflow:error', code: 'expired' | 'not_found' | 'cancelled' | … }
{ type: 'brickflow:closed' } // lo emite el propio widget al cerrarseHay una página de prueba en /widget-demo.html?url=<url de la sesión>. Notas: el iframe pide permiso solo de cámara (no micrófono); el evento completed es informativo, la decisión definitiva llega por webhook; si tu página usa Referrer-Policy: no-referrer el flujo no conocerá tu origen y no enviará eventos. Hoy el flujo alojado puede incrustarse desde cualquier sitio; se prevé restringirlo por organización.
Webhook (recomendado)
Registra un endpoint en el panel (Webhooks) y guarda su secreto. Cada entrega es un POST JSON sin datos personales con el encabezado Webhook-Signature: t=<unix>,v1=<hex>, donde v1 es el HMAC-SHA256 de "<t>.<cuerpo crudo>" con el secreto del endpoint.
{
"id": "evt_…",
"type": "verification.completed",
"createdAt": "2026-10-03T15:04:05.000Z",
"data": {
"sessionId": "…",
"externalRef": "pedido-1234",
"status": "approved",
"verdict": "approved",
"reasons": [],
"completedAt": "2026-10-03T15:04:05.000Z"
}
}| Evento | Cuándo |
|---|---|
| verification.completed | La sesión terminó con veredicto (approved o declined). |
| verification.in_review | Requiere revisión manual; la decisión llegará después. Trátalo como pendiente. |
| webhook.test | Evento de prueba enviado desde el panel con el botón "Probar". |
Verificación de firma · Node.js
import crypto from 'node:crypto';
import express from 'express';
const app = express();
const SECRET = process.env.BRICKFLOW_WEBHOOK_SECRET; // whsec_… del endpoint
const TOLERANCE_SECONDS = 300; // ventana anti-replay
// IMPORTANTE: necesitas el cuerpo CRUDO, no el JSON ya parseado.
app.post('/webhooks/brickflow', express.raw({ type: 'application/json' }), (req, res) => {
const header = req.get('Webhook-Signature') || '';
const parts = Object.fromEntries(header.split(',').map((p) => p.trim().split('=')));
const t = Number(parts.t);
const body = req.body.toString('utf8');
if (!t || !parts.v1) return res.sendStatus(400);
if (Math.abs(Date.now() / 1000 - t) > TOLERANCE_SECONDS) return res.sendStatus(400);
const expected = crypto.createHmac('sha256', SECRET).update(t + '.' + body).digest('hex');
const a = Buffer.from(expected, 'hex');
const b = Buffer.from(parts.v1, 'hex');
if (a.length !== b.length || !crypto.timingSafeEqual(a, b)) return res.sendStatus(400);
const event = JSON.parse(body);
// Idempotencia: ignora si ya procesaste event.id
// switch (event.type) { case 'verification.completed': … }
res.sendStatus(200); // responde 2xx rápido; procesa en segundo plano
});
app.listen(4000);Verificación de firma · Python
import hashlib, hmac, os, time
from flask import Flask, request, abort
app = Flask(__name__)
SECRET = os.environ["BRICKFLOW_WEBHOOK_SECRET"].encode()
TOLERANCE_SECONDS = 300 # ventana anti-replay
@app.post("/webhooks/brickflow")
def brickflow_webhook():
header = request.headers.get("Webhook-Signature", "")
parts = dict(p.strip().split("=", 1) for p in header.split(",") if "=" in p)
try:
t = int(parts["t"])
v1 = parts["v1"]
except (KeyError, ValueError):
abort(400)
if abs(time.time() - t) > TOLERANCE_SECONDS:
abort(400)
body = request.get_data() # cuerpo CRUDO (bytes)
expected = hmac.new(SECRET, f"{t}.".encode() + body, hashlib.sha256).hexdigest()
if not hmac.compare_digest(expected, v1):
abort(400)
event = request.get_json()
# Idempotencia: ignora si ya procesaste event["id"]
return "", 200Ventana anti-replay: rechaza firmas cuyo t difiera más de 5 minutos de tu reloj y compara con tiempo constante. Firma sobre el cuerpo crudo: si lo re-serializas, la firma no coincidirá.
Reintentos: responde con 2xx. Si tu endpoint falla o expira, se reintenta con esperas de 5 s, 30 s, 5 min y 30 min; después la entrega se marca como fallida (puedes revisarla en el panel). Por eso tu handler debe ser idempotente.
Alternativa: consulta (polling)
Útil como respaldo o si no puedes exponer un endpoint público.
curl https://apibio.brickflows.com/api/v1/public/sessions/<id> -H "x-api-key: $BRICKFLOW_API_KEY"
# { "id", "status", "verdict", "externalRef", "expiresAt", "completedAt", "createdAt" }
# Cancelar una sesión pendiente:
curl -X POST https://apibio.brickflows.com/api/v1/public/sessions/<id>/cancel -H "x-api-key: $BRICKFLOW_API_KEY"| Estado | Significado |
|---|---|
| pending | Sesión creada; el usuario aún no abre el enlace. |
| consented | El usuario aceptó el consentimiento y está completando el flujo. |
| processing | Se recibió todo; se está analizando. |
| in_review | Pendiente de revisión manual. |
| approved | Verificación aprobada (final). |
| declined | Verificación rechazada (final). |
| cancelled | Cancelada por tu sistema (final). |
| expired | El enlace venció antes de completarse (final). |
| Código | Causa |
|---|---|
| 401 | API key ausente, inválida o revocada. |
| 402 | Sin créditos: recarga antes de crear nuevas sesiones. |
| 404 | Sesión inexistente o de otra organización. |
| 409 | Estado incompatible (p. ej. cancelar una sesión ya finalizada). |
| 410 | El enlace de verificación expiró o fue cancelado. |
| 422 | Cuerpo inválido: revisa los campos enviados. |
Créditos: cada sesión creada consume un crédito de tu organización. Consulta el saldo en el panel; cuando se agota, la creación responde 402 hasta que se recargue.
- En el panel, Webhooks, usa el botón Probar: envía un evento
webhook.testfirmado a tu endpoint para validar la firma. - Usa un túnel (ngrok, cloudflared) para recibir webhooks en tu máquina local.
- Pon un
externalRefreconocible para correlacionar sesiones en el panel. - Abre
/widget-demo.html?url=…para ver todos los eventos del widget.
- Nunca expongas la API key en el navegador: crea las sesiones desde tu backend y entrega al cliente solo la
url. - Verifica siempre la firma del webhook y la ventana de tiempo antes de procesar.
- Implementa idempotencia por
iddel evento: puede entregarse más de una vez. - Trata
in_reviewcomo pendiente, no como aprobado ni rechazado. - No tomes decisiones con los eventos del widget; son informativos y los puede manipular el cliente.
- Guarda el secreto del webhook en variables de entorno y rótalo si se filtra.