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.
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.
Existen dos flujos de eventos con contratos, namespaces y públicos diferentes.
| Canal | Entre quiénes | Namespace | Origen 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.
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 | Sí |
rejected |
El titular denegó un consentimiento que aún estaba pendiente | Rejected |
Cerrar la jornada; no reintentar sobre el mismo consentimiento | Sí | Sí |
revoked |
Un consentimiento activo fue revocado | Revoked |
Detener el acceso de inmediato | Sí | Sí |
expired |
Terminó la vigencia del consentimiento | Expired |
Detener el acceso; continuar exige un consentimiento nuevo | Sí | No — en el canal regulado llega como revoked con newState=Expired |
replaced |
El alcance del consentimiento fue sustituido por completo | Authorised — sin cambio |
Releer el detalle: los permisos vigentes cambiaron | No | No |
merged |
Se agregaron permisos al alcance vigente | Authorised — sin 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.
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.
CloudEvents 1.0 simplificado.
Los atributos de extensión van en minúsculas y sin separadores porque así lo exige la especificación.
| Campo | Obligatorio | Descripción |
|---|---|---|
specversion | Sí | Versión de CloudEvents — siempre "1.0" |
contractversion | Sí | Versión mayor de este contrato (hoy "1"). No confundir con specversion |
type | Sí | El hecho ocurrido, en el namespace sensedia.consent.* |
source | Sí | Emisor (issuer del realm). Base del callback de detalle: GET {source}/grants/{grantId} |
subject | Sí | Identificador del consentimiento, en el formato consent:{grantId} |
id | Sí | UID RFC 4122 del evento — igual al encabezado x-idempotency-key |
time | Sí | Instante de la ocurrencia (ISO-8601 UTC), no de la entrega |
datacontenttype | Sí | application/json |
role | No | Rol en que actuó la plataforma: IPI/IPC (el cambio nació aquí) o PSBI/PSIP (llegó de un participante externo y se retransmite) |
consenttype | No | SHARE (compartición de datos) o PAYMENT (iniciación de pago) |
originorgid | No | El IPI/IPC de origen. Presente sólo cuando role es PSBI o PSIP |
data | Sí | { previousState, newState, reason? } — objeto abierto, puede ganar campos nuevos |
event | Sí | { 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 emparejados — IPI/PSBI siempre con SHARE, e
IPC/PSIP siempre con PAYMENT. Las combinaciones cruzadas no existen.
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:
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).
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}.
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.
| Encabezado | Contenido | ¿Cambia en el reintento? |
|---|---|---|
Authorization | Bearer <token> emitido por el API Gateway que publica la integración | — |
x-idempotency-key | UID RFC 4122 del evento, igual al campo id. Es la clave de deduplicación | No |
x-webhook-interaction-id | UID RFC 4122 del intento (opcional). Sirve para correlacionar logs, no para deduplicar | Sí |
Content-Type | application/cloudevents+json | — |
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.
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.
| Respuesta del receptor | Interpretación del emisor |
|---|---|
2xx (se recomienda 202) | ACK — evento aceptado. El cuerpo no es leído por el emisor |
| 4xx | Falla permanente, sin reintento — el evento pasa directamente a dead-letter |
| 408 y 429 | Excepciones: se tratan como falla transitoria, con reintento. En 429 el receptor puede enviar Retry-After |
| 5xx o timeout | Falla transitoria, con reintento según el cronograma |
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).
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:
x-idempotency-key, con una ventana de al menos 24 horas.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).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.
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.
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:
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).type y de estado: los tipos no reconocidos deben ignorarse respondiendo 2xx, nunca con 4xx/5xx — eso generaría reintentos y dead-letter innecesarios.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 cambio | Ejemplos | Efecto |
|---|---|---|
| Retrocompatible | Agregar campos a data o a la envoltura; agregar tipos de evento; agregar valores de estado o de reason | No incrementa contractversion |
| Incompatible | Quitar o renombrar un campo; cambiar la semántica de un campo existente; cambiar el significado de un type ya publicado | Genera 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.
subject; la única diferencia es consenttype: PAYMENT.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).