webhook

Webhooks de Consentimiento — Open Finance Chile · Sensedia
Contrato de integración · Open Finance Chile

Webhooks de Consentimiento (Sensedia → Institución)

La plataforma notifica al backend de la institución, de forma asíncrona, cada cambio relevante en el ciclo de vida de un consentimiento. La entrega es por HTTP POST hacia el endpoint que la institución registra como callback URL, con envoltura CloudEvents 1.0 y cuerpo application/cloudevents+json.

Versión del contrato1
EnvolturaCloudEvents 1.0
Namespacesensedia.consent.*
EntregaPush · al menos una vez
ActualizadoSeptiembre/2026
Bajo construcción

Esta funcionalidad se encuentra bajo construcción. La descripción a continuación refleja el contrato vigente y puede sufrir cambios hasta su publicación.

01Dos canales distintos — no confundir

Existen dos flujos de eventos con contratos, namespaces y públicos diferentes.

CanalEntre quiénesNamespaceOrigen del contrato
Regulado Plataforma ↔ demás participantes del SFA y Directorio sfa.consent.* NCG N°569 / Anexo Técnico N°3
De producto (este documento) Plataforma → backend de la institución sensedia.consent.* Contrato propio de la solución Sensedia

El namespace sensedia.consent.* es deliberadamente distinto del regulatorio: los tipos de este catálogo no cambian porque la CMF publique los suyos.

02Catálogo de eventos

El type nombra el hecho ocurrido, no el estado resultante.

El estado viaja en data.newState. Un mismo estado puede ser resultado de hechos distintos, y hay hechos que no cambian el estado.

Tipo (sensedia.consent.) Hecho que lo dispara data.newState Acción esperada del receptor Terminal CMF
created El consentimiento fue creado y espera la autorización del titular AwaitingAuthorisation Mostrar como pendiente; todavía no hay acceso No No
authorised El titular aprobó; el consentimiento queda activo Authorised Habilitar el consumo de las APIs con el token No
rejected El titular denegó un consentimiento que aún estaba pendiente Rejected Cerrar la jornada; no reintentar sobre el mismo consentimiento
revoked Un consentimiento activo fue revocado Revoked Detener el acceso de inmediato
expired Terminó la vigencia del consentimiento Expired Detener el acceso; continuar exige un consentimiento nuevo No — en el canal regulado llega como revoked con newState=Expired
replaced El alcance del consentimiento fue sustituido por completo Authorisedsin cambio Releer el detalle: los permisos vigentes cambiaron No No
merged Se agregaron permisos al alcance vigente Authorisedsin cambio Releer el detalle: los permisos vigentes cambiaron No No

Terminal significa que no habrá más eventos para ese consent:{grantId}. La columna CMF indica si el hecho está tipificado por la normativa: sólo authorised, revoked y rejected lo están; los demás existen únicamente en este contrato de producto.

Atención — replaced y merged

replaced y merged no cambian el estado: son los únicos casos en que previousState y newState vienen iguales. Un receptor que sólo observe el estado los ignoraría y quedaría con una visión desactualizada de lo que el titular consintió. Ante ellos, relea el detalle en GET {source}/grants/{grantId}.

El catálogo publica solamente hechos que ocurren: no se anticipan tipos para flujos que la plataforma todavía no ejecuta. Cuando se incorporen, entrarán como tipos nuevos — cambio retrocompatible, cubierto por la obligación del receptor de tolerar tipos desconocidos.

03Envoltura del evento

CloudEvents 1.0 simplificado.

Los atributos de extensión van en minúsculas y sin separadores porque así lo exige la especificación.

CampoObligatorioDescripción
specversionVersión de CloudEvents — siempre "1.0"
contractversionVersión mayor de este contrato (hoy "1"). No confundir con specversion
typeEl hecho ocurrido, en el namespace sensedia.consent.*
sourceEmisor (issuer del realm). Base del callback de detalle: GET {source}/grants/{grantId}
subjectIdentificador del consentimiento, en el formato consent:{grantId}
idUID RFC 4122 del evento — igual al encabezado x-idempotency-key
timeInstante de la ocurrencia (ISO-8601 UTC), no de la entrega
datacontenttypeapplication/json
roleNoRol en que actuó la plataforma: IPI/IPC (el cambio nació aquí) o PSBI/PSIP (llegó de un participante externo y se retransmite)
consenttypeNoSHARE (compartición de datos) o PAYMENT (iniciación de pago)
originorgidNoEl IPI/IPC de origen. Presente sólo cuando role es PSBI o PSIP
data{ previousState, newState, reason? } — objeto abierto, puede ganar campos nuevos
event{ streamId, sequence } — metadatos de ordenamiento

role, consenttype y originorgid viajan en la envoltura, y no dentro de data, porque describen el origen del evento y no el cambio de estado: así el receptor (o un gateway intermedio) puede enrutar y filtrar sin deserializar data.

Los roles del SFA ya vienen separados por dominio, de modo que role y consenttype están emparejadosIPI/PSBI siempre con SHARE, e IPC/PSIP siempre con PAYMENT. Las combinaciones cruzadas no existen.

04Estados transportados por el webhook

Cinco estados, con el mismo nombre y significado que el status de la Grant Management API.

Son un subconjunto de la máquina de estados del consentimiento — no hay traducción que hacer:

01AwaitingAuthorisationCreado, pendiente del titular
02AuthorisedActivo, habilita el consumo
03RejectedDenegado por el titular
04RevokedRevocado — detener el acceso
05ExpiredFin de la vigencia

newState es el estado completo, no un delta — por eso aplicar last-writer-wins por event.sequence es seguro. previousState es nulo cuando el consentimiento acaba de ser creado.

El campo reason es opcional y no exhaustivo: trátelo como texto libre, nunca como enum cerrado. Valores en uso hoy: psu_denied (denegado por el usuario), psu_request (revocado a solicitud del usuario) y expired (fin de la vigencia).

Eventos livianos (thin)

data lleva solamente la transición de estado — sin PII, sin RUT y sin montos. El detalle (permisos, finalidad, vigencia) se obtiene bajo demanda en GET {source}/grants/{grantId}.

05Ejemplo de evento

POST /webhooks/consent HTTP/1.1
Content-Type: application/cloudevents+json
Authorization: Bearer <access_token>
x-idempotency-key: a1b2c3d4-0011-4455-8899-aabbccddeeff
x-webhook-interaction-id: 761bab4e-0179-11ee-be56-0242ac120002

{
  "specversion": "1.0",
  "contractversion": "1",
  "type": "sensedia.consent.revoked",
  "source": "https://as.sensedia.example/realms/institucion",
  "subject": "consent:9d8a7c6b-1e2f-4a3b-8c9d-0e1f2a3b4c5d",
  "id": "a1b2c3d4-0011-4455-8899-aabbccddeeff",
  "time": "2026-07-27T14:30:00Z",
  "datacontenttype": "application/json",
  "role": "PSBI",
  "consenttype": "SHARE",
  "originorgid": "org-987654",
  "data": {
    "previousState": "Authorised",
    "newState": "Revoked",
    "reason": "psu_request"
  },
  "event": {
    "streamId": "consent:9d8a7c6b-1e2f-4a3b-8c9d-0e1f2a3b4c5d",
    "sequence": 3
  }
}

La ruta /webhooks/consent es ilustrativa: vale la callback URL que la institución registre en la suscripción.

06Encabezados HTTP

EncabezadoContenido¿Cambia en el reintento?
AuthorizationBearer <token> emitido por el API Gateway que publica la integración
x-idempotency-keyUID RFC 4122 del evento, igual al campo id. Es la clave de deduplicaciónNo
x-webhook-interaction-idUID RFC 4122 del intento (opcional). Sirve para correlacionar logs, no para deduplicar
Content-Typeapplication/cloudevents+json
Deduplique por el encabezado correcto

Deduplique siempre por x-idempotency-key. Deduplicar por x-webhook-interaction-id — que cambia en cada intento — haría que las reentregas legítimas se procesaran como eventos nuevos.

07Entrega, reintentos y ACK

Entrega solamente push, con garantía al menos una vez.

No hay API de consulta en esta fase. Las reentregas son esperadas, por lo que la idempotencia es obligatoria en el receptor.

  • Reintentos: 1 / 2 / 5 / 15 / 60 minutos.
  • Presupuesto total: 24 horas; agotado el presupuesto, el evento pasa a dead-letter y genera alerta operacional.
  • Timeout: ~10 s por intento.
Respuesta del receptorInterpretación del emisor
2xx (se recomienda 202)ACK — evento aceptado. El cuerpo no es leído por el emisor
4xxFalla permanente, sin reintento — el evento pasa directamente a dead-letter
408 y 429Excepciones: se tratan como falla transitoria, con reintento. En 429 el receptor puede enviar Retry-After
5xx o timeoutFalla transitoria, con reintento según el cronograma

Cuándo responder

El ACK confirma que el receptor aceptó la responsabilidad por el evento, no que ya terminó de procesarlo. Responda al aceptar — persista o encole el evento y devuelva 2xx de inmediato, procesando de forma asíncrona. Procesar antes de responder consume el presupuesto de timeout del intento: si se agota, el emisor lo trata como falla transitoria y reintenta, generando una entrega duplicada de algo que el receptor ya estaba procesando. El ACK es síncrono, en la respuesta del propio POST: no existe confirmación diferida.

El contrato no tipa el cuerpo de error a propósito — el emisor sólo registra la clase del status en log. Si el receptor quiere devolver detalle, sugerimos RFC 9457 (application/problem+json).

08Orden y deduplicación

No se garantiza el orden de llegada.

Cada consentimiento es un stream propio (event.streamId), con numeración monotónica y sin saltos (event.sequence) asignada en el origen, en el orden real de los cambios de estado. La entrega, sin embargo, no está serializada: si un intento entra en reintento, el evento siguiente del mismo consentimiento puede llegar antes. Es deliberado — serializar bloquearía el stream completo detrás de un receptor no disponible.

Por lo tanto, el receptor debe:

  1. Deduplicar por x-idempotency-key, con una ventana de al menos 24 horas.
  2. Reconstruir el orden por event.sequence, nunca por el orden de llegada: guardar el mayor sequence ya aplicado por streamId y descartar el que llegue con valor menor o igual (evento antiguo o reentrega).
  3. Usar la ausencia de saltos para detectar que un evento anterior aún no ha llegado.

Como data.newState es el estado completo, aplicar last-writer-wins por sequence es seguro. El evento es la autoridad sobre el estado; el callback sirve para el detalle (permisos y metadatos), no para confirmar el estado.

09Transporte y seguridad

La entrega es siempre sobre HTTPS (TLS 1.2+, se recomienda 1.3). La plataforma se autentica con un access token en el encabezado Authorization: Bearer, emitido por el API Gateway que publica la integración (OAuth2 client credentials).

La validación del token puede quedar en el propio API Gateway — es lo habitual cuando el endpoint de recepción se publica en él, y en ese caso el backend del receptor no necesita repetir la verificación. La exigencia es que ninguna solicitud llegue al procesamiento sin haber sido autenticada: si el endpoint también es alcanzable por fuera del gateway, el backend debe validar por su cuenta.

No hay firma del payload ni cifrado a nivel de aplicación: los eventos son livianos y no llevan datos sensibles.

10Compatibilidad — obligaciones del receptor

El objeto data es abierto y va a crecer: se agregarán campos nuevos conforme evolucione el producto, como cambio retrocompatible. Por eso el receptor debe:

  • Ignorar campos desconocidos en data y en la envoltura, en lugar de rechazar el mensaje. Los deserializadores estrictos deben relajarse (ej. Jackson FAIL_ON_UNKNOWN_PROPERTIES=false, JsonSerializerOptions permisivo en .NET).
  • No depender del orden de las claves ni de la ausencia de un campo.
  • Tolerar valores nuevos de type y de estado: los tipos no reconocidos deben ignorarse respondiendo 2xx, nunca con 4xx/5xx — eso generaría reintentos y dead-letter innecesarios.

11Política de versionamiento

El evento identifica su versión en contractversion, con la versión mayor del contrato (hoy "1"). Un receptor que necesite ramificar por versión debe hacerlo sobre ese valor, nunca sobre specversion.

Tipo de cambioEjemplosEfecto
RetrocompatibleAgregar campos a data o a la envoltura; agregar tipos de evento; agregar valores de estado o de reasonNo incrementa contractversion
IncompatibleQuitar o renombrar un campo; cambiar la semántica de un campo existente; cambiar el significado de un type ya publicadoGenera una versión mayor nueva

Una versión mayor nueva se anuncia con antelación y ambas conviven durante al menos 12 meses, para que la institución migre sin ventana de indisponibilidad. Nunca se reutiliza un nombre de type ya publicado con semántica distinta.

Sobre una eventual divergencia futura con la CMF: el namespace es independiente y estos tipos no cambian porque la CMF publique los suyos. Si la normativa define un homónimo con significado distinto, se trata como cambio incompatible (versión mayor nueva, comunicación previa y convivencia); si sólo agrega un estado que no teníamos, entra como tipo nuevo del catálogo — cambio retrocompatible.

12Fuera del alcance de este contrato

  • Registro y verificación de la suscripción del endpoint.
  • Eventos del Directorio.
  • Eventos de pago (la transacción). Nota: el consentimiento de pago sí está cubierto — usa el mismo catálogo, la misma máquina de estados y el mismo subject; la única diferencia es consenttype: PAYMENT.
  • Recuperación de eventos por el receptor (feed de reconciliación tipo desde={checkpoint}): no existe en esta fase. La recuperación depende de los reintentos automáticos dentro de la ventana de 24 h — una indisponibilidad mayor puede exigir reconciliación manual entre las partes.

Actualizado en Septiembre/2026 · Contrato de producto Sensedia — Open Finance Chile (SFA).

Undefined