Cabeceras

Headers enviados por el dispatcher org-level

Header Ejemplo Uso
Content-Type application/json Body JSON serializado como string.
User-Agent Kapture-Webhooks/1.0 Identifica el dispatcher publico actual.
X-Kapture-Signature sha256=<digest> Firma HMAC SHA-256 del body crudo.
X-Kapture-Event record.created Nombre del evento emitido.
X-Kapture-Timestamp 2026-04-30T15:00:00.000Z Timestamp del envio HTTP.

Contrato

Lo que el receptor debe cumplir

Responder rapido

El dispatcher org-level espera una respuesta en menos de 5 segundos.

Confirmar con 2xx cuando ya acepto

Si el evento ya quedo duradero en el receptor, la respuesta debe ser `200` o `202`.

Conservar raw body

No se debe regenerar JSON ni cambiar el orden antes de validar la firma.

Aplicar idempotencia

Usa `event + id` como llave minima para evitar duplicados aguas abajo.

Evento

Schema de `record.created`

Campo Tipo Descripcion
event string Siempre `record.created`.
id string ID del registro.
parent_id string ID de la captura a la que pertenece.
timestamp string ISO 8601 `transactionDate` del registro o fecha actual como fallback.
type string `templateId` del registro o `UNKNOWN`.
vendor string opcional Nombre del vendor si existe.
currency string opcional Moneda detectada en el registro.
data objeto Payload de negocio aplanado desde `record.data`.

Ejemplo

{
  "event": "record.created",
  "id": "rec_demo_001",
  "parent_id": "cap_demo_001",
  "timestamp": "2026-04-30T15:00:00.000Z",
  "type": "INVOICE",
  "vendor": "ACME S.A.S.",
  "currency": "COP",
  "data": {
    "invoice_number": "FAC-001",
    "total": 120000
  }
}

Evento

Schema de `expedient.completed`

Campo Tipo Descripcion
event string Siempre `expedient.completed`.
id string ID del expediente.
timestamp string ISO 8601 Fecha de construccion del payload.
type string `templateId` del expediente o `UNKNOWN`.
data objeto Documento del expediente ya sanitizado, sin `imageBytes`.

Ejemplo

{
  "event": "expedient.completed",
  "id": "exp_demo_001",
  "timestamp": "2026-04-30T15:00:00.000Z",
  "type": "INVOICE",
  "data": {
    "id": "exp_demo_001",
    "userId": "usr_demo_001",
    "templateId": "INVOICE",
    "sessionName": "Lote de prueba",
    "status": "completed"
  }
}

Firma

Verificacion de HMAC SHA-256

El digest se calcula sobre el string JSON exacto. La formula conceptual es: `sha256 = HMAC(secret, rawBody)`.

Node.js + Express

import crypto from 'crypto';
import express from 'express';

const app = express();

app.post('/kapture/webhook', express.raw({ type: 'application/json' }), (req, res) => {
  const signatureHeader = req.header('x-kapture-signature');
  if (!signatureHeader) return res.status(400).send('missing signature');

  const [algorithm, digest] = signatureHeader.split('=');
  if (algorithm !== 'sha256' || !digest) {
    return res.status(400).send('invalid signature format');
  }

  const expectedDigest = crypto
    .createHmac('sha256', process.env.KAPTURE_WEBHOOK_SECRET)
    .update(req.body)
    .digest('hex');

  const valid = crypto.timingSafeEqual(
    Buffer.from(digest, 'hex'),
    Buffer.from(expectedDigest, 'hex')
  );

  return valid ? res.status(202).send('accepted') : res.status(401).send('invalid');
});

Next.js App Router

import crypto from 'crypto';

export async function POST(request) {
  const rawText = await request.text();
  const signatureHeader = request.headers.get('x-kapture-signature');

  if (!signatureHeader) {
    return new Response('missing signature', { status: 400 });
  }

  const [algorithm, digest] = signatureHeader.split('=');
  const expectedDigest = crypto
    .createHmac('sha256', process.env.KAPTURE_WEBHOOK_SECRET)
    .update(rawText)
    .digest('hex');

  const valid =
    algorithm === 'sha256' &&
    digest &&
    crypto.timingSafeEqual(Buffer.from(digest, 'hex'), Buffer.from(expectedDigest, 'hex'));

  if (!valid) {
    return new Response('invalid signature', { status: 401 });
  }

  const payload = JSON.parse(rawText);
  console.log(payload);

  return new Response('accepted', { status: 202 });
}

Python + FastAPI

import hashlib
import hmac
from fastapi import FastAPI, Header, Request, HTTPException

app = FastAPI()
SECRET = "replace-me"

@app.post("/kapture/webhook")
async def receive_webhook(
    request: Request,
    x_kapture_signature: str | None = Header(default=None)
):
    if not x_kapture_signature:
        raise HTTPException(status_code=400, detail="missing signature")

    raw_body = await request.body()
    algo, digest = x_kapture_signature.split("=", 1)
    expected = hmac.new(
        SECRET.encode("utf-8"),
        raw_body,
        hashlib.sha256
    ).hexdigest()

    if algo != "sha256" or not hmac.compare_digest(digest, expected):
        raise HTTPException(status_code=401, detail="invalid signature")

    return {"status": "accepted"}

Entrega

Semantica de entrega que importa

Org-level

El dispatcher directo usa `axios.post()` y considera error cualquier no-2xx o timeout. El timeout actual es 5 segundos.

Legacy worker

La via legacy distingue 4xx y 5xx: 4xx se descartan, 5xx o fallos de red devuelven 500 para que la cola pueda reintentar.

No mezclar envelopes

El webhook org-level envia el payload publico directamente. El worker legacy envuelve los datos en un objeto con `eventId`, `eventType`, `timestamp` y `data`.

Usa siempre idempotencia

La forma recomendada es persistir `event:id` y confirmar rapido con `2xx`.