
# Abonner un webhook au journal des modifications

`POST https://api.keenauth.com/v1/accounts/{account_id}/changelog-subscriptions` · propriétaire du compte ou administrateur

Enregistre un point de terminaison HTTPS qui vous appartient et qui reçoit un webhook `changelog.entry_published` chaque fois qu'une entrée du [journal des modifications](/docs/changelog) appartenant à l'une des catégories choisies est publiée. La requête est effectuée avec les identifiants de `keenauth login` : le compte doit être celui dans lequel vous vous êtes connecté, et vous devez en être le propriétaire ou un administrateur. Un compte peut avoir au plus 5 abonnements activés.

Chaque abonnement possède son propre secret de signature, renvoyé une seule fois dans la réponse. Les livraisons suivent Standard Webhooks : chaque requête contient les en-têtes `webhook-id`, `webhook-timestamp` et `webhook-signature`, et la signature (`v1,<base64>`) est un HMAC-SHA256 calculé sur `<webhook-id>.<webhook-timestamp>.<body>`. La clé HMAC correspond aux 32 octets encodés par les 43 caractères qui suivent le préfixe `idp_whsec_` (base62, big-endian) ; les 6 derniers caractères forment une somme de contrôle et ne font pas partie de la clé. Vérifiez la signature et rejetez un horodatage trop éloigné de l'heure actuelle (les bibliothèques Standard Webhooks acceptent 5 minutes) avant de faire confiance au contenu.

Ne placez pas de données personnelles, d'identifiants ou de jetons dans l'URL : elle est stockée avec l'abonnement et affichée à tous les propriétaires et administrateurs du compte.

## Requête

En-têtes :

| En-tête | Obligatoire | Description |
| --- | --- | --- |
| `Authorization` | oui | `DPoP <access token>` : le jeton d'accès reçu par la CLI lors de votre connexion. |
| `DPoP` | oui | Une preuve DPoP pour cette requête (`htm` `POST`, `htu` l'URL de cette requête), signée avec la clé à laquelle le jeton d'accès est lié ; elle contient le hachage du jeton d'accès et le nonce du serveur. |
| `Content-Type` | oui | `application/json`. |
| `Idempotency-Key` | non | 16 à 64 caractères parmi `A-Z`, `a-z`, `0-9`, `_` et `-`. Répéter la requête avec la même clé et le même corps dans les 24 heures renvoie le même abonnement sans le secret, avec `secret_redacted: true` et l'en-tête `Idempotent-Replayed: true`. |

Corps :

| Champ | Obligatoire | Description |
| --- | --- | --- |
| `url` | oui | Le point de terminaison : `https`, port 443, un nom d'hôte (pas une adresse IP ni un nom local uniquement tel que `.localhost`, `.local` ou `.internal`), sans informations d'utilisateur ni fragment, au plus 2048 octets. |
| `categories` | non | Parmi `api`, `sdk`, `deprecation`, `security` et `incident`, chacune au plus une fois. Omettez le champ pour recevoir toutes les catégories ; un tableau vide est refusé. |

## Réponse

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

| Champ | Description |
| --- | --- |
| `id` | L'abonnement (`chlg_…`). |
| `account_id` | Le compte (`acct_…`). |
| `url` | Le point de terminaison. |
| `categories` | Les catégories reçues, dans l'ordre `api`, `sdk`, `deprecation`, `security`, `incident`. |
| `status` | `enabled`, ou `disabled` lorsque les livraisons ont continué d'échouer. |
| `created_at` | Date de création de l'abonnement (RFC 3339, UTC). |
| `signing_secret` | Le secret de signature (`idp_whsec_…`). Renvoyé uniquement dans cette réponse ; conservez-le dans votre gestionnaire de secrets. |
| `secret_redacted` | `true` lors d'une rejouabilité idempotente, qui ne contient pas `signing_secret`. |

## Erreurs

| Statut | Code | Cas |
| --- | --- | --- |
| 400 | `malformed_request` | Le corps n'est pas du JSON, ou l'en-tête `Idempotency-Key` est répété, vide ou mal formé. |
| 400 | `unknown_field` | Le corps contient un champ autre que `url` et `categories`. |
| 401 | `unauthenticated` | Aucun jeton d'accès n'a été envoyé. |
| 401 | `invalid_token` | Le jeton d'accès est invalide, expiré ou n'appartient pas à la CLI ; reconnectez-vous avec `keenauth login`. |
| 401 | `invalid_dpop_proof` | La preuve DPoP est absente, répétée ou ne correspond pas à la requête ou à la clé du jeton. |
| 401 | `use_dpop_nonce` | Réessayez avec une preuve contenant le nonce de l'en-tête de réponse `DPoP-Nonce`. |
| 401 | `insufficient_user_authentication` | L'authentification multifacteur est requise (ou, pour les propriétaires et administrateurs, une clé d'accès ou une clé de sécurité) ; reconnectez-vous. |
| 403 | `insufficient_permissions` | Vous n'êtes ni propriétaire ni administrateur du compte. |
| 403 | `account_restricted` | Le statut du compte ne permet pas de modifications. |
| 403 | `quota_exceeded` | Le compte possède déjà 5 abonnements activés (`errors[].code` `changelog_subscription_limit_exceeded`). |
| 404 | `not_found` | Le compte n'existe pas, n'est pas celui dans lequel vous vous êtes connecté, ou vous n'en êtes pas membre. |
| 413 | `payload_too_large` | Le corps dépasse 8 Kio. |
| 415 | `unsupported_media_type` | Le corps n'est pas `application/json`. |
| 422 | `validation_failed` | `url` est absent ou invalide, une catégorie est inconnue ou répétée, `categories` est vide, ou un champ a un type incorrect. |
| 422 | `idempotency_key_reuse` | Le `Idempotency-Key` a été utilisé avec un autre corps dans les 24 heures. |
| 429 | `rate_limited` | Plus de 10 abonnements ont été créés pour le compte au cours de la dernière heure ; attendez `Retry-After`. |

## Livraison des webhooks

Lorsqu'une entrée est publiée, KeenAuth envoie une `POST` à l'URL de chaque abonnement activé dont les catégories incluent l'une des catégories de l'entrée. La même entrée n'est jamais envoyée deux fois au même abonnement.

### Requête

| En-tête | Description |
| --- | --- |
| `Content-Type` | `application/json`. |
| `webhook-id` | L'identifiant de l'événement `id` (`evt_…`). Il est identique à chaque nouvelle tentative pour la même entrée ; utilisez-le pour ignorer les doublons. |
| `webhook-timestamp` | Heure Unix en secondes à laquelle cette tentative a été signée. Elle change à chaque tentative. |
| `webhook-signature` | `v1,<base64>` : le HMAC-SHA256 décrit ci-dessus, calculé sur `<webhook-id>.<webhook-timestamp>.<body>`. |

Le corps fait au plus 16 Kio. KeenAuth ne suit pas les redirections.

### Événement

```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"
  }
}
```

| Champ | Description |
| --- | --- |
| `id` | L'événement (`evt_…`), identique à l'en-tête `webhook-id`. |
| `type` | Toujours `changelog.entry_published`. |
| `schema_version` | Toujours `1`. De nouveaux champs facultatifs peuvent être ajoutés à `data` dans la version 1 ; ignorez les champs que vous ne connaissez pas. |
| `occurred_at` | Date de publication de l'entrée (RFC 3339). |
| `recorded_at` | Date à laquelle KeenAuth a mis la livraison en file d'attente (RFC 3339). |
| `data.id`, `data.url`, `data.title`, `data.date_published` | L'entrée, comme dans les flux du journal des modifications. |
| `data.tags` | Une ou plusieurs valeurs parmi `added`, `changed`, `fixed`, `removed`, `deprecation`, `security`, `breaking` et `incident`. |
| `data.categories` | Les catégories d'abonnement auxquelles appartient l'entrée. |
| `data.surfaces` | Les surfaces concernées : `kind` (`api`, `sdk`, `cli`, `provider`, `mcp` ou `docs`), `name` et, lorsqu'elle est connue, `version`. |
| `data.breaking` | `true` lorsque la modification est incompatible. |
| `data.sunset_at` | La date de retrait d'une obsolescence (`YYYY-MM-DD`), sinon `null`. |

Le schéma lisible par machine est `changelog.entry_published.v1`.

### Réponses et nouvelles tentatives

Répondez avec un statut `2xx` dans un délai de 15 secondes. Le corps de la réponse est ignoré. Tout autre résultat est une tentative échouée et est réessayée : une erreur de connexion ou TLS, un délai dépassé, une redirection, `429`, `5xx` et tout autre statut sauf `410`. La première tentative est effectuée immédiatement ; les suivantes environ 1 minute, 5 minutes, 30 minutes, 2 heures, 8 heures, 24 heures et 36 heures après l'échec précédent (chaque intervalle varie jusqu'à 10 pour cent), soit 8 tentatives sur environ 3 jours au total.

Si la huitième tentative échoue, la livraison est abandonnée et l'abonnement est désactivé (`status` `disabled`). Les livraisons déjà en file d'attente pour cet abonnement sont annulées.

### 410 Gone

Répondez `410 Gone` pour indiquer à KeenAuth que vous ne souhaitez plus recevoir de livraisons. L'abonnement est immédiatement désactivé, sans nouvelle tentative, et ses livraisons en file d'attente sont annulées. Un abonnement désactivé ne peut pas être réactivé et ne compte pas dans la limite de 5 ; créez-en un nouveau pour recevoir à nouveau des entrées. Chaque désactivation est consignée dans le journal d'audit du compte sous `changelog.subscription_disabled`, avec la raison `receiver_gone`, `permanent_failure` ou `schedule_exhausted`.

## Exemple

```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"]}'
```
