
# Suscribir un webhook al registro de cambios

`POST https://api.keenauth.com/v1/accounts/{account_id}/changelog-subscriptions` · propietario de la cuenta o administrador

Registra un endpoint HTTPS propio que recibe un webhook `changelog.entry_published` cada vez que se publica una entrada del [registro de cambios](/docs/changelog) en una de las categorías elegidas. La solicitud se realiza con las credenciales de `keenauth login`: la cuenta debe ser aquella en la que ha iniciado sesión, y usted debe ser su propietario o administrador. Una cuenta puede tener como máximo 5 suscripciones habilitadas.

Cada suscripción tiene su propio secreto de firma, devuelto una sola vez en la respuesta. Las entregas siguen Standard Webhooks: cada solicitud incluye los encabezados `webhook-id`, `webhook-timestamp` y `webhook-signature`, y la firma (`v1,<base64>`) es un HMAC-SHA256 sobre `<webhook-id>.<webhook-timestamp>.<body>`. La clave HMAC son los 32 bytes que codifican los 43 caracteres posteriores al prefijo `idp_whsec_` (base62, big-endian); los últimos 6 caracteres son una suma de control y no forman parte de la clave. Verifique la firma y rechace una marca de tiempo que esté demasiado alejada de la hora actual (las bibliotecas de Standard Webhooks permiten 5 minutos) antes de confiar en el cuerpo.

No incluya datos personales, credenciales ni tokens en la URL: se almacena con la suscripción y se muestra a todos los propietarios y administradores de la cuenta.

## Solicitud

Encabezados:

| Encabezado | Obligatorio | Descripción |
| --- | --- | --- |
| `Authorization` | sí | `DPoP <access token>`: el token de acceso que la CLI recibió al iniciar sesión. |
| `DPoP` | sí | Una prueba DPoP para esta solicitud (`htm` `POST`, `htu` la URL de esta solicitud), firmada con la clave a la que está vinculado el token de acceso; contiene el hash del token de acceso y el nonce del servidor. |
| `Content-Type` | sí | `application/json`. |
| `Idempotency-Key` | no | De 16 a 64 caracteres de `A-Z`, `a-z`, `0-9`, `_` y `-`. Repetir la solicitud con la misma clave y el mismo cuerpo en un plazo de 24 horas devuelve la misma suscripción sin el secreto, con `secret_redacted: true` y el encabezado `Idempotent-Replayed: true`. |

Cuerpo:

| Campo | Obligatorio | Descripción |
| --- | --- | --- |
| `url` | sí | El endpoint: `https`, puerto 443, un nombre de host (no una dirección IP ni un nombre solo local como `.localhost`, `.local` o `.internal`), sin información de usuario ni fragmento, de como máximo 2048 bytes. |
| `categories` | no | Cualquiera de `api`, `sdk`, `deprecation`, `security` y `incident`, cada uno como máximo una vez. Omita el campo para recibir todas las categorías; un array vacío se rechaza. |

## Respuesta

`201 Created` con `Cache-Control: no-store`:

| Campo | Descripción |
| --- | --- |
| `id` | La suscripción (`chlg_…`). |
| `account_id` | La cuenta (`acct_…`). |
| `url` | El endpoint. |
| `categories` | Las categorías recibidas, en el orden `api`, `sdk`, `deprecation`, `security`, `incident`. |
| `status` | `enabled`, o `disabled` cuando las entregas siguen fallando. |
| `created_at` | Cuándo se creó la suscripción (RFC 3339, UTC). |
| `signing_secret` | El secreto de firma (`idp_whsec_…`). Se devuelve solo en esta respuesta; guárdelo en su gestor de secretos. |
| `secret_redacted` | `true` en una repetición idempotente, que no incluye `signing_secret`. |

## Errores

| Estado | Código | Cuándo |
| --- | --- | --- |
| 400 | `malformed_request` | El cuerpo no es JSON, o el encabezado `Idempotency-Key` está repetido, vacío o mal formado. |
| 400 | `unknown_field` | El cuerpo tiene un campo distinto de `url` y `categories`. |
| 401 | `unauthenticated` | No se envió ningún token de acceso. |
| 401 | `invalid_token` | El token de acceso no es válido, ha caducado o no es el de la CLI; inicie sesión de nuevo con `keenauth login`. |
| 401 | `invalid_dpop_proof` | La prueba DPoP falta, está repetida o no coincide con la solicitud ni con la clave del token. |
| 401 | `use_dpop_nonce` | Reintente con una prueba que incluya el nonce del encabezado `DPoP-Nonce` de la respuesta. |
| 401 | `insufficient_user_authentication` | Se requiere autenticación multifactor (o, para propietarios y administradores, una clave de acceso o una clave de seguridad); inicie sesión de nuevo. |
| 403 | `insufficient_permissions` | No es propietario ni administrador de la cuenta. |
| 403 | `account_restricted` | El estado de la cuenta no permite cambios. |
| 403 | `quota_exceeded` | La cuenta ya tiene 5 suscripciones habilitadas (`errors[].code` `changelog_subscription_limit_exceeded`). |
| 404 | `not_found` | La cuenta no existe, no es la cuenta en la que ha iniciado sesión, o no es miembro de ella. |
| 413 | `payload_too_large` | El cuerpo supera 8 KiB. |
| 415 | `unsupported_media_type` | El cuerpo no es `application/json`. |
| 422 | `validation_failed` | `url` falta o no es válido, una categoría es desconocida o está repetida, `categories` está vacío, o un campo tiene un tipo incorrecto. |
| 422 | `idempotency_key_reuse` | Se usó `Idempotency-Key` con otro cuerpo en un plazo de 24 horas. |
| 429 | `rate_limited` | Se crearon más de 10 suscripciones para la cuenta en la última hora; espere `Retry-After`. |

## Entrega de webhooks

Cuando se publica una entrada, KeenAuth envía un `POST` a la URL de cada suscripción habilitada cuyas categorías incluyan alguna de las categorías de la entrada. La misma entrada nunca se envía dos veces a una misma suscripción.

### Solicitud

| Encabezado | Descripción |
| --- | --- |
| `Content-Type` | `application/json`. |
| `webhook-id` | El identificador del evento `id` (`evt_…`). Es el mismo en cada reintento de la misma entrada, así que úselo para ignorar duplicados. |
| `webhook-timestamp` | Tiempo Unix en segundos en el que se firmó este intento. Es nuevo en cada intento. |
| `webhook-signature` | `v1,<base64>`: el HMAC-SHA256 descrito arriba, sobre `<webhook-id>.<webhook-timestamp>.<body>`. |

El cuerpo tiene como máximo 16 KiB. KeenAuth no sigue redirecciones.

### Evento

```json
{
  "id": "evt_01jb0000000000000000000000",
  "type": "changelog.entry_published",
  "schema_version": 1,
  "occurred_at": "2026-11-04T00:00:00Z",
  "recorded_at": "2026-11-04T08:30:12Z",
  "data": {
    "id": "2026-11-04-example",
    "url": "https://docs.keenauth.com/en/changelog/#2026-11-04-example",
    "title": "Example entry",
    "date_published": "2026-11-04T00:00:00Z",
    "tags": ["deprecation"],
    "categories": ["api", "deprecation"],
    "surfaces": [{"kind": "api", "name": "management-v1", "version": "v1"}],
    "breaking": false,
    "sunset_at": "2027-11-04"
  }
}
```

| Campo | Descripción |
| --- | --- |
| `id` | El evento (`evt_…`), igual al encabezado `webhook-id`. |
| `type` | Siempre `changelog.entry_published`. |
| `schema_version` | Siempre `1`. Pueden añadirse nuevos campos opcionales a `data` dentro de la versión 1; ignore los campos que no conozca. |
| `occurred_at` | Cuándo se publicó la entrada (RFC 3339). |
| `recorded_at` | Cuándo puso KeenAuth en cola la entrega (RFC 3339). |
| `data.id`, `data.url`, `data.title`, `data.date_published` | La entrada, tal como aparece en los feeds del registro de cambios. |
| `data.tags` | Una o más de `added`, `changed`, `fixed`, `removed`, `deprecation`, `security`, `breaking` y `incident`. |
| `data.categories` | Las categorías de suscripción a las que pertenece la entrada. |
| `data.surfaces` | Las superficies afectadas: `kind` (`api`, `sdk`, `cli`, `provider`, `mcp` o `docs`), `name` y, cuando se conoce, `version`. |
| `data.breaking` | `true` cuando el cambio es disruptivo. |
| `data.sunset_at` | La fecha de retirada de una deprecación (`YYYY-MM-DD`); en caso contrario, `null`. |

El esquema legible por máquina es `changelog.entry_published.v1`.

### Respuestas y reintentos

Responda con cualquier estado `2xx` en un plazo de 15 segundos. El cuerpo de la respuesta se ignora. Cualquier otro resultado es un intento fallido y se reintenta: un error de conexión o de TLS, un tiempo de espera, una redirección, `429`, `5xx` y cualquier otro estado distinto de `410`. El primer intento se realiza de inmediato; los siguientes, unos 1 minuto, 5 minutos, 30 minutos, 2 horas, 8 horas, 24 horas y 36 horas después del fallo anterior (cada intervalo varía hasta un 10 por ciento), 8 intentos en unos 3 días en total.

Si el octavo intento falla, la entrega se abandona y la suscripción se deshabilita (`status` `disabled`). Las entregas que ya estaban en cola para ella se cancelan.

### 410 Gone

Responda `410 Gone` para indicar a KeenAuth que ya no desea recibir entregas. La suscripción se deshabilita de inmediato, sin más intentos, y sus entregas en cola se cancelan. Una suscripción deshabilitada no vuelve a habilitarse y no cuenta para el límite de 5; cree una nueva para volver a recibir entradas. Cada deshabilitación se registra en el registro de auditoría de la cuenta como `changelog.subscription_disabled`, con el motivo `receiver_gone`, `permanent_failure` o `schedule_exhausted`.

## Ejemplo

```bash
curl -X POST https://api.keenauth.com/v1/accounts/acct_01jb0000000000000000000000/changelog-subscriptions \
  -H "Authorization: DPoP $ACCESS_TOKEN" \
  -H "DPoP: $DPOP_PROOF" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: changelog-hooks-2026-10" \
  -d '{"url": "https://hooks.example.com/keenauth/changelog", "categories": ["deprecation", "security"]}'
```
