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).

Resumen del flujo
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.

Paso 1 · Crear una sesión
POST /api/v1/public/sessions. Todos los campos del cuerpo son opcionales.

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, expiresAt
Paso 2 · Abrir la verificación

Opció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 cerrarse

Hay 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.

Paso 3 · Recibir el resultado

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"
  }
}
EventoCuándo
verification.completedLa sesión terminó con veredicto (approved o declined).
verification.in_reviewRequiere revisión manual; la decisión llegará después. Trátalo como pendiente.
webhook.testEvento 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 "", 200

Ventana 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"
Estados de una sesión
EstadoSignificado
pendingSesión creada; el usuario aún no abre el enlace.
consentedEl usuario aceptó el consentimiento y está completando el flujo.
processingSe recibió todo; se está analizando.
in_reviewPendiente de revisión manual.
approvedVerificación aprobada (final).
declinedVerificación rechazada (final).
cancelledCancelada por tu sistema (final).
expiredEl enlace venció antes de completarse (final).
Códigos de error
CódigoCausa
401API key ausente, inválida o revocada.
402Sin créditos: recarga antes de crear nuevas sesiones.
404Sesión inexistente o de otra organización.
409Estado incompatible (p. ej. cancelar una sesión ya finalizada).
410El enlace de verificación expiró o fue cancelado.
422Cuerpo 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.

Consejos para probar
  • En el panel, Webhooks, usa el botón Probar: envía un evento webhook.test firmado a tu endpoint para validar la firma.
  • Usa un túnel (ngrok, cloudflared) para recibir webhooks en tu máquina local.
  • Pon un externalRef reconocible para correlacionar sesiones en el panel.
  • Abre /widget-demo.html?url=… para ver todos los eventos del widget.
Lista de seguridad
  • 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 id del evento: puede entregarse más de una vez.
  • Trata in_review como 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.