1. Preparacion

Pre-requisitos

Antes de tocar codigo, conviene confirmar estas piezas. Sin ellas, el debugging se vuelve ruidoso muy rapido.

Accesos y datos

  • Acceso al panel de Kapture con una organizacion activa.
  • Endpoint HTTPS que pueda recibir POST JSON.
  • Secret compartido para HMAC SHA-256.
  • Logs del receptor disponibles para inspeccionar headers y body.

Conocimiento minimo

  • El receptor debe preservar el body crudo.
  • La respuesta debe ser `2xx` cuando el evento ya fue aceptado.
  • `event + id` debe tratarse como llave de idempotencia.
  • `record.created` y `expedient.completed` se disparan en momentos distintos.

2. Configuracion

Configurar la organizacion

La configuracion publica vive en el documento de organizacion y se administra desde el panel en `settings/organization/general`.

Campo Ubicacion Uso
webhookEnabled organizations/{orgId} Activa o apaga la emision del webhook saliente.
webhookUrl organizations/{orgId} URL HTTPS destino del POST firmado.
webhookSecret organizations/{orgId} Secret compartido usado para firmar el body.
webhookEvents organizations/{orgId} Lista de eventos habilitados: `record.created`, `expedient.completed`.
webhookUpdatedAt organizations/{orgId} Marca de tiempo de la ultima actualizacion.
Compatibilidad importante

Si una organizacion antigua tiene URL y secret, pero no `webhookEvents`, el backend resuelve por compatibilidad solo `record.created`. Si se quiere `expedient.completed`, hay que guardar explicitamente el array de eventos.

3. Receptor

Crear un receptor minimo

El requisito tecnico mas importante es conservar el body crudo antes de parsear JSON. El ejemplo siguiente usa Express con `express.raw()`.

Ejemplo Node.js + Express

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

const app = express();
const secret = process.env.KAPTURE_WEBHOOK_SECRET;

app.post(
  '/kapture/webhook',
  express.raw({ type: 'application/json' }),
  (req, res) => {
    const signature = req.header('x-kapture-signature');
    const event = req.header('x-kapture-event');
    const rawBody = req.body;

    if (!secret || !signature || !Buffer.isBuffer(rawBody)) {
      return res.status(400).send('invalid request');
    }

    const expected = crypto
      .createHmac('sha256', secret)
      .update(rawBody)
      .digest('hex');

    const [algorithm, digest] = signature.split('=');
    const valid =
      algorithm === 'sha256' &&
      digest &&
      crypto.timingSafeEqual(Buffer.from(digest, 'hex'), Buffer.from(expected, 'hex'));

    if (!valid) {
      return res.status(401).send('invalid signature');
    }

    const payload = JSON.parse(rawBody.toString('utf8'));
    const idempotencyKey = `${payload.event}:${payload.id}`;

    console.log({ event, idempotencyKey, payload });

    return res.status(202).send('accepted');
  }
);

app.listen(3000);
Buena practica

El receptor deberia confirmar rapido con `202` o `200` y mover el trabajo pesado a una cola, job interno o proceso asincrono propio.

4. Seguridad

Validar la firma correctamente

La firma se calcula sobre el JSON serializado exacto que sale del backend. Cualquier mutacion del body antes de verificar rompe el digest.

  1. 1
    Lee `X-Kapture-Signature`. El formato esperado es `sha256=<hex_digest>`.
  2. 2
    Usa el body crudo exacto. En Node debe ser un `Buffer`; en Python, bytes sin reinterpretar.
  3. 3
    Calcula HMAC SHA-256 con el secret compartido. La comparacion debe hacerse con una funcion de tiempo constante.
  4. 4
    Solo entonces parsea JSON. Primero se verifica integridad; despues se transforma el payload.

5. Pruebas

Probar `record.created` y `expedient.completed`

Los dos eventos requieren escenarios distintos, asi que lo mejor es validarlos por separado.

Prueba de `record.created`

  • Activa `record.created` en el panel.
  • Apunta el webhook a un endpoint visible o a `webhook.site`.
  • Genera una captura que produzca registros.
  • Confirma que llega `event=record.created` con `parent_id` y `data` aplanado.

Prueba de `expedient.completed`

  • Activa `expedient.completed` en el panel.
  • Procesa un expediente hasta que termine sin `errorMessage`.
  • Confirma que llega `event=expedient.completed` y que `data.imageBytes` no existe.
  • Verifica que `type` corresponda a `templateId` o `UNKNOWN`.

6. Produccion

Checklist de go-live

  • El receptor usa raw body y valida la firma con tiempo constante.
  • El sistema receptor deduplica por `event + id`.
  • La respuesta HTTP sale en menos de 5 segundos.
  • Existe trazabilidad minima: request ID, event, id, status y timestamp.
  • Los eventos activados en `webhookEvents` coinciden con el flujo real esperado.
  • La prueba con `webhook.site` o un receptor temporal ya fue exitosa.
No asumir retries por costumbre

El quickstart deja la integracion viva, pero la postura exacta de retries depende del canal y del trigger que emite el evento. La pagina de arquitectura detalla esas diferencias.