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