Developers

Webhooks

Recibe eventos firmados de Consensa y verifica su autenticidad.

Consensa entrega eventos a los endpoints HTTPS que registras en el Portal. Los webhooks son la forma de enterarte de hechos que no nacen de una llamada tuya: una revocación hecha por el titular, un artifact que quedó listo, o la obligación de avisarle a un titular REDEC.

El registro de endpoints y la rotación de su secreto se hacen desde el Portal, no desde esta API.

El envelope

Al crear un webhook puedes configurar Header name y Header value opcionales, por ejemplo Authorization y Bearer tu-token. Completa ambos o deja ambos vacíos. El header se incluye en cada entrega y reintento; su valor se guarda cifrado y no se vuelve a mostrar en el Portal. No puedes reemplazar los headers x-consensa-* ni los headers de transporte como Host, Content-Type o Content-Length.

Cada entrega es un POST con este cuerpo:

{
  "id": "evt_01J8F3S2Q4",
  "sequence": 4821,
  "type": "redec.notification.required",
  "occurredAt": 1787702400500,
  "data": { }
}
CampoPara qué sirve
idIdentificador único del evento. Úsalo para deduplicar: una entrega puede repetirse.
sequenceSecuencia global monótona. Puede tener saltos por otros tenants, eventos internos o tipos a los que no te suscribiste.
typeTipo del evento (ver el catálogo más abajo).
occurredAtCuándo ocurrió el hecho, en milisegundos epoch.
dataCuerpo específico del tipo de evento.

Y estos headers:

content-type: application/json
user-agent: Consensa-Webhooks/1.0
x-consensa-event-id: evt_01J8F3S2Q4
x-consensa-delivery-id: dlv_8c21f0
x-consensa-timestamp: 1787702401
x-consensa-signature: v1=3f8a…

x-consensa-timestamp está en segundos epoch. x-consensa-delivery-id identifica el intento de entrega, no el evento: varios intentos del mismo evento comparten x-consensa-event-id y difieren en x-consensa-delivery-id.

Verificar la firma

La firma es un HMAC-SHA256, en hexadecimal minúsculo, sobre el string <timestamp>.<cuerpo exacto>, con el secreto del endpoint (whsec_…) como clave.

Verifícala sobre el cuerpo crudo, antes de parsear el JSON: cualquier reserialización cambia los bytes y rompe la firma.

import { createHmac, timingSafeEqual } from 'node:crypto';

const MAX_SKEW_SECONDS = 300;

export function verifyConsensaWebhook(rawBody, headers, secret) {
  const timestamp = headers['x-consensa-timestamp'];
  const received = headers['x-consensa-signature'];
  if (!timestamp || !received?.startsWith('v1=')) return false;

  // Rechaza entregas viejas para acotar el replay.
  const ageSeconds = Math.abs(Math.floor(Date.now() / 1000) - Number(timestamp));
  if (!Number.isFinite(ageSeconds) || ageSeconds > MAX_SKEW_SECONDS) return false;

  const expected = createHmac('sha256', secret)
    .update(`${timestamp}.${rawBody}`)
    .digest('hex');

  const a = Buffer.from(expected, 'hex');
  const b = Buffer.from(received.slice(3), 'hex');
  return a.length === b.length && timingSafeEqual(a, b);
}

Usa siempre comparación en tiempo constante. Nunca proceses un evento cuya firma no verificaste.

Responder y reintentos

Responde 2xx apenas aceptes el evento, y haz el trabajo real de forma asincrónica. Cualquier respuesta que no sea 2xx, o un error de red, hace que Consensa reintente.

  • Hasta 8 intentos por entrega.
  • Backoff exponencial que empieza en 1 minuto y se acota en 15 minutos.
  • Después del octavo intento la entrega queda terminal y no se reintenta.

Diseña tu handler como idempotente: deduplica por id y acepta que un evento te llegue más de una vez.

Catálogo de eventos

typeCuándo
consent_action.createdSe registró una decisión de consentimiento (otorgamiento, denegación o revocación).
redec.consent_artifact.readyEl respaldo documental de un consentimiento REDEC quedó custodiado y descargable.
redec.notification.requiredNació la obligación de avisar al titular de un otorgamiento o una revocación REDEC.
redec.access.occurredSe confirmó un acceso REDEC con resultado accessed.
redec.access.failedSe confirmó un acceso REDEC con resultado failed.
redec.access.cancelledSe confirmó un acceso REDEC con resultado cancelled.
data_subject_request.createdEl titular creó una solicitud de derechos.
data_subject_request.resolvedUna solicitud de derechos llegó a un estado terminal.
data_subject_request.review_startedComenzó la revisión, también al resolver directamente una solicitud recién recibida.
data_subject_request.additional_information_requestedLa organización pidió antecedentes al titular.
data_subject_request.additional_information_receivedSe incorporaron antecedentes, solicitados o espontáneos, incluso sin cambio de estado.
data_subject_request.deadline_extendedSe amplió el plazo de respuesta.
data_subject_request.fulfillment_confirmedEl tenant confirmó la ejecución externa del derecho.

Al registrar un endpoint eliges a qué tipos se suscribe.

Solicitudes del titular

Los siete eventos data_subject_request.* llevan schemaVersion: 1, requestId, customerId, rightType, status y requestVersion. Esta última es la secuencia del historial del caso: identifica la versión a la que corresponde el hecho. La confirmación de ejecución conserva la versión de la resolución; no agrega una transición de estado. Los eventos históricos de creación y resolución pueden no incluir requestVersion.

Los eventos intermedios agregan previousStatus, dueAt (plazo vigente, en milisegundos epoch) y actorType (data_subject o tenant_user). Además:

EventoDatos específicos
createddueAt; requestVersion vale 1.
additional_information_receivedrespondsToInformationRequest distingue una respuesta de un aporte espontáneo; attachmentCount cuenta sólo los archivos de ese aporte.
deadline_extendedpreviousDueAt conserva el plazo anterior; dueAt contiene el nuevo.
resolvedresolutionOutcome (resolved, partially_resolved o rejected) y fulfillmentRequired.
fulfillment_confirmedoutcome (confirmed o partially_confirmed), performedAt declarado por el tenant y recordedAt controlado por Consensa.

Por ejemplo, un aporte del titular puede dejar el estado intacto:

{
  "schemaVersion": 1,
  "requestId": "request-9f2",
  "customerId": "customer-4821",
  "rightType": "suppression",
  "requestVersion": 4,
  "status": "in_review",
  "previousStatus": "in_review",
  "dueAt": 1790208000000,
  "actorType": "data_subject",
  "respondsToInformationRequest": false,
  "attachmentCount": 1
}

Los webhooks no llevan textos libres, nombres de archivos, datos de contacto ni referencias de autenticación. Consulta los antecedentes y archivos con la API de solicitudes, usando dsr:manage. Resuelve siempre el estado actual por API cuando recibas eventos fuera de orden; deduplica por el id del envelope, ya que la ejecución y la resolución pueden compartir requestVersion.

Los cinco tipos nuevos se ofrecen para suscripción explícita; los endpoints existentes conservan sus suscripciones. Los payloads internos anteriores a esta ampliación no se entregan como eventos públicos.

redec.notification.required

Es el evento con más consecuencias y vale la pena detallarlo. Se emite una vez por otorgamiento y una vez por revocación, apenas el hecho queda registrado.

{
  "id": "evt_01J8F3S2Q4",
  "sequence": 4821,
  "type": "redec.notification.required",
  "occurredAt": 1787702400500,
  "data": {
    "notification_reason": "grant",
    "redec_consent_id": "redec-consent-9c2",
    "internal_consent_code": "R0K7Q2M9X4V8B1N6C3Z",
    "customer_tenant_id": "customer-tenant-4f1",
    "consent_action_id": "consent-action-8e0",
    "delivery_responsibility": "tenant",
    "channel_policy": "tenant_determined",
    "channel": "digital",
    "granted_at": 1787702400000,
    "initial_access_valid_until": 1789430400000,
    "finality_code": "2",
    "objective_codes": ["01"],
    "medium_code": "1"
  }
}

Con notification_reason: "revoke" el payload trae revoked_at en lugar de los campos de otorgamiento.

Lo que haces depende de delivery_responsibility:

  • consensa: nada. Consensa envía el aviso por correo en nombre de tu organización y registra la evidencia. El evento sólo trae el designation_id opaco; el email del titular nunca viaja.
  • tenant: envías el aviso por tus canales y después registras su evidencia con POST /v1/redec/consents/{redecConsentId}/notification-evidence. El destino lo resuelves desde tus propios registros: el evento no lo transporta.

El evento no lleva datos de deuda, credenciales CMF ni datos de contacto del titular.

Si pierdes un evento

Los webhooks son el mecanismo primario, no el único. Para reconciliar la obligación de aviso REDEC existe una vista durable:

curl --get "$CONSENSA_API_URL/v1/redec/notification-requirements" \
  --data-urlencode "status=pending" \
  --header "Authorization: Bearer $CONSENSA_API_KEY"

Devuelve una página determinista de obligaciones con su estado de entrega conocido, y un nextCursor para continuar. Úsala para auditar y recuperarte de interrupciones en las entregas.

En esta página