
# Sottoscrivi un webhook al changelog

`POST https://api.keenauth.com/v1/accounts/{account_id}/changelog-subscriptions` · proprietario dell'account o amministratore

Registra un endpoint HTTPS di tua proprietà che riceve un webhook `changelog.entry_published` ogni volta che una voce del [changelog](/docs/changelog) in una delle categorie scelte viene pubblicata. La richiesta viene effettuata con le credenziali di `keenauth login`: l'account deve essere quello con cui hai effettuato l'accesso e tu devi esserne il proprietario o un amministratore. Un account può avere al massimo 5 sottoscrizioni abilitate.

Ogni sottoscrizione ha un proprio segreto di firma, restituito una sola volta nella risposta. Le consegne seguono Standard Webhooks: ogni richiesta contiene le intestazioni `webhook-id`, `webhook-timestamp` e `webhook-signature`, e la firma (`v1,<base64>`) è un HMAC-SHA256 calcolato su `<webhook-id>.<webhook-timestamp>.<body>`. La chiave HMAC sono i 32 byte codificati dai 43 caratteri che seguono il prefisso `idp_whsec_` (base62, big-endian); gli ultimi 6 caratteri sono un checksum e non fanno parte della chiave. Verifica la firma e rifiuta un timestamp troppo distante dall'ora corrente (le librerie Standard Webhooks ammettono 5 minuti) prima di considerare attendibile il corpo.

Non inserire dati personali, credenziali o token nell'URL: viene memorizzato insieme alla sottoscrizione e mostrato a ogni proprietario e amministratore dell'account.

## Richiesta

Intestazioni:

| Intestazione | Obbligatoria | Descrizione |
| --- | --- | --- |
| `Authorization` | sì | `DPoP <access token>`: il token di accesso ricevuto dalla CLI quando hai effettuato l'accesso. |
| `DPoP` | sì | Una prova DPoP per questa richiesta (`htm` `POST`, `htu` l'URL di questa richiesta), firmata con la chiave a cui è vincolato il token di accesso; contiene l'hash del token di accesso e il nonce del server. |
| `Content-Type` | sì | `application/json`. |
| `Idempotency-Key` | no | Da 16 a 64 caratteri di `A-Z`, `a-z`, `0-9`, `_` e `-`. Ripetere la richiesta con la stessa chiave e lo stesso corpo entro 24 ore restituisce la stessa sottoscrizione senza il segreto, con `secret_redacted: true` e l'intestazione `Idempotent-Replayed: true`. |

Corpo:

| Campo | Obbligatorio | Descrizione |
| --- | --- | --- |
| `url` | sì | L'endpoint: `https`, porta 443, un nome host (non un indirizzo IP né un nome valido solo in locale come `.localhost`, `.local` o `.internal`), senza informazioni utente o frammento, al massimo 2048 byte. |
| `categories` | no | Uno o più tra `api`, `sdk`, `deprecation`, `security` e `incident`, ciascuno al massimo una volta. Ometti il campo per ricevere tutte le categorie; un array vuoto viene rifiutato. |

## Risposta

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

| Campo | Descrizione |
| --- | --- |
| `id` | La sottoscrizione (`chlg_…`). |
| `account_id` | L'account (`acct_…`). |
| `url` | L'endpoint. |
| `categories` | Le categorie ricevute, nell'ordine `api`, `sdk`, `deprecation`, `security`, `incident`. |
| `status` | `enabled`, oppure `disabled` quando le consegne continuano a fallire. |
| `created_at` | Quando la sottoscrizione è stata creata (RFC 3339, UTC). |
| `signing_secret` | Il segreto di firma (`idp_whsec_…`). Restituito solo in questa risposta; conservalo nel tuo gestore di segreti. |
| `secret_redacted` | `true` in caso di ripetizione idempotente, che non contiene `signing_secret`. |

## Errori

| Stato | Codice | Quando |
| --- | --- | --- |
| 400 | `malformed_request` | Il corpo non è JSON, oppure l'intestazione `Idempotency-Key` è ripetuta, vuota o malformata. |
| 400 | `unknown_field` | Il corpo contiene un campo diverso da `url` e `categories`. |
| 401 | `unauthenticated` | Non è stato inviato alcun token di accesso. |
| 401 | `invalid_token` | Il token di accesso non è valido, è scaduto o non appartiene alla CLI; accedi di nuovo con `keenauth login`. |
| 401 | `invalid_dpop_proof` | La prova DPoP manca, è ripetuta oppure non corrisponde alla richiesta o alla chiave del token. |
| 401 | `use_dpop_nonce` | Riprova con una prova che contenga il nonce dell'intestazione di risposta `DPoP-Nonce`. |
| 401 | `insufficient_user_authentication` | È richiesta l'autenticazione a più fattori (oppure, per proprietari e amministratori, una passkey o una chiave di sicurezza); accedi di nuovo. |
| 403 | `insufficient_permissions` | Non sei proprietario né amministratore dell'account. |
| 403 | `account_restricted` | Lo stato dell'account non consente modifiche. |
| 403 | `quota_exceeded` | L'account ha già 5 sottoscrizioni abilitate (`errors[].code` `changelog_subscription_limit_exceeded`). |
| 404 | `not_found` | L'account non esiste, non è quello con cui hai effettuato l'accesso, oppure non ne sei membro. |
| 413 | `payload_too_large` | Il corpo supera 8 KiB. |
| 415 | `unsupported_media_type` | Il corpo non è `application/json`. |
| 422 | `validation_failed` | `url` manca o non è valido, una categoria è sconosciuta o ripetuta, `categories` è vuoto, oppure un campo ha il tipo sbagliato. |
| 422 | `idempotency_key_reuse` | `Idempotency-Key` è stata usata con un altro corpo entro 24 ore. |
| 429 | `rate_limited` | Nell'ultima ora sono state create più di 10 sottoscrizioni per l'account; attendi `Retry-After`. |

## Consegna del webhook

Quando una voce viene pubblicata, KeenAuth invia una `POST` all'URL di ogni sottoscrizione abilitata le cui categorie includono una delle categorie della voce. La stessa voce non viene mai inviata due volte alla stessa sottoscrizione.

### Richiesta

| Intestazione | Descrizione |
| --- | --- |
| `Content-Type` | `application/json`. |
| `webhook-id` | L'evento `id` (`evt_…`). È lo stesso in ogni nuovo tentativo della stessa voce, quindi usalo per ignorare i duplicati. |
| `webhook-timestamp` | Tempo Unix in secondi in cui questo tentativo è stato firmato. È nuovo a ogni tentativo. |
| `webhook-signature` | `v1,<base64>`: l'HMAC-SHA256 descritto sopra, calcolato su `<webhook-id>.<webhook-timestamp>.<body>`. |

Il corpo è al massimo di 16 KiB. KeenAuth non segue i reindirizzamenti.

### 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 | Descrizione |
| --- | --- |
| `id` | L'evento (`evt_…`), uguale all'intestazione `webhook-id`. |
| `type` | Sempre `changelog.entry_published`. |
| `schema_version` | Sempre `1`. Nella versione 1 possono essere aggiunti nuovi campi facoltativi a `data`; ignora i campi che non conosci. |
| `occurred_at` | Quando la voce è stata pubblicata (RFC 3339). |
| `recorded_at` | Quando KeenAuth ha messo in coda la consegna (RFC 3339). |
| `data.id`, `data.url`, `data.title`, `data.date_published` | La voce, come nei feed del changelog. |
| `data.tags` | Uno o più tra `added`, `changed`, `fixed`, `removed`, `deprecation`, `security`, `breaking` e `incident`. |
| `data.categories` | Le categorie della sottoscrizione a cui appartiene la voce. |
| `data.surfaces` | Le superfici interessate: `kind` (`api`, `sdk`, `cli`, `provider`, `mcp` oppure `docs`), `name` e, quando noti, `version`. |
| `data.breaking` | `true` quando la modifica è breaking. |
| `data.sunset_at` | La data di rimozione di una deprecazione (`YYYY-MM-DD`), altrimenti `null`. |

Lo schema leggibile dalle macchine è `changelog.entry_published.v1`.

### Risposte e nuovi tentativi

Rispondi con uno stato `2xx` entro 15 secondi. Il corpo della risposta viene ignorato. Qualsiasi altro esito è un tentativo fallito e viene ritentato: un errore di connessione o TLS, un timeout, un reindirizzamento, `429`, `5xx` e ogni altro stato tranne `410`. Il primo tentativo viene effettuato subito; i successivi circa 1 minuto, 5 minuti, 30 minuti, 2 ore, 8 ore, 24 ore e 36 ore dopo il fallimento precedente (ogni intervallo varia fino al 10 per cento), 8 tentativi in circa 3 giorni in tutto.

Se anche l'ottavo tentativo fallisce, la consegna viene abbandonata e la sottoscrizione viene disabilitata (`status` `disabled`). Le consegne già in coda per essa vengono annullate.

### 410 Gone

Rispondi `410 Gone` per comunicare a KeenAuth che non vuoi più ricevere consegne. La sottoscrizione viene disabilitata subito, senza ulteriori tentativi, e le consegne in coda vengono annullate. Una sottoscrizione disabilitata non viene riabilitata e non conta nel limite di 5; per ricevere di nuovo le voci, creane una nuova. Ogni disabilitazione viene registrata nel registro di audit dell'account come `changelog.subscription_disabled`, con motivo `receiver_gone`, `permanent_failure` o `schedule_exhausted`.

## Esempio

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