# Changes

The path carries the major version (`/v1`). Inside it we only add (fields, values, kinds, templates, endpoints, events); nothing that shipped is renamed, removed or changed in meaning. Ignore fields you don't know and accept unknown values in `state`, `tier`, `typ` and, in responses, an audience's `type`. A retired feature carries `Deprecation` and `Sunset` headers on every response that uses it, is listed under `deprecations` in `/.well-known/ckrets.json` and here, gets at least six months, then answers `410 feature-sunset` with the replacement. A `/v2` would be announced the same way and `/v1` would run a year beside it. A feature a stored secret depends on (a proof kind, a template, a wrapped-key format, a receipt claim) is grandfathered per secret until it expires: a sunset never breaks a reveal. Limits (sizes, TTLs, counts) are policy, not contract: they change on `/limits.md` and `GET /v1/limits` with 30 days' notice, abuse response excepted.

Every change to the API and its documented behaviour, newest first.

- 2026-11-06: `HEAD` answers like `GET` without a body on `GET /v1/limits`, `/.well-known/jwks.json` and the two public views (it was `404`); never on a reveal.
- 2026-11-06: a request can be answered encrypted: `POST /v1/requests` takes `encJwk` (your X25519 public key), the page `/q/` then encrypts the answer in the person's browser (`hpke.v1`) and ckrets stores ciphertext; the request's answer, receipt (`ce`) and `answerEncryption` say `client`. A key that requires client encryption may now make requests that carry `encJwk`.
- 2026-11-06: a key can require client encryption: `clientEncryptionRequired: true` at `POST /v1/keys`, or `PATCH /v1/me/key` (new; it turns the setting on and never off). Such a key's plain creates, hosted MCP creates and requests answer `400 plaintext-refused` (a new problem type), nothing made or counted. `GET /v1/me/key` and `GET /v1/limits` show it.
- 2026-11-06: every receipt carries `ce` (a new, additive claim): `true` when the value arrived encrypted on the sender's side, `false` when ckrets sealed it. Every create's `201` (a replay too) and a request answer's `200` carry `Ckrets-Encryption: client` or `server`, and the MCP `create_secret` result says `encryption` the same way. A receipt signed before this has no `ce`.
- 2026-11-06: every answer from the API carries `Ckrets-Request-Id` (a problem's `requestId` is the same id; quote it in a report)
- 2026-11-06: the beta launches: product version 0.9.0 (it was 0.9.0-draft.20261106) and the first dated contract, `Ckrets-Version: 2026-11-06`. Fixes come as 0.9.x, the next feature release as 0.10.0, and 1.0.0 when the beta's exit criteria are met.
- 2026-11-06: the contract is dated. Every answer carries `Ckrets-Version` (`2026-11-06`, the beta launch date); a request may send it, and a date not served is `400 version-unknown` (a new problem type) listing the dates that are. A request without the header gets the original contract. A new date comes only with a breaking change, announced here, and an old date is served for at least 12 months after the next. The helper, the Python client and the MCP server send the date they were built against.
- 2026-11-06: requests are in v1: `POST /v1/requests` (a key asks a person for a password, a code or text through the page `/q/<requestId>`, never in chat), `GET` and `DELETE /v1/requests/{requestId}`, and the page's own `GET …/public` and `POST …/answer`. The answer becomes a secret only the asking key opens (one reveal, an hour). The MCP tool `request_from_human` is renamed `request_secret` and is on the remote server with `get_request`. The request's `state` `open` is now `pending`; the token comes with `fulfilled`, not at create; `audience` and `purpose` are not taken. Limits: `/limits.md`, "Requests to a person".
- 2026-11-06: ckrets launches as a beta: the API label is 0.9.0 (0.9.0-draft.<date> until the launch), the packages' first release is 0.9.0 and every example pins `@0.9.0`; `/.well-known/ckrets.json` says `status: "beta"`. The path stays `/v1`; 1.0 follows when the beta's exit criteria are met.
- 2026-11-05: `expiresAt` is a whole second (the create's time plus `ttl`, cut down to the second; stored as shown, so at `expiresAt` a reveal is `410`); a repeated report of the same token from one address counts once against the 10 an hour; `passphrase`, `purpose`, report `reason` and key `label` lengths count Unicode code points, as `maxLength` does (a key `label` over 140 is `400`; it was cut to 64).
- 2026-11-05: two creates sent at once with the same `Idempotency-Key` make one secret: the second answers `409 idempotency-in-progress` with `Retry-After: 1`, and its retry gets the first answer.
- 2026-11-05: `/mcp` takes one JSON-RPC request per POST (a batch array is `400`, `-32600`, and nothing in it runs), and the door's 300 creates a minute per address counts `POST /mcp` with `POST /v1/secrets`; a reveal right after its create never misses the new token.
- 2026-11-05: an OIDC audience under GitLab or CircleCI must name the job by `sub`; `repository` or `job_workflow_ref` there (GitHub-only claims, which would never match) is `400 audience-invalid`.
- 2026-11-05: an unknown field in a create (`POST /v1/secrets` and the MCP `create_secret`), a key mint or a policy change is `400 invalid-request` naming it, and nothing is made; it was ignored, so a misspelt `audience` made a secret anyone with the URL could open. The puzzle template's create refuses unknown fields too, a top-level `ttl` included (its expiry is `puzzle.window`). A puzzle box now keeps its `purpose` as its title (at most 140 code points; the owner's status and list show it, `/public` never does).
- 2026-11-05: every field is camelCase: `clientEncrypted` and `wrappedKey` on create and reveal (and in the MCP tools), `encryption.dedicatedKey` in status. The snake_case names (`client_encrypted`, `wrapped_key`) are still accepted as input until launch; answers carry camelCase only. Puzzlebox's `plan: "plus"` is now `puzzle.plus: true` (the old form is accepted until launch).
- 2026-11-05: a request that reaches the API's own address instead of `https://ckrets.ai` is `403 wrong-host` (it was `403 invalid-request`); a key minted with `domain` keeps it, and `GET /v1/me/key` shows it (`verified: false` until v1.1) with the key's own tier and limits.
- 2026-11-05: a secret's status shows `encryption` (`custody`, `dedicated_key`), read-only; limits.md states when a deleted secret or account is gone (at once; from backups within 7 days; a key revocation within the key cache's lifetime).
- 2026-11-05: a secret's reveal `200` names its `sender`, exactly as `GET /v1/secrets/{token}/public` shows it (who created it, from the creator's credential; not a puzzle box's).
- 2026-11-05: once a reveal is counted it answers `200` with the value: a failure to delete the stored value or to write the access record no longer turns it into a `503` with the outcome unknown (a value whose deletion failed is removed by the next hourly sweep).
- 2026-11-05: a reveal's receipt is signed before the reveal is counted, so a signing failure spends nothing (a retryable `503`, or `503 key-unavailable`); it was a `503` with the outcome unknown after the reveal had been counted.
- 2026-11-05: a reveal whose key cannot be opened on our side is `503 key-unavailable` (`Retry-After`, `retryable: true`) and spends nothing; it was a `503` with the outcome unknown, after the reveal had been counted.
- 2026-11-05: the default TTL is 7 days on every tier (was 1 hour); the maximum is 7 days anonymous (was 4 hours) and 1 year with a free key (was 30 days); `"ttl":"never"` (no expiry) is reserved for a paid key and is `400 ttl-too-long` today.
- 2026-11-05: a GitHub Actions audience may name the job by `repository_id` (with `ref` or `job_workflow_ref`) instead of `repository`, so it keeps matching after the repository is renamed or transferred.
- 2026-11-05: inside a token's cooldown, a reveal answers the same `429` whatever passphrase is sent, right or wrong (a wrong one was slower to answer).
- 2026-11-05: a `409 idempotency-mismatch` counts as one create, and an `Idempotency-Key` used with three different requests is retired (`409 idempotency-key-retired` for every request with it, the right one included), so a key can no longer be used to test guesses of a value.
- 2026-11-05: over 5 puzzle hints, a hint over 280 characters, a game rubric over 800 or a key email over 254 is `400 invalid-request`; each was cut before (a game's judge read a cut rubric).
- 2026-11-05: a refused puzzle create spends nothing: every check runs before the session ticket is spent and the day's box is counted. A riddle or dated box's value must be `v1.<nonce>.<ciphertext>`, and its size cap (2 KB anonymous, 16 KB signed in) now applies; before, it was never checked, and a value over 8,192 characters was stored cut.
- 2026-11-05: a puzzle create's `puzzle.window` past the tier's maximum is `400 ttl-too-long` naming the longest allowed, and nothing is counted (it was cut to the maximum with `201`); a malformed window is `400`, not the 7-day default.
- 2026-11-05: a puzzle box's `PATCH .../policy` window past the tier's maximum is `400 ttl-too-long` and changes nothing (it was accepted and quietly cut to the maximum).
- 2026-11-05: status (the creator's alone) carries `audienceHint` when a live secret's audience key does not exist or is revoked, so no one can open it; the create still checks the key id's form only.
- 2026-11-05: a frozen token (reported, pending review) is `410 token-frozen` (was `410 token-gone`, same title): do not retry; ask its sender for a new token.
- 2026-11-05: an anonymous create refused by a limit (`428`, `429`) no longer spends the tier's daily budget, so one address's refused flood cannot pause the tier for others.
- 2026-11-05: `GET /v1/limits` no longer shows `budgetUsdPerDay` for the free key (a $1-a-day budget that was never enforced); a key's bound is its 1,000 creates a day.
- 2026-11-05: an expired secret's value is removed within the hour (an hourly sweep), its record answers status for 30 days; a create refused by a rate limit spends nothing; a string with a lone surrogate is `400` (it was stored as U+FFFD); a report reason over 2,000 characters is `400` (before this change, a longer one was cut to 200; today nothing is cut).
- 2026-11-05: a reveal that fails after it was counted (a throttled receipt or store) answers `503` with `outcome: "unknown"` and `check` its `/public` view, never "nothing was done".
- 2026-11-05: an `Authorization` header that is not `Bearer <credential>` is `401 invalid-credentials` (it was served as anonymous); `client_encrypted` must be a JSON boolean (`400` otherwise); an `Idempotency-Key` older than 24 hours makes a new secret even before its row is deleted.
- 2026-11-05: a write (`POST`, `PATCH`) must be `Content-Type: application/json` (or carry no body): anything else is `415 unsupported-media-type`; `/mcp` gives browsers the same CORS answer as `/v1` (only the first-party origins).
- 2026-11-05: free-key minting is capped at 10 an hour and 30 a day per address and 100 a day per network (IPv4 /24, IPv6 /48); a refused mint spends nothing.
- 2026-11-05: MCP `create_secret` leaves out `digestKey` unless asked (`includeDigestKey: true`).
- 2026-11-04: a client-encrypted create's `wrapped_key` over 4,096 characters is `400 invalid-request` (it was cut); it does not count toward the value's size.
- 2026-11-04: `GET /v1/limits` names the row in `app`; with `Ckrets-App: puzzlebox` an anonymous caller gets Puzzlebox's row (one box a day), the one its `RateLimit-*` headers count.
- 2026-11-04: a public-key audience's signature must cover `@method`, `@authority` and `@path` (and `content-digest` matching the body, when there is one); there is no nonce, so keep `expires` short (docs/signing.md).
- 2026-11-04: a `Ckrets-App` header counts only on an app's own routes; on an agent-API route (a plain create, keys, report, receipts, status, revoke) it is ignored and the answer is `api`'s.
- 2026-11-04: the in-flight cap (4, 2 on `/mcp`) was never enforced and is gone from the docs and `GET /v1/limits` for v1 (a pre-launch change); the per-address and per-key rates are the concurrency control.
- 2026-11-04: when a key's lease ends it can make nothing new, but the secrets it made live to their own TTL; only revoking the key revokes them.
- 2026-11-04: a revoke after the last reveal is a `204` that leaves status `revealed`; an `Idempotency-Key` over 64 characters is `400`; MCP `revoke_secret` answers `{revoked: true}`.
- 2026-11-04: a report (`POST /v1/report`) freezes a bearer token, but only records an audience-bound one (its recipient still opens it); 10 reports an hour per address, one per token per address counted; status shows `reports`, `frozen` and `lastCase`.
- 2026-11-04: a create refused as invalid (`400`, `413`) no longer counts against your creates; the request is checked before anything is counted.
- 2026-11-04: a key's scopes hold on every route: a key without `reveal` cannot reveal (even a secret addressed to it), without `status` or `revoke` it cannot read or revoke a secret's status; `403 scope-missing`. The manage token is unaffected.
- 2026-11-04: a CI (OIDC) audience must name one job: `sub`, or `repository` with `ref` or `job_workflow_ref`; an issuer alone or a repository alone is `400 audience-invalid` (before, it let every repository, or every branch, reveal).
- 2026-11-04: a `403 audience-mismatch` no longer spends the token's reveal attempts (a stranger holding the URL cannot use them up); it is still recorded, with a deny receipt and the alarm. A wrong passphrase still counts.
- 2026-11-04: an `Idempotency-Key` is yours alone (your address when anonymous, else your key) and bound to the request: the same key with a different request is `409 idempotency-mismatch`, nothing made; a replay of the same request returns the first answer for 24 hours.
- 2026-11-04: a dated box's `opensAt` is kept to the whole second, rounded up, so the time a `425 not-yet-open` reports is exactly when it opens.
- 2026-11-04: `GET /v1/limits` says which deployment answered (`stage`: `prod` is https://ckrets.ai; any other value is a test deployment).
- 2026-11-04: a `503 temporarily-unavailable` with `outcome: "unknown"` means a write may have happened (read `check` first); the same puzzle commitment as the last counted claim, sent again, answers as the first time and spends nothing (`replayed: true`).
- 2026-11-04: a `503` with no `Retry-After` and no problem body comes from the platform in front of the API (shedding load): nothing was done; wait 2 seconds and retry.
- 2026-11-03: a call the service is briefly too busy to take answers `503` with problem type `temporarily-unavailable` and `Retry-After`, and does nothing (before, it could answer `500 internal`).
- 2026-11-03: a proof-of-work stamp is good from the network it was issued to (IPv4 /24, IPv6 /64), not only the exact address; anonymous creates also have a per-network ceiling (240 an hour, 1,200 a day per IPv4 /24 or IPv6 /48), and a refusal there names `netCreatesPerHour` or `netCreatesPerDay`.
- 2026-11-02: before launch, `Ckrets-Stamp`, `Ckrets-Manage-Token` and `/v1/keys/me` were removed, not aliased (pre-launch renames are free): use `Ckrets-Proof: stamp=…`, `Authorization: Bearer ckm_…` and `/v1/me/key`.
- 2026-11-02: the production build: the signed-agent tier arrives in v1.1, not v1; the free key's default TTL was 1 hour, as on every tier (since 2026-11-05 it is 7 days); key minting's cap per address is published.
- 2026-10-23: the anonymous tier shrank, and a signed-agent tier (Web Bot Auth) was added to the tier table.
