Trifaco API

Recorrido rápido

Desde un checkout limpio, emite una factura simulada, consúltala, recibe su webhook firmado y descarga el PDF y el XML.

Este recorrido corre todo en tu equipo, en el ambiente simulation. No usa AWS, certificados ni la DIAN. Al terminar habrás:

  1. creado una empresa ficticia con su credencial,
  2. revisado su preparación (readiness),
  3. prevalidado y emitido una factura, y comprobado que repetirla no la duplica,
  4. consultado su resultado,
  5. recibido el aviso document.validated en un receptor que verifica la firma y deduplica,
  6. descargado el PDF y el XML, verificando tamaño y SHA-256.

Requisitos

HerramientaVersiónPara qué
Node.js24 (exactamente la de .node-version)API, workers y ejemplos
npmla incluida con Node.js 24dependencias
Docker con ComposerecientePostgreSQL local (puerto 55439)
OpenSSL3.xsolo para el verificador de certificados y las pruebas; este recorrido no lo usa
xmllintincluido en macOS; libxml2-utils en Ubuntusolo para las pruebas XML
cloudflared o ngrokrecienteexponer el receptor de webhooks por HTTPS (paso 5)

Todos los comandos se ejecutan desde la raíz del repositorio. Los archivos que se crean en .local/ y .env contienen secretos, tienen permisos 0600 y están excluidos de Git: no los compartas ni los imprimas.

Preparar el servicio y crear la empresa

npm ci
npm run dev:env
docker compose up -d --wait
npm run build
npm run db:migrate
npm run dev:bootstrap
npm run dev:webhooks
  • dev:env crea .env con secretos aleatorios; si ya existe, lo conserva.
  • dev:bootstrap crea una sola vez una empresa ficticia con su propietario, su clave y un rango de numeración SIM. Guarda la credencial en .local/credentials.json y una factura lista para enviar en .local/invoice.json. Si ya lo ejecutaste en este equipo, reutiliza esos archivos y no borres .local/bootstrap.lock.
  • dev:webhooks crea las claves dedicadas del proceso de webhooks en .local/webhooks/ y su configuración en .local/webhooks.env. Si ya existen, el comando falla a propósito: reutilízalas.

En producción, las empresas no se crean así: las incorpora un operador de Trifaco (ver Incorporación).

Arrancar la API y los workers

Abre tres terminales en la raíz del repositorio:

WEBHOOK_PUBLIC_KEY_FILE="$PWD/.local/webhooks/public.pem" npm run start:api

La API necesita la clave pública de webhooks para cifrar los secretos de los destinos. Sin ella, crear un destino falla.

Comprueba que la API responde:

curl --silent http://127.0.0.1:3000/health/ready

Revisar la empresa y su preparación

Toda operación /v1 lleva la credencial en X-API-Key. Para no dejarla en el historial de la terminal, pásala a curl desde un archivo privado:

node -e 'const {secret}=require("./.local/credentials.json");require("fs").writeFileSync(".local/api.headers",`X-API-Key: ${secret}\n`,{mode:0o600})'
curl --silent --header @.local/api.headers http://127.0.0.1:3000/v1/company
curl --silent --header @.local/api.headers http://127.0.0.1:3000/v1/company/readiness
curl --silent --header @.local/api.headers http://127.0.0.1:3000/v1/capabilities
  • GET /v1/company devuelve el perfil y status.
  • GET /v1/company/readiness lista cada requisito (identity, numbering, certificate…) con su estado, sin modificar nada. status: ready es disponibilidad administrativa: cada admisión vuelve a comprobar firma, configuración y rango.
  • GET /v1/capabilities debe incluir invoice.simulation habilitada y environment: simulation.

Prevalidar, emitir y consultar la factura

Elige una de las dos implementaciones. Ambas usan el mismo cuerpo y la misma clave de idempotencia por defecto, así que ejecutar las dos recupera la misma admisión.

npm run example:simulation

Código: backend/examples/simulation.ts sobre el cliente mínimo backend/examples/integration.ts. Acepta rutas de credencial, factura y clave como argumentos: npm run example:simulation -- <credenciales> <factura> <clave>.

Los dos recorridos:

  1. consultan empresa, capacidades y rangos;
  2. envían la factura a POST /v1/invoice-validations y exigen valid: true;
  3. la admiten con POST /v1/invoices202 con document_id, operation_id y status_url;
  4. repiten a propósito el mismo envío con la misma Idempotency-Key y comprueban que devuelve el mismo document_id;
  5. consultan GET /v1/documents/{id} hasta 20 veces, una por segundo.

Resultado esperado: processing_status: completed y dian.status: validated con código SIMULATION_ONLY y ambiente simulation. No acredita una validación real. Si se agotan las consultas, el ejemplo queda en pending sin emitir otra factura: vuelve a consultar con el mismo ID.

La prevalidación comprueba estructura y reglas fiscales, pero no certificado, autorización ni numeración. 202 solo significa recibido y guardado.

Recibir el webhook

El receptor de ejemplo verifica la firma, el reloj, la empresa y el ambiente; guarda cada evento en una bandeja durable con deduplicación por event_id y responde 204 solo después de guardarlo.

  1. Abre un túnel HTTPS hacia el puerto del receptor y copia la URL pública que muestre:

    cloudflared tunnel --url http://127.0.0.1:3082
  2. Registra el destino inactivo. El comando guarda el secreto, que se entrega una sola vez, en .local/webhook-receiver.json (0600) y falla si ese archivo ya existe:

    node dist/backend/examples/webhook-register.js register .local/credentials.json .local/webhook-receiver.json receiver-register-001 https://TU-TUNEL/webhooks/trifaco
  3. En otra terminal, arranca el receptor:

    node dist/backend/examples/webhook-receiver.js .local/webhook-receiver.json .local/webhook-inbox
  4. Con el receptor activo, activa el destino:

    node dist/backend/examples/webhook-register.js activate .local/credentials.json .local/webhook-receiver.json receiver-activate-001

Código: webhook-register.ts y webhook-receiver.ts. Los destinos no reciben eventos anteriores a su activación. El contrato completo del aviso está en Verificar y recibir.

Emitir, esperar el aviso y descargar los archivos

El piloto necesita una factura nueva. Si reutilizas .local/invoice.json, la API devuelve el documento del paso 4, cuyo aviso se generó antes de activar el destino, y el piloto agota la espera. Crea una copia con otra referencia y otra fecha de emisión, y consérvala sin cambios:

node -e '
const f=require("./.local/invoice.json");
f.external_reference=`WEBHOOK-${Date.now()}`;
f.issued_at=new Date().toISOString();
require("fs").writeFileSync(".local/invoice-webhook.json",JSON.stringify(f,null,2),{mode:0o600,flag:"wx"})'
node dist/backend/examples/webhook-pilot.js .local/credentials.json .local/invoice-webhook.json invoice-webhook-001

El piloto (webhook-pilot.ts):

  1. prevalida y admite la factura una sola vez;
  2. espera en .local/webhook-inbox el evento final de ese documento (hasta 60 s);
  3. consulta el estado actual del documento, porque los avisos pueden llegar desordenados;
  4. lista sus archivos con GET /v1/documents/{id}/artifacts y descarga el PDF y la vista previa XML con GET /v1/artifacts/{id}/content, verificando tamaño y SHA-256;
  5. guarda los archivos en .local/webhook-downloads/, nombrados por su ID y sin sobrescribir.

Salida esperada (IDs distintos):

{"event_id":"…","document_id":"…","event_type":"document.validated","status":"validated","artifacts":["…","…"]}

La clave invoice-webhook-001 queda ligada a esa copia. Para otra prueba, crea otra copia con otro nombre y usa otra clave: repetir la clave con otro cuerpo devuelve 409 IDEMPOTENCY_CONFLICT.

Si se agota la espera, conserva el documento, los bytes y la clave: consulta el documento y los intentos del evento en lugar de emitir otra factura.

Comprobar la deduplicación

Reenvía el evento al mismo destino. Usa el event_id que imprimió el piloto:

EVENT_ID=pega-aqui-el-event_id
ENDPOINT_ID=$(node -p 'require("./.local/webhook-receiver.json").endpoint.id')
curl --silent --header @.local/api.headers --header 'Content-Type: application/json' \
  --header 'Idempotency-Key: replay-quickstart-001' \
  --data "{\"endpoint_id\":\"$ENDPOINT_ID\",\"reason\":\"Comprobar la deduplicación del receptor\"}" \
  "http://127.0.0.1:3000/v1/webhook-events/$EVENT_ID/replays"

La respuesta es 202 con un intento queued (POST /v1/webhook-events/{id}/replays). Unos segundos después:

curl --silent --header @.local/api.headers "http://127.0.0.1:3000/v1/webhook-events/$EVENT_ID/attempts"
ls .local/webhook-inbox

Los dos intentos aparecen como acknowledged y la bandeja conserva un solo archivo, <event_id>.json.

Apagar

Desactiva el destino antes de cerrar el túnel, para que no se programen intentos hacia una URL que dejará de existir:

curl --silent --dump-header .local/endpoint.headers --output .local/endpoint.json \
  --header @.local/api.headers "http://127.0.0.1:3000/v1/webhook-endpoints/$ENDPOINT_ID"
ETAG=$(grep -i '^etag:' .local/endpoint.headers | cut -d' ' -f2 | tr -d '\r')
BODY=$(node -p 'const e=require("./.local/endpoint.json");JSON.stringify({url:e.url,event_types:e.event_types,active:false})')
curl --silent --request PATCH --header @.local/api.headers --header 'Content-Type: application/json' \
  --header "If-Match: $ETAG" --header 'Idempotency-Key: receiver-deactivate-001' \
  --data "$BODY" "http://127.0.0.1:3000/v1/webhook-endpoints/$ENDPOINT_ID"

La respuesta muestra "active": false. Después detén los procesos con Ctrl+C y el túnel. docker compose stop detiene PostgreSQL y conserva los datos. Más detalles en Recuperación.

Siguientes pasos

En esta página