Subscribe a webhook to the changelog
POST https://api.keenauth.com/v1/accounts/{account_id}/changelog-subscriptions · account owner or admin
Registers an HTTPS endpoint of your own that receives a changelog.entry_published webhook whenever a changelog entry in one of the chosen categories is published. The request is made with the credentials of keenauth login: the account must be the one you signed in to, and you must be its owner or admin. An account can have at most 5 enabled subscriptions.
Each subscription has its own signing secret, returned once in the response. Deliveries follow Standard Webhooks: every request carries the webhook-id, webhook-timestamp and webhook-signature headers, and the signature (v1,<base64>) is an HMAC-SHA256 over <webhook-id>.<webhook-timestamp>.<body>. The HMAC key is the 32 bytes that the 43 characters after the idp_whsec_ prefix encode (base62, big-endian); the last 6 characters are a checksum and are not part of the key. Verify the signature and reject a timestamp that is too far from the current time (the Standard Webhooks libraries allow 5 minutes) before you trust the body.
Do not put personal data, credentials or tokens in the URL: it is stored with the subscription and shown to every owner and admin of the account.
Request
Headers:
| Header | Required | Description |
|---|---|---|
Authorization |
yes | DPoP <access token>: the access token the CLI received when you signed in. |
DPoP |
yes | A DPoP proof for this request (htm POST, htu the URL of this request), signed with the key the access token is bound to; it carries the hash of the access token and the server nonce. |
Content-Type |
yes | application/json. |
Idempotency-Key |
no | 16 to 64 characters of A-Z, a-z, 0-9, _ and -. Repeating the request with the same key and body within 24 hours returns the same subscription without the secret, with secret_redacted: true and the Idempotent-Replayed: true header. |
Body:
| Field | Required | Description |
|---|---|---|
url |
yes | The endpoint: https, port 443, a host name (not an IP address or a local-only name such as .localhost, .local or .internal), no user information or fragment, at most 2048 bytes. |
categories |
no | Any of api, sdk, deprecation, security and incident, each at most once. Omit the field to receive every category; an empty array is refused. |
Response
201 Created with Cache-Control: no-store:
| Field | Description |
|---|---|
id |
The subscription (chlg_…). |
account_id |
The account (acct_…). |
url |
The endpoint. |
categories |
The categories received, in the order api, sdk, deprecation, security, incident. |
status |
enabled, or disabled when deliveries kept failing. |
created_at |
When the subscription was created (RFC 3339, UTC). |
signing_secret |
The signing secret (idp_whsec_…). Returned only in this response; store it in your secret manager. |
secret_redacted |
true on an idempotent replay, which carries no signing_secret. |
Errors
| Status | Code | When |
|---|---|---|
| 400 | malformed_request |
The body is not JSON, or the Idempotency-Key header is repeated, empty or malformed. |
| 400 | unknown_field |
The body has a field other than url and categories. |
| 401 | unauthenticated |
No access token was sent. |
| 401 | invalid_token |
The access token is invalid, expired or not the CLI's; sign in again with keenauth login. |
| 401 | invalid_dpop_proof |
The DPoP proof is missing, repeated or does not match the request or the token's key. |
| 401 | use_dpop_nonce |
Retry with a proof that carries the nonce of the DPoP-Nonce response header. |
| 401 | insufficient_user_authentication |
Multi-factor authentication (or, for owners and admins, a passkey or security key) is required; sign in again. |
| 403 | insufficient_permissions |
You are not an owner or admin of the account. |
| 403 | account_restricted |
The account's status does not allow changes. |
| 403 | quota_exceeded |
The account already has 5 enabled subscriptions (errors[].code changelog_subscription_limit_exceeded). |
| 404 | not_found |
The account does not exist, is not the account you signed in to, or you are not its member. |
| 413 | payload_too_large |
The body is larger than 8 KiB. |
| 415 | unsupported_media_type |
The body is not application/json. |
| 422 | validation_failed |
url is missing or invalid, a category is unknown or repeated, categories is empty, or a field has the wrong type. |
| 422 | idempotency_key_reuse |
The Idempotency-Key was used with another body within 24 hours. |
| 429 | rate_limited |
More than 10 subscriptions were created for the account in the last hour; wait for Retry-After. |
Webhook delivery
When an entry is published, KeenAuth sends one POST to the URL of every enabled subscription whose categories include one of the entry's categories. The same entry is never sent twice to one subscription.
Request
| Header | Description |
|---|---|
Content-Type |
application/json. |
webhook-id |
The event id (evt_…). It is the same on every retry of the same entry, so use it to ignore duplicates. |
webhook-timestamp |
Unix time in seconds at which this attempt was signed. It is new on every attempt. |
webhook-signature |
v1,<base64>: the HMAC-SHA256 described above, over <webhook-id>.<webhook-timestamp>.<body>. |
The body is at most 16 KiB. KeenAuth does not follow redirects.
Event
{
"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"
}
}
| Field | Description |
|---|---|
id |
The event (evt_…), equal to the webhook-id header. |
type |
Always changelog.entry_published. |
schema_version |
Always 1. New optional fields may be added to data within version 1; ignore fields you do not know. |
occurred_at |
When the entry was published (RFC 3339). |
recorded_at |
When KeenAuth queued the delivery (RFC 3339). |
data.id, data.url, data.title, data.date_published |
The entry, as in the changelog feeds. |
data.tags |
One or more of added, changed, fixed, removed, deprecation, security, breaking and incident. |
data.categories |
The subscription categories the entry belongs to. |
data.surfaces |
The affected surfaces: kind (api, sdk, cli, provider, mcp or docs), name and, when known, version. |
data.breaking |
true when the change is breaking. |
data.sunset_at |
The removal date of a deprecation (YYYY-MM-DD), otherwise null. |
The machine-readable schema is changelog.entry_published.v1.
Responses and retries
Answer with any 2xx status within 15 seconds. The response body is ignored. Any other outcome is a failed attempt and is retried: a connection or TLS error, a timeout, a redirect, 429, 5xx and every other status except 410. The first attempt is made at once; the following ones about 1 minute, 5 minutes, 30 minutes, 2 hours, 8 hours, 24 hours and 36 hours after the previous failure (each interval varies by up to 10 percent), 8 attempts in about 3 days in all.
If the eighth attempt fails, the delivery is abandoned and the subscription is disabled (status disabled). Deliveries already queued for it are cancelled.
410 Gone
Answer 410 Gone to tell KeenAuth that you no longer want deliveries. The subscription is disabled at once, without further attempts, and its queued deliveries are cancelled. A disabled subscription is not enabled again and does not count towards the limit of 5; create a new one to receive entries again. Every disabling is recorded in the account audit log as changelog.subscription_disabled, with the reason receiver_gone, permanent_failure or schedule_exhausted.
Example
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"]}'