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:
- creado una empresa ficticia con su credencial,
- revisado su preparación (readiness),
- prevalidado y emitido una factura, y comprobado que repetirla no la duplica,
- consultado su resultado,
- recibido el aviso
document.validateden un receptor que verifica la firma y deduplica, - descargado el PDF y el XML, verificando tamaño y SHA-256.
Requisitos
| Herramienta | Versión | Para qué |
|---|---|---|
| Node.js | 24 (exactamente la de .node-version) | API, workers y ejemplos |
| npm | la incluida con Node.js 24 | dependencias |
| Docker con Compose | reciente | PostgreSQL local (puerto 55439) |
| OpenSSL | 3.x | solo para el verificador de certificados y las pruebas; este recorrido no lo usa |
xmllint | incluido en macOS; libxml2-utils en Ubuntu | solo para las pruebas XML |
cloudflared o ngrok | reciente | exponer 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:webhooksdev:envcrea.envcon secretos aleatorios; si ya existe, lo conserva.dev:bootstrapcrea una sola vez una empresa ficticia con su propietario, su clave y un rango de numeraciónSIM. Guarda la credencial en.local/credentials.jsony 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:webhookscrea 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:apiLa 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/readyRevisar 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/capabilitiesGET /v1/companydevuelve el perfil ystatus.GET /v1/company/readinesslista cada requisito (identity,numbering,certificate…) con su estado, sin modificar nada.status: readyes disponibilidad administrativa: cada admisión vuelve a comprobar firma, configuración y rango.GET /v1/capabilitiesdebe incluirinvoice.simulationhabilitada yenvironment: 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:simulationCó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:
- consultan empresa, capacidades y rangos;
- envían la factura a
POST /v1/invoice-validationsy exigenvalid: true; - la admiten con
POST /v1/invoices→202condocument_id,operation_idystatus_url; - repiten a propósito el mismo envío con la misma
Idempotency-Keyy comprueban que devuelve el mismodocument_id; - 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.
-
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 -
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 -
En otra terminal, arranca el receptor:
node dist/backend/examples/webhook-receiver.js .local/webhook-receiver.json .local/webhook-inbox -
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-001El piloto (webhook-pilot.ts):
- prevalida y admite la factura una sola vez;
- espera en
.local/webhook-inboxel evento final de ese documento (hasta 60 s); - consulta el estado actual del documento, porque los avisos pueden llegar desordenados;
- lista sus archivos con
GET /v1/documents/{id}/artifactsy descarga el PDF y la vista previa XML conGET /v1/artifacts/{id}/content, verificando tamaño y SHA-256; - 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-inboxLos 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
- Autenticación y permisos: crea claves con permisos mínimos en lugar de usar la del propietario.
- Errores e idempotencia: cómo recuperarte de timeouts sin duplicar.
- Notas crédito y débito y habilitación.