{
 "openapi": "3.1.0",
 "info": {
  "title": "ckrets API",
  "version": "0.9.0",
  "summary": "Secret tokens for agents, handed over once, to a named recipient, with a receipt.",
  "description": "Put a value in, get a token and a URL. Whoever the token is for reveals it once (or until it expires), then it is gone.\nNo key for the first call: the anonymous tier allows 1 KB values, a 7-day maximum TTL, 1 reveal, 60 creates an hour and\n300 a day per IP. A free key (POST /v1/keys) raises every limit; the signed-agent tier (HTTP message signatures, Web\nBot Auth) is planned.\nEvery response from the API carries RateLimit-Limit, RateLimit-Remaining, RateLimit-Reset, Ckrets-Tier, Ckrets-App and Ckrets-Version\n(the dated contract; send Ckrets-Version to name one, or none for the original contract),\nand Cache-Control: no-store. A write (POST, PATCH) takes Content-Type: application/json, or no body and no type;\nanything else is 415 unsupported-media-type. Errors are application/problem+json with typed problems (branch on type; title varies);\na bad Authorization header is 401 invalid-credentials on every route; a 429 with Retry-After means slow down, a 429 without it means stop until\nresetsAt. Every create and reveal returns a receipt (compact JWS, ES256) verifiable offline against /.well-known/jwks.json.\nEvery time is UTC in RFC 3339 with Z and whole seconds (2026-10-24T01:11:09Z). Rate windows are fixed (the clock hour,\nthe UTC day), not rolling. There is no in-flight cap in v1: the rates are the control.\nVersions and stages: the path carries the API's major version (/v1); everything in this document is major\nversion 1. x-ckrets-stage marks release stages inside it (beta = live in 0.9, preview = live but opt-in,\nplanned = specified, not built), all additive, none changing the path; a planned path answers 501 not-in-v1 until it ships. Within a major we only add: optional request fields, response fields, response enum\nvalues, audience and proof kinds, templates, endpoints, events, problem types, receipt claims. We never rename, remove\nor change the meaning of anything that shipped. Clients ignore unknown fields and accept unknown values of enums\nmarked x-extensible-enum; request enums stay closed. A retired feature carries Deprecation (RFC 9745), Sunset\n(RFC 8594) and Link rel=deprecation headers on every response that uses it, a row in /.well-known/ckrets.json\ndeprecations and a line in /llms.txt, for at least six months, then answers 410 feature-sunset. A feature a stored secret depends on (a proof\nkind, a template, a wrapped-key format name, a receipt claim) is grandfathered per record until that record expires,\nso a sunset never breaks the reveal of a secret created before it. A new major (/v2) is for changes that cannot be\nadditive; /v1 then runs at least twelve months more. Limits (sizes, TTLs, counts, cooldowns, budgets) are policy,\nnot contract: they change at /limits.md and GET /v1/limits with 30 days' notice, abuse response excepted; the\ncompatibility rules cover shapes, not numbers. The same rules and common headers apply to first-party app routes\nand the puzzle layer's browser-facing routes.\nCredentials: one header, Authorization: Bearer (the scheme in any case), the kind read from the prefix (ck_live_ key\nor lease, ckm_ manage token, cka_ account session, a JWT for CI OIDC). Anonymous proofs: one header, Ckrets-Proof,\nkind=value (stamp; session for first-party apps). A first-party app's own layer holds no credential: its three calls\n(void, judging, daily counts) go to the private API with AWS SigV4 by its caller role.\nTokens are ck_ + 26 Crockford base32 characters from a cryptographic random source (130 bits, not time-ordered); manage tokens are ckm_ + 26 made the same way, also 130 bits. Receipts carry the issuing\nenvironment's origin as iss: https://ckrets.ai in production; pin the one you expect.\nIds: tokens ck_…, manage tokens ckm_…, request ids ckq_…, key ids key_… (give these to senders), keys ck_live_… (keep these).\nThe share url (https://ckrets.ai/r/<token>) is a page for humans that reveals on a click; agents reveal with GET /secrets/{token}.\nFormats: /docs/client-encryption.md, /docs/receipts.md, /docs/signing.md.\nTemplates: `onetime` (the default, for agents and people) and `puzzle` (riddle, game and dated\nboxes, used by the puzzlebox app at pleaseunbox.com). The API is the platform's one unlock service: one record, one\nledger, one access record and one set of limits, with tiers per (app, tier). First-party apps call it from their own\ndomains through a CORS allow-list with a public app id on every call (a browser holds no app secret); keys stay apart by class (agent secrets under the agent root, game boxes under the\nconsumer root, riddle and dated boxes under no platform key).\nNo model runs on this service; the puzzlebox's model calls (generator, normaliser, judge) live in its own layer.\n",
  "contact": {
   "name": "ckrets",
   "url": "https://ckrets.ai/security"
  },
  "termsOfService": "https://ckrets.ai/docs/terms"
 },
 "servers": [
  {
   "url": "https://ckrets.ai/v1",
   "description": "Production, live in beta (0.9.0). A staging or lab copy of these docs serves the same API at its own origin: use that origin as the server; nothing else changes (a CI OIDC token's aud stays https://ckrets.ai, problem type URIs stay https://ckrets.ai/problems/…). GET /v1/limits reports the stage that answered. Take the origin from your own configuration, never from a document; in production check stage is prod and pin receipts' iss to https://ckrets.ai."
  }
 ],
 "security": [
  {},
  {
   "bearerKey": []
  },
  {
   "httpSignature": []
  }
 ],
 "tags": [
  {
   "name": "secrets",
   "description": "Create, reveal, inspect and revoke secret tokens"
  },
  {
   "name": "batches",
   "description": "Many secrets to many named recipients at once (post-incident rotation), planned"
  },
  {
   "name": "requests",
   "description": "Ask a person for a password, a code or text through a page that names the asking key"
  },
  {
   "name": "keys",
   "description": "Mint, inspect, delegate and revoke keys and leases"
  },
  {
   "name": "webhooks",
   "description": "Signed event delivery to a verified host, planned"
  },
  {
   "name": "limits",
   "description": "Tiers, counters and budgets"
  },
  {
   "name": "trust",
   "description": "Receipts, reports and the signing keys"
  },
  {
   "name": "templates",
   "description": "Public metadata, attempt tickets, verifier claims and owner policy changes (the puzzle template)"
  },
  {
   "name": "private",
   "description": "Not part of the public API: these routes are served only by the private API (IAM, a first-party app's caller role). On the public API they answer 403 (void-not-allowed). Listed so a first-party app's layer has one spec; an agent never calls them"
  },
  {
   "name": "apps",
   "description": "First-party apps: sessions for anonymous makers, voids by the app layer, tiers per (app, tier)"
  }
 ],
 "paths": {
  "/secrets": {
   "post": {
    "tags": [
     "secrets"
    ],
    "operationId": "createSecret",
    "x-ckrets-stage": "beta",
    "summary": "Store a value under a policy and get a token and a URL",
    "security": [
     {},
     {
      "bearerKey": []
     },
     {
      "oidcToken": []
     },
     {
      "httpSignature": []
     },
     {
      "anonymousProof": []
     },
     {
      "accountSession": []
     }
    ],
    "description": "Storing a secret, by use case. This one call stores every kind; the use case is set by the body. Each row has a\nfull example below (the name in brackets).\n- Hand a value to whoever gets the link: value, ttl, reveals [anonymous].\n- The same, but the reader needs a passphrase you send another way: add passphrase [withPassphrase].\n- Hand a value to one agent or person with a ckrets key: audience {type: key} [toAgent].\n- Hand a value to a CI job: audience {type: oidc} with the job's claims; the job reveals with its OIDC token [toCiJob].\n- Hand a value to a server or an agent that holds a signing key: audience {type: publicKey} or {type: httpsig} [noView, toSignedAgent].\n- Keep the platform from ever reading it: clientEncrypted with a wrappedKey (/docs/client-encryption.md) [noView].\n- A riddle box that opens on the right answer: template puzzle, mode riddle, commitments and one wrap per accepted answer, clientEncrypted [riddleBox].\n- A game box opened by a judge's verdict: template puzzle, mode game, judge; the platform holds the key [gameBox].\n- A box that opens on a date: template puzzle, mode dated, opensAt, the key in the link [datedBox].\n- Many secrets to many recipients after an incident: POST /batches (planned).\n- Ask a person for a one-time code instead of storing one: POST /requests (planned).\nHand-off by default, storage when needed: the defaults suit a hand-off (ttl 7 days), and the same call keeps a\nsecret longer: up to 7 days anonymously, a year with a free key, no expiry (\"never\") on a paid key. The family\nlegacy vault and no-view HIPAA tokenization are separate products, designed but not specified here.\nAnonymous calls normally succeed without a stamp. When the anonymous tier is busy (more than 20 anonymous creates a second across the whole tier, whoever sends them; one\ncaller over its own rate gets 429, never 428) the API answers 428 (problem type\nproof-of-work-required) whose body carries `stamp: {alg, difficulty, nonce, expiresAt}` (also at GET /limits); the caller\nfinds a `counter` such that sha256(nonce + \":\" + counter) has `difficulty` leading zero bits (11 by default, about\n10 ms in Node; it never changes with load: only the operator sets it, the 428 and GET /limits give the current value,\nand a stamp is good only at the difficulty it was issued with), and resends\nwith the header `Ckrets-Proof: stamp=<nonce>:<counter>`. A stamp is good until its expiresAt, from the network it\nwas issued to (IPv4 /24, IPv6 /64, so an agent behind a rotating egress pool keeps it). Keyed callers never need a stamp.\nA CI job can create with its own OIDC token as the bearer (aud https://ckrets.ai) instead of a stored key: the\ncreator is the job's identity (issuer, repository, workflow), limits are a free key's counted per repository,\nand nothing is stored in CI.\nSet `audience` so that only a named recipient can reveal: a ckrets key, a CI OIDC identity, an HTTP-signature key\nor a recipient public key. Without `audience` the token is a bearer secret: anyone holding the URL reveals it once.\nWith `clientEncrypted: true` the value is ciphertext and `wrappedKey` is required (the schema enforces it); the\nplatform is then no-view. The response carries a keyed `digest` and its `digestKey` (see SecretCreated): the creator and\nthe recipient can confirm which value a receipt refers to; nobody else learns anything about the value from it.\n",
    "parameters": [
     {
      "name": "Ckrets-Proof",
      "in": "header",
      "required": false,
      "schema": {
       "type": "string"
      },
      "description": "Anonymous proof, kind=value: `stamp=<nonce>:<counter>` (proof of work, after a 428; parameters in the 428 body or GET /limits; sha256(nonce + ':' + counter) must have `difficulty` leading zero bits, before stamp.expiresAt, from the network it was issued to: IPv4 /24, IPv6 /64. A stamp lives 120 s and is good for any number of creates until then, each still counted; counter is a decimal number from 0, at most 15 digits; a stamp that is malformed, too weak, expired, from another network or issued at another difficulty is 428 again with a fresh stamp, never 400; a stamp is not read when none is required, and a 428 counts no create. Only this route asks for one, never /mcp) or `session=<ticket>` (POST /sessions, first-party apps; Turnstile is verified there only). One header for every proof kind."
     },
     {
      "name": "Idempotency-Key",
      "in": "header",
      "required": false,
      "schema": {
       "type": "string",
       "maxLength": 64
      },
      "description": "Optional, 1 to 64 characters (longer is 400, never cut). Only this route reads it: every other route ignores it (a POST /keys with one still mints a new key), and so does a puzzle create. Scoped to you: your address when anonymous (an IPv6 caller by its /64), else your key, or for a CI job its issuer and repository (every job of one repository shares that scope, as its creates are counted). Bound to the request: exactly value, ttl, reveals, audience, whether a passphrase is set, clientEncrypted and wrappedKey. purpose and the passphrase's value are not bound: a replay returns the first secret exactly as it was made, with the first purpose and passphrase, even if those changed. Someone who guesses your key and sends any other request (another value) gets 409, never your answer. The credential is checked first, so a replay needs the same caller; anonymous callers behind one address (a NAT, a shared runner) share one scope, so use an unguessable key (a UUID), never a counter. The key is recorded only when a secret is made: a create refused with 400, 413, 428 or 429 leaves it unused, and the resend is a first create. For 24 hours from the create that first used it (to the whole second) the same key and the same request return the first answer again: 201, the same body, Idempotent-Replay: true, nothing counted; the same key with a different request is 409 idempotency-mismatch, nothing made and one create counted; after three, the key is 409 idempotency-key-retired for every request. Without the header every create makes a new secret: a duplicate body is never a 409. After a 503 with outcome unknown the key stays reserved for 30 s (the create's answer was never stored): a retry with the same key is 409 idempotency-in-progress until then, and after that makes a new secret; the first one, if made, expires unrevealed because nobody holds its token. After a bare platform 503 (no body) the create may have finished, and a retry with the same key then gets its answer. Two creates with one key at once make one secret: the second is 409 idempotency-in-progress (Retry-After: 1), then gets the first answer."
     }
    ],
    "requestBody": {
     "required": true,
     "content": {
      "application/json": {
       "schema": {
        "$ref": "#/components/schemas/CreateSecret"
       },
       "examples": {
        "toAgent": {
         "summary": "Bound to another ckrets key; only that holder can reveal",
         "value": {
          "value": "LAB_7f3a0c…",
          "ttl": "15m",
          "audience": {
           "type": "key",
           "id": "key_7TQM3…"
          },
          "purpose": "db password for the deploy agent"
         }
        },
        "toCiJob": {
         "summary": "Bound to a GitHub Actions job; the job presents its OIDC token at reveal",
         "value": {
          "value": "LAB_7f3a0c…",
          "ttl": "30m",
          "audience": {
           "type": "oidc",
           "issuer": "https://token.actions.githubusercontent.com",
           "claims": {
            "repository": "acme/api",
            "job_workflow_ref": "acme/api/.github/workflows/deploy.yml@refs/heads/main"
           }
          }
         }
        },
        "noView": {
         "summary": "Client-side encrypted (SDK), recipient public key",
         "value": {
          "value": "base64url(ciphertext)",
          "clientEncrypted": true,
          "wrappedKey": "base64url(wrapped)",
          "ttl": "4h",
          "reveals": 1,
          "audience": {
           "type": "publicKey",
           "jwk": {
            "kty": "OKP",
            "crv": "Ed25519",
            "x": "…"
           }
          }
         }
        },
        "toSignedAgent": {
         "summary": "Bound to an agent that signs its requests (Web Bot Auth): planned; until then the server answers 400 audience-type-unknown",
         "value": {
          "value": "LAB_7f3a0c…",
          "ttl": "1h",
          "audience": {
           "type": "httpsig",
           "directory": "https://agents.example.com/.well-known/http-message-signatures-directory",
           "keyid": "ba3e…"
          }
         }
        },
        "keyed": {
         "summary": "With a free key (Authorization bearer), longer TTL and more reveals",
         "value": {
          "value": "LAB_7f3a0c…",
          "ttl": "7d",
          "reveals": 3,
          "audience": {
           "type": "key",
           "id": "key_7TQM3…"
          }
         }
        },
        "anonymous": {
         "summary": "For a person who clicks: no audience, so the URL is bearer (no key; 1 KB, 7 d max)",
         "value": {
          "value": "LAB_7f3a0c…",
          "ttl": "1h",
          "reveals": 1
         }
        },
        "withPassphrase": {
         "summary": "Anyone with the link, but the reader also needs a passphrase sent another way",
         "value": {
          "value": "LAB_7f3a0c…",
          "ttl": "4h",
          "passphrase": "corvid-lantern-42"
         }
        },
        "riddleBox": {
         "summary": "Riddle box (puzzlebox app; headers Ckrets-App: puzzlebox and Ckrets-Proof: session=…): opens only on an accepted answer; the key comes from the answer and the link token, so the platform cannot open it",
         "value": {
          "template": "puzzle",
          "value": "v1.Q2hp….",
          "clientEncrypted": true,
          "wrappedKey": "answer.v1",
          "puzzle": {
           "mode": "riddle",
           "strictness": "standard",
           "attempts": 7,
           "revealOnce": true,
           "window": "P7D",
           "commitments": [
            "Zm9v…",
            "YmFy…"
           ],
           "wraps": [
            "d3Jh…",
            "cDI…"
           ],
           "hints": [
            "Think music",
            "It has 88 of them"
           ]
          },
          "public": {
           "riddle": "I have keys but open no locks…",
           "shape": "one word, 5-7 letters"
          }
         }
        },
        "gameBox": {
         "summary": "Game box opened by the judge's signed verdict; the platform holds the key under the consumer root and releases the value on a pass",
         "value": {
          "template": "puzzle",
          "value": "The spare key is under the blue pot",
          "puzzle": {
           "mode": "game",
           "attempts": 5,
           "judge": "puzzlebox-judge-1",
           "rubric": "Passes if it names bourbon, mint, sugar and crushed ice",
           "hints": [
            "Mint is involved"
           ]
          },
          "public": {
           "riddle": "Describe the perfect mint julep.",
           "shape": "a sentence or two"
          }
         }
        },
        "datedBox": {
         "summary": "Box that opens on a date; the key travels in the link's fragment, so only the link opens it, and not before opensAt",
         "value": {
          "template": "puzzle",
          "value": "v1.Tm93….",
          "clientEncrypted": true,
          "wrappedKey": "fragment.v1",
          "puzzle": {
           "mode": "dated",
           "opensAt": "2026-12-25T07:00:00Z",
           "window": "P7D"
          },
          "public": {
           "note": "Open on Christmas morning"
          }
         }
        }
       }
      }
     }
    },
    "responses": {
     "201": {
      "description": "Created. A replay (the same Idempotency-Key and request within 24 hours) is also 201, with the first answer's body and the extra header Idempotent-Replay: true; nothing is made or counted",
      "headers": {
       "RateLimit-Limit": {
        "$ref": "#/components/headers/RateLimit-Limit"
       },
       "RateLimit-Remaining": {
        "$ref": "#/components/headers/RateLimit-Remaining"
       },
       "RateLimit-Reset": {
        "$ref": "#/components/headers/RateLimit-Reset"
       },
       "Ckrets-Tier": {
        "$ref": "#/components/headers/Ckrets-Tier"
       },
       "Ckrets-App": {
        "$ref": "#/components/headers/Ckrets-App"
       },
       "Ckrets-Encryption": {
        "$ref": "#/components/headers/Ckrets-Encryption"
       }
      },
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/SecretCreated"
        },
        "example": {
         "token": "ck_SRG73W7Q8ZQ3N4T2HYKD5MXF0A",
         "url": "https://ckrets.ai/r/ck_SRG73W7Q8ZQ3N4T2HYKD5MXF0A",
         "manageToken": "ckm_SRG73W7R…",
         "expiresAt": "2026-10-24T02:11:09Z",
         "revealsLeft": 1,
         "digest": "hmac-sha256:Rk3pX9…",
         "digestKey": "u8Zt0q…",
         "audience": {
          "type": "key",
          "id": "key_7TQM3…"
         },
         "receipt": "eyJhbGciOiJFUzI1NiJ9…"
        }
       }
      }
     },
     "400": {
      "description": "invalid-request (bad JSON, a missing, empty or non-string value, a malformed field, a ttl under a minute, reveals outside 1 to the tier's maximum), ttl-too-long (over the tier's maximum), or an audience type (audience-type-unknown, audience-invalid). Checked before anything is counted; never clamped",
      "$ref": "#/components/responses/Problem"
     },
     "401": {
      "$ref": "#/components/responses/Problem"
     },
     "403": {
      "description": "From the API, a problem body (a key without the create scope: scope-missing). From the door, CloudFront's plain 403 with no problem body: more than 300 creates a minute from one address; wait a minute or two and slow down"
     },
     "409": {
      "description": "idempotency-mismatch: the Idempotency-Key was used with a different request; nothing was made, and it counts as one create. idempotency-key-retired: the key was used with three different requests, so every request with it is refused, the right one included; use a new key. idempotency-in-progress: a create with this key and this request is still running; retry after Retry-After (1 s) to get its answer",
      "$ref": "#/components/responses/Problem"
     },
     "413": {
      "description": "value-too-large: the decoded value string's UTF-8 bytes (after JSON escapes, not the request body) are over the tier's size (1,024 bytes anonymous, 65,536 with a key)",
      "$ref": "#/components/responses/Problem"
     },
     "415": {
      "description": "unsupported-media-type: the body is not Content-Type application/json",
      "$ref": "#/components/responses/Problem"
     },
     "428": {
      "description": "Proof-of-work stamp required (anonymous tier, busy): the body carries a fresh stamp; solve it and resend with Ckrets-Proof: stamp=<nonce>:<counter>. A stamp that is missing, malformed, too weak, expired, from another network or issued at another difficulty is 428 again with a fresh stamp, never 400. A solved stamp is good for any number of creates until its expiresAt",
      "$ref": "#/components/responses/Problem"
     },
     "429": {
      "$ref": "#/components/responses/RateLimited"
     },
     "503": {
      "$ref": "#/components/responses/TierBudget"
     }
    }
   }
  },
  "/secrets/{token}": {
   "parameters": [
    {
     "name": "token",
     "in": "path",
     "required": true,
     "schema": {
      "type": "string",
      "pattern": "^ck_[0-9A-HJKMNP-TV-Z]{26}$"
     },
     "description": "Uppercase, case-sensitive, used exactly as given: there is no look-alike mapping (O, I, L and U are not in the alphabet and are not read as 0, 1, 1 or V). Nothing checks the pattern in front of the API: a malformed token (lowercased, a look-alike, a suffix such as .json) is 410 token-gone on a reveal, 204 on a revoke, 404 on status and /public; an empty one (/secrets/) or one with a trailing slash is 404, no such route. A query string is not part of the path and is ignored: GET /secrets/{token}?anything is the reveal"
    }
   ],
   "get": {
    "tags": [
     "secrets"
    ],
    "operationId": "revealSecret",
    "x-ckrets-stage": "beta",
    "summary": "Reveal the value once (counts against reveals)",
    "description": "Claim-first, read-first. Each reveal is claimed by one conditional write, so with N reveals left exactly N of any\nnumber of concurrent callers get the value; the others get 410. Every GET of a bearer token is the reveal and spends\nit: never GET it to test the token, in a health check or through a link prefetcher (GET /secrets/{token}/public is\nthe safe check). The last permitted reveal deletes the value and its digest key; the record stays, so\nstatus keeps answering `revealed` (until 30 days after expiresAt, then 404). Expiry is checked to the millisecond on\nthe service clock: a token reveals while now is before expiresAt, and at expiresAt it is 410. The claim's write\ndecides: a reveal claimed before expiresAt returns the value even if the answer leaves after it (the receipt's iat\nmay then be later than its exp). What counts as an attempt: a wrong passphrase only. Not counted: a success, a GET that names no\npassphrase (401 passphrase-required, pointing at POST), a refusal inside the cooldown (429), a wrong audience proof\n(403, recorded as a denial), and a reveal after the last one (410, recorded as consumedAttempts). After the nth\ncounted attempt the token waits 2^(n-1) s, per token whoever calls (1, 2, 4, 8 s on a budget of 5; up to 256 s on a\nbudget of 10); a call inside the wait is 429, not counted. Every wrong passphrase up to the last of the budget is 401;\nevery call after the last is 410 attempts-exhausted, and status shows state dead. Each attempt is one conditional\nwrite, so wrong passphrases sent at once get one 401 and 429 for the rest. A passphrase is sent only in a POST body\n(`{\"passphrase\": \"…\"}`); one in the query string is never read, so that GET answers 401 passphrase-required, not\ncounted. `POST` with `{}` is the same as this GET; HEAD is 404. A reveal is not idempotent: once claimed it is\nspent even if the answer never reaches the caller, and nothing fetches a value again (check /public). An unknown, malformed (tokens are\nuppercase Crockford base32) or consumed token is 410 token-gone, with nothing counted: existence is not a secret,\nonly the titles differ. A frozen token (reported, pending review) is 410 token-frozen, title \"Frozen pending review\":\ndo not retry; ask its sender for a new token.\nA manage token (`ckm_…`) is not a reveal credential: sent as the bearer it makes the caller anonymous, so it\nreveals a secret without an audience (and spends it) and fails a key audience like anyone else. If the secret has an `audience`, the caller proves it: a ckrets key or lease as the\nbearer token (type key); the CI OIDC JWT as the bearer token with `aud` = https://ckrets.ai (type oidc); an RFC 9421\nsignature over the request with the named key (types httpsig and publicKey). A reveal attempted after the last one\nis answered 410 and raises the `secret.consumed_attempt` alarm for the creator.\nPrefer a helper for delivery: `npx -y @ckrets/helper@0.9.0 reveal <token> --env NAME -- <command>` puts the value in a\nprocess environment, never in a transcript; it proves key, OIDC and signature audiences itself (see /for-agents.md).\nThe share url (/r/<token>) is for humans and reveals on a click, so link previews do not consume it.\n",
    "security": [
     {},
     {
      "bearerKey": []
     },
     {
      "oidcToken": []
     },
     {
      "httpSignature": []
     }
    ],
    "responses": {
     "200": {
      "description": "The value and a signed receipt",
      "headers": {
       "RateLimit-Limit": {
        "$ref": "#/components/headers/RateLimit-Limit"
       },
       "RateLimit-Remaining": {
        "$ref": "#/components/headers/RateLimit-Remaining"
       },
       "RateLimit-Reset": {
        "$ref": "#/components/headers/RateLimit-Reset"
       },
       "Ckrets-Tier": {
        "$ref": "#/components/headers/Ckrets-Tier"
       },
       "Ckrets-App": {
        "$ref": "#/components/headers/Ckrets-App"
       }
      },
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/SecretRevealed"
        },
        "example": {
         "value": "LAB_7f3a0c…",
         "revealsLeft": 0,
         "sender": {
          "kind": "anonymous"
         },
         "digestKey": "u8Zt0q…",
         "receipt": "eyJhbGciOiJFUzI1NiJ9…"
        }
       }
      }
     },
     "401": {
      "description": "passphrase-required: a passphrase is needed (a GET, or a POST without one; not counted) or was wrong (counted, with a deny receipt). invalid-credentials: the Authorization header is not a valid credential (an unknown, revoked or ended key, a malformed bearer), refused before the token is looked at; send no header for a bearer reveal",
      "$ref": "#/components/responses/Problem"
     },
     "403": {
      "description": "audience-mismatch: the proof is missing (no key, no OIDC token, no signature) or names someone else; never 401, never counted. scope-missing: a key without the reveal scope",
      "$ref": "#/components/responses/Problem"
     },
     "410": {
      "description": "Gone: token-gone (unknown, malformed, revealed, expired, revoked, or the creator key revoked), token-frozen (reported, pending review: ask the sender for a new token) or attempts-exhausted (dead). Never counted",
      "$ref": "#/components/responses/Problem"
     },
     "429": {
      "$ref": "#/components/responses/RateLimited"
     },
     "503": {
      "description": "key-unavailable: the key that opens this secret is unavailable on our side; nothing was spent, try again after Retry-After. temporarily-unavailable: busy (nothing done), or outcome unknown (read check)",
      "$ref": "#/components/responses/Problem"
     }
    }
   },
   "post": {
    "tags": [
     "secrets"
    ],
    "operationId": "revealSecretWithProof",
    "x-ckrets-stage": "beta",
    "summary": "Reveal with a proof (a passphrase, a commitment, or an attempt ticket with a commitment or a verifier's verdict)",
    "description": "One body per proof:\n`{passphrase}` for onetime secrets with a passphrase (sent to a secret that has none, it is ignored: the POST is the\nreveal and spends it, as a GET would); `{commitment}` for a Strict riddle box (the call claims the\nattempt itself, claim-first and read-first, cards 07 and 08; the commitment is compared in constant time against the\nrecord's commitments); `{ticket, commitment}` after a counted attempt for Standard and Relaxed riddle boxes;\n`{ticket, verdict}` for game boxes, where the verdict is a verifier claim signed by the judge the policy names. A\nreveal that carries a ticket is not counted again. Dated boxes reveal with no body once `opensAt` has passed on\nthe service clock (`425 not-yet-open` before). Audience proofs (key, OIDC, signature) still apply on top.\n",
    "requestBody": {
     "required": false,
     "content": {
      "application/json": {
       "schema": {
        "$ref": "#/components/schemas/RevealProof"
       }
      }
     }
    },
    "responses": {
     "200": {
      "description": "The value (ciphertext for client-encrypted boxes) and a signed receipt",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/SecretRevealed"
        }
       }
      }
     },
     "401": {
      "description": "Wrong passphrase, commitment or verdict, or an invalid ticket (counted unless a ticket already counted it)",
      "$ref": "#/components/responses/Problem"
     },
     "403": {
      "$ref": "#/components/responses/Problem"
     },
     "410": {
      "$ref": "#/components/responses/Problem"
     },
     "425": {
      "description": "Not yet open (dated box): resetsAt says when",
      "$ref": "#/components/responses/Problem"
     },
     "429": {
      "$ref": "#/components/responses/RateLimited"
     },
     "503": {
      "description": "key-unavailable: the key that opens this secret is unavailable on our side; nothing was spent, try again after Retry-After. temporarily-unavailable: busy (nothing done), or outcome unknown (read check)",
      "$ref": "#/components/responses/Problem"
     }
    }
   },
   "delete": {
    "tags": [
     "secrets"
    ],
    "operationId": "revokeSecret",
    "x-ckrets-stage": "beta",
    "summary": "Revoke before reveal or expiry, with the creator key or the manage token (both as Authorization Bearer)",
    "description": "The manage token (ckm_…, from the create response) is the only way for an anonymous creator; keyed creators and account sessions use their own credential. Whoever holds the manage token can revoke, read status, tighten the policy and adopt the secret (POST /me/secrets/adopt, which retires the token), so keep it like the key. A revoke has no receipt: it is 204 with no body in every case, and status shows it. An unknown or malformed token is also 204 (idempotent). Without a credential, or with someone else's, it is 403 not-creator; 401 only for a malformed bearer. A revoke and a reveal are each one conditional write: a reveal claimed first gets the value; a reveal after the revoke is 410.",
    "security": [
     {
      "bearerKey": []
     },
     {
      "bearerManage": []
     },
     {
      "accountSession": []
     }
    ],
    "responses": {
     "204": {
      "description": "Revoked, no body (idempotent; also for an unknown token). After the last permitted reveal there is nothing left to revoke: also 204, and status keeps revealed"
     },
     "403": {
      "$ref": "#/components/responses/Problem"
     },
     "429": {
      "$ref": "#/components/responses/RateLimited"
     }
    }
   }
  },
  "/secrets/{token}/status": {
   "parameters": [
    {
     "name": "token",
     "in": "path",
     "required": true,
     "schema": {
      "type": "string"
     }
    }
   ],
   "get": {
    "tags": [
     "secrets"
    ],
    "operationId": "getSecretStatus",
    "x-ckrets-stage": "beta",
    "summary": "The creator's view of a token (never the value)",
    "description": "For the creator (key or account session) or the holder of the manage token (Authorization Bearer ckm_…); anyone else gets 403 not-creator (401 only for a malformed bearer). An unknown or malformed token is 404, and so is a token 30 days after its expiresAt, when the record and its manage token expire (the ledger keeps the metadata 13 months, see /limits.md). The manage token itself never appears in status, a receipt or /public (it is stored as a hash). `attempts` counts wrong passphrases; `denials` counts every refusal of a proof (wrong passphrase and wrong audience). Says whether the token is pending, revealed, expired unrevealed, revoked or dead, with timestamps and counts. Status reads consistently, so every reveal, attempt and revoke already answered shows at once. This is what a sender checks instead of asking the recipient.",
    "security": [
     {
      "bearerKey": []
     },
     {
      "bearerManage": []
     },
     {
      "accountSession": []
     }
    ],
    "responses": {
     "200": {
      "description": "Status",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/SecretStatus"
        },
        "example": {
         "state": "revealed",
         "reveals": 1,
         "revealsLeft": 0,
         "attempts": 0,
         "denials": 0,
         "createdAt": "2026-10-24T01:11:09Z",
         "expiresAt": "2026-10-24T02:11:09Z",
         "revealedAt": [
          "2026-10-24T01:14:40Z"
         ],
         "revealedBy": [
          {
           "type": "key",
           "id": "key_7TQM3…"
          }
         ],
         "consumedAttempts": 0,
         "digest": "hmac-sha256:Rk3pX9…"
        }
       }
      }
     },
     "403": {
      "$ref": "#/components/responses/Problem"
     },
     "404": {
      "$ref": "#/components/responses/Problem"
     }
    }
   }
  },
  "/secrets/{token}/public": {
   "parameters": [
    {
     "name": "token",
     "in": "path",
     "required": true,
     "schema": {
      "type": "string"
     }
    }
   ],
   "get": {
    "tags": [
     "templates"
    ],
    "operationId": "getPublicMetadata",
    "x-ckrets-stage": "beta",
    "summary": "The secret's public view, readable without auth (never the value, the salt, a commitment or a wrapped key)",
    "description": "For pages that show a secret or a box before anyone tries it. For a onetime secret (the reveal page reads it): the state, the sender's kind and id, the size, whether a passphrase is needed and the audience; reading it counts nothing and writes nothing, and after the reveal it answers 200 with state revealed. For a puzzle box: the riddle, the answer shape, the strictness label, the state and the countdown). Hints are not public metadata, because a hint costs the solver an attempt and a public hint would be a free one; how hints are released is open. Up to 4 KB set by the creator in `public`, plus the state the service owns.",
    "security": [
     {}
    ],
    "responses": {
     "200": {
      "description": "Public metadata",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/PublicView"
        },
        "example": {
         "template": "puzzle",
         "mode": "riddle",
         "strictness": "standard",
         "state": "pending",
         "attemptBudget": 7,
         "attemptsLeft": 7,
         "hintCount": 2,
         "cooldownUntil": null,
         "opensAt": null,
         "expiresAt": "2026-11-02T10:00:00Z",
         "public": {
          "riddle": "I have keys but open no locks…",
          "shape": "one word, 5-7 letters"
         }
        }
       }
      }
     },
     "404": {
      "description": "An unknown or malformed token, or one past its record's life (30 days after expiresAt)",
      "$ref": "#/components/responses/Problem"
     }
    }
   }
  },
  "/secrets/{token}/attempts": {
   "parameters": [
    {
     "name": "token",
     "in": "path",
     "required": true,
     "schema": {
      "type": "string"
     }
    }
   ],
   "post": {
    "tags": [
     "templates"
    ],
    "operationId": "claimAttempt",
    "x-ckrets-stage": "beta",
    "summary": "Claim one attempt first and get a ticket, or a hint (puzzle template)",
    "description": "Counts the attempt when it is claimed (claim-first; an `ambiguous` outcome downstream still counted) and\nreturns a ticket: a KMS HMAC over the token's hash, the attempt number and an expiry 120 s out (the model buckets'\nworst wait). The puzzlebox layer accepts the ticket to run its normaliser or judge, so every model call is paid\nfor by a counted attempt; a busy answer from that layer spends nothing, and the page resends the same ticket. If no\nmodel answered inside the window, the puzzle layer (never a browser) voids the ticket and the claim is uncounted.\nThe reveal then takes the ticket with a commitment or a verdict and does not count again.\nWith `{hint: n}` the claim returns the box's hint n instead of a ticket, under the same budget and cooldown; every\nclaim of a hint counts, the same hint claimed again included.\nCooldown runs from the claim: claim n+1 is refused until 2^(n-1) s after claim n (1, 2, 4, 8, 16 s) unless the\nbox has opened; a refusal for cooldown is 429 rate-limited with Retry-After and is not counted.\n",
    "requestBody": {
     "required": false,
     "content": {
      "application/json": {
       "schema": {
        "type": "object",
        "properties": {
         "hint": {
          "type": "integer",
          "minimum": 1,
          "description": "Claim an attempt and get hint n instead of a ticket"
         }
        }
       }
      }
     }
    },
    "security": [
     {},
     {
      "anonymousProof": []
     },
     {
      "bearerKey": []
     },
     {
      "accountSession": []
     }
    ],
    "responses": {
     "201": {
      "description": "The ticket",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/AttemptTicket"
        },
        "examples": {
         "ticket": {
          "value": {
           "ticket": "ckt_v1.Zr8…",
           "attempt": 3,
           "attemptsLeft": 4,
           "expiresAt": "2026-10-26T10:02:00Z"
          }
         },
         "hint": {
          "value": {
           "hint": "Think music",
           "hintNumber": 1,
           "attempt": 2,
           "attemptsLeft": 5
          }
         }
        }
       }
      }
     },
     "404": {
      "description": "No such box (not-found): an unknown or malformed token, or a onetime secret, which has no attempts to claim",
      "$ref": "#/components/responses/Problem"
     },
     "410": {
      "description": "Attempts exhausted, the box is gone (token-gone), or it was reported and is held for review (token-frozen: ask the sender)",
      "$ref": "#/components/responses/Problem"
     },
     "422": {
      "description": "No such hint (type hint-out-of-range)",
      "$ref": "#/components/responses/Problem"
     },
     "425": {
      "description": "Not yet open",
      "$ref": "#/components/responses/Problem"
     },
     "429": {
      "description": "In cooldown from the last claim (rate-limited, Retry-After, not counted)",
      "$ref": "#/components/responses/RateLimited"
     }
    }
   }
  },
  "/secrets/{token}/policy": {
   "parameters": [
    {
     "name": "token",
     "in": "path",
     "required": true,
     "schema": {
      "type": "string"
     }
    }
   ],
   "patch": {
    "tags": [
     "templates"
    ],
    "operationId": "narrowPolicy",
    "x-ckrets-stage": "beta",
    "summary": "Change a secret's policy as its owner, only in directions that never weaken the gate",
    "description": "The owner is the creator's key, a ckrets.ai account session, or the manage token (all as Authorization Bearer). Allowed: a longer window (within the\ntier's maximum), fewer attempts, a longer cooldown, `revealOnce: true`, a later `opensAt`. Refused with 409\n`policy-widening`: more attempts, an earlier `opensAt`, a shorter cooldown, removing an audience, changing the\ntemplate, and any change to `commitments` after the first attempt. Every change is a ledger event and a receipt.\nPuzzle boxes only: on a onetime secret this route is 404 (a onetime secret's policy never changes; revoke it and\ncreate again). `ttl` is an alias of `window` (one field; if both are sent, `window` is read): `P<n>D`, counted from\nnow, or from `opensAt` when that is later. A new end before the current expiry is 409 `policy-widening`; one past\nthe tier's maximum (counted from `opensAt`, or from creation for a box with no `opensAt`) is `400 ttl-too-long`,\nwhose `detail` names the latest expiry the box can have, and changes nothing: never clamped. The answer's\n`policy.expiresAt` is the end you asked for. `attempts` cannot go\nbelow the attempts already used: used attempts never reset.\n",
    "security": [
     {
      "bearerKey": []
     },
     {
      "bearerManage": []
     },
     {
      "accountSession": []
     }
    ],
    "requestBody": {
     "required": true,
     "content": {
      "application/merge-patch+json": {
       "schema": {
        "$ref": "#/components/schemas/PolicyPatch"
       },
       "example": {
        "window": "P14D",
        "attempts": 5
       }
      }
     }
    },
    "responses": {
     "200": {
      "description": "The policy as it now stands and a receipt",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "properties": {
          "policy": {
           "type": "object"
          },
          "receipt": {
           "type": "string"
          }
         }
        }
       }
      }
     },
     "403": {
      "$ref": "#/components/responses/Problem"
     },
     "409": {
      "description": "The change would widen the gate (type policy-widening)",
      "$ref": "#/components/responses/Problem"
     }
    }
   }
  },
  "/me/secrets": {
   "get": {
    "tags": [
     "secrets"
    ],
    "operationId": "listMySecrets",
    "x-ckrets-stage": "beta",
    "summary": "The caller's secrets with their status (never values), for a key or a ckrets.ai account session",
    "description": "An anonymous creator has no list; each manage token addresses one secret. A key needs the status scope. Filter by `app` and `state`; a page holds up to 50 items, `cursor` fetches the next. Each item carries `id` (the token hash, as receipts show it): the token itself is never stored, so the list cannot open a box; a signed-in maker manages it by id.",
    "security": [
     {
      "bearerKey": []
     },
     {
      "accountSession": []
     }
    ],
    "parameters": [
     {
      "name": "app",
      "in": "query",
      "required": false,
      "schema": {
       "type": "string"
      }
     },
     {
      "name": "state",
      "in": "query",
      "required": false,
      "schema": {
       "$ref": "#/components/schemas/State"
      }
     },
     {
      "name": "cursor",
      "in": "query",
      "required": false,
      "schema": {
       "type": "string"
      }
     }
    ],
    "responses": {
     "200": {
      "description": "A page of secrets",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "properties": {
          "items": {
           "type": "array",
           "items": {
            "$ref": "#/components/schemas/SecretStatus"
           }
          },
          "cursor": {
           "type": "string"
          }
         }
        }
       }
      }
     },
     "400": {
      "description": "Unknown state or cursor",
      "$ref": "#/components/responses/Problem"
     },
     "401": {
      "$ref": "#/components/responses/Problem"
     },
     "403": {
      "description": "A key without the status scope (type scope-missing)",
      "$ref": "#/components/responses/Problem"
     }
    }
   }
  },
  "/me/secrets/{id}": {
   "parameters": [
    {
     "name": "id",
     "in": "path",
     "required": true,
     "schema": {
      "type": "string",
      "pattern": "^sha256:[0-9a-f]{64}$"
     },
     "description": "The item's id from GET /me/secrets (URL-encode the colon or not)"
    }
   ],
   "get": {
    "tags": [
     "secrets"
    ],
    "operationId": "getMySecret",
    "x-ckrets-stage": "beta",
    "summary": "One of the account's secrets by id",
    "security": [
     {
      "accountSession": []
     }
    ],
    "responses": {
     "200": {
      "description": "Status",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/SecretStatus"
        }
       }
      }
     },
     "401": {
      "$ref": "#/components/responses/Problem"
     },
     "404": {
      "description": "No such secret for this account (another account's id is not found)",
      "$ref": "#/components/responses/Problem"
     }
    }
   },
   "delete": {
    "tags": [
     "secrets"
    ],
    "operationId": "revokeMySecret",
    "x-ckrets-stage": "beta",
    "summary": "Revoke one of the account's secrets by id",
    "security": [
     {
      "accountSession": []
     }
    ],
    "responses": {
     "204": {
      "description": "Revoked"
     },
     "401": {
      "$ref": "#/components/responses/Problem"
     },
     "404": {
      "$ref": "#/components/responses/Problem"
     }
    }
   }
  },
  "/me/secrets/{id}/policy": {
   "parameters": [
    {
     "name": "id",
     "in": "path",
     "required": true,
     "schema": {
      "type": "string",
      "pattern": "^sha256:[0-9a-f]{64}$"
     }
    }
   ],
   "patch": {
    "tags": [
     "templates"
    ],
    "operationId": "narrowMyPolicy",
    "x-ckrets-stage": "beta",
    "summary": "PATCH /secrets/{token}/policy for one of the account's boxes, by id",
    "security": [
     {
      "accountSession": []
     }
    ],
    "requestBody": {
     "required": true,
     "content": {
      "application/merge-patch+json": {
       "schema": {
        "$ref": "#/components/schemas/PolicyPatch"
       }
      }
     }
    },
    "responses": {
     "200": {
      "description": "The policy as it now stands and a receipt",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "properties": {
          "policy": {
           "type": "object"
          },
          "receipt": {
           "type": "string"
          }
         }
        }
       }
      }
     },
     "401": {
      "$ref": "#/components/responses/Problem"
     },
     "404": {
      "$ref": "#/components/responses/Problem"
     },
     "409": {
      "description": "The change would widen the gate (type policy-widening)",
      "$ref": "#/components/responses/Problem"
     }
    }
   }
  },
  "/secrets/{token}/attempts/{ticket}/void": {
   "parameters": [
    {
     "name": "token",
     "in": "path",
     "required": true,
     "schema": {
      "type": "string"
     }
    },
    {
     "name": "ticket",
     "in": "path",
     "required": true,
     "schema": {
      "type": "string"
     }
    }
   ],
   "post": {
    "tags": [
     "private"
    ],
    "operationId": "voidAttempt",
    "x-internal": true,
    "x-ckrets-stage": "beta",
    "summary": "Uncount a claim that no model answered (the app's own layer only, on the private API)",
    "description": "For the puzzle layer when its normaliser or judge gave no answer inside the ticket's 120 s. Served only by the private API to the app's caller role (SigV4); on the public API it is refused before any work, so farming a model still costs a counted attempt per call. A voided ticket cannot reveal or read a rubric.",
    "servers": [
     {
      "url": "https://{privateApi}/v1",
      "description": "The private API (ckapi-<stage>-private): IAM on every route; its invoke URL is a stack output",
      "variables": {
       "privateApi": {
        "default": "abc123.execute-api.us-east-2.amazonaws.com",
        "description": "the private API's invoke host, from the ckapi-<stage> stack outputs"
       }
      }
     }
    ],
    "security": [
     {
      "layerIam": []
     }
    ],
    "responses": {
     "204": {
      "description": "Uncounted"
     },
     "401": {
      "description": "The ticket is not valid for this box (type ticket-invalid)",
      "$ref": "#/components/responses/Problem"
     },
     "403": {
      "description": "Not the app's caller role, or the public API (type void-not-allowed)",
      "$ref": "#/components/responses/Problem"
     },
     "409": {
      "description": "The ticket was already used or a model answered it",
      "$ref": "#/components/responses/Problem"
     }
    }
   }
  },
  "/secrets/{token}/attempts/{ticket}/judging": {
   "parameters": [
    {
     "name": "token",
     "in": "path",
     "required": true,
     "schema": {
      "type": "string"
     }
    },
    {
     "name": "ticket",
     "in": "path",
     "required": true,
     "schema": {
      "type": "string"
     }
    }
   ],
   "post": {
    "tags": [
     "private"
    ],
    "operationId": "readJudging",
    "x-internal": true,
    "x-ckrets-stage": "beta",
    "summary": "A game box's question and rubric for the app's judge (the app's own layer only, on the private API)",
    "description": "For a live, claimed ticket that is neither used nor voided, of a game box of the calling app, the question (public.riddle) and the rubric, nothing else (never the value, an answer or a commitment). Every call is counted on the record and ledgered.",
    "servers": [
     {
      "url": "https://{privateApi}/v1",
      "description": "The private API (ckapi-<stage>-private): IAM on every route; its invoke URL is a stack output",
      "variables": {
       "privateApi": {
        "default": "abc123.execute-api.us-east-2.amazonaws.com",
        "description": "the private API's invoke host, from the ckapi-<stage> stack outputs"
       }
      }
     }
    ],
    "security": [
     {
      "layerIam": []
     }
    ],
    "responses": {
     "200": {
      "description": "What the judge reads",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "required": [
          "question",
          "rubric"
         ],
         "properties": {
          "question": {
           "type": "string"
          },
          "rubric": {
           "type": "string"
          }
         }
        }
       }
      }
     },
     "401": {
      "description": "The ticket is not valid, used or voided (type ticket-invalid)",
      "$ref": "#/components/responses/Problem"
     },
     "403": {
      "$ref": "#/components/responses/Problem"
     },
     "404": {
      "description": "No such game box for this app",
      "$ref": "#/components/responses/Problem"
     }
    }
   }
  },
  "/apps/me/daily": {
   "get": {
    "tags": [
     "private"
    ],
    "operationId": "getAppDaily",
    "x-internal": true,
    "x-ckrets-stage": "beta",
    "summary": "The calling app's boxes created and opened per UTC day (the app's own layer only, on the private API)",
    "description": "Counts only, from the ledger filtered by the calling app (\"me\" is the app whose caller role signed). byMode counts creates per mode. At most 31 days; the last 7 by default.",
    "servers": [
     {
      "url": "https://{privateApi}/v1",
      "description": "The private API (ckapi-<stage>-private): IAM on every route; its invoke URL is a stack output",
      "variables": {
       "privateApi": {
        "default": "abc123.execute-api.us-east-2.amazonaws.com",
        "description": "the private API's invoke host, from the ckapi-<stage> stack outputs"
       }
      }
     }
    ],
    "security": [
     {
      "layerIam": []
     }
    ],
    "parameters": [
     {
      "name": "from",
      "in": "query",
      "required": false,
      "schema": {
       "type": "string",
       "format": "date"
      }
     },
     {
      "name": "to",
      "in": "query",
      "required": false,
      "schema": {
       "type": "string",
       "format": "date"
      }
     }
    ],
    "responses": {
     "200": {
      "description": "One row per day",
      "content": {
       "application/json": {
        "schema": {
         "type": "array",
         "items": {
          "$ref": "#/components/schemas/DailyCount"
         }
        }
       }
      }
     },
     "400": {
      "$ref": "#/components/responses/Problem"
     },
     "403": {
      "$ref": "#/components/responses/Problem"
     }
    }
   }
  },
  "/sessions": {
   "post": {
    "tags": [
     "apps"
    ],
    "operationId": "createSession",
    "x-ckrets-stage": "beta",
    "summary": "Trade one Turnstile solve for a session ticket (anonymous creates on a first-party app)",
    "description": "The page solves Turnstile once and sends the token here with its public app id (Ckrets-App). The API verifies it\nagainst the app's Turnstile secret and returns a session ticket: a KMS HMAC good for 10 minutes, one create and up\nto three calls to the app's generator. The app's layer accepts the ticket for its generator with verify-only access\nto the key; the create consumes it as the anonymous proof (`Ckrets-Proof: session=<ticket>`). A signed-in maker\nuses the account session instead. A Turnstile token is good once; this is the only call that verifies one.\n",
    "security": [
     {}
    ],
    "parameters": [
     {
      "name": "Ckrets-App",
      "in": "header",
      "required": true,
      "schema": {
       "type": "string",
       "example": "puzzlebox"
      }
     }
    ],
    "requestBody": {
     "required": true,
     "content": {
      "application/json": {
       "schema": {
        "type": "object",
        "required": [
         "turnstile"
        ],
        "properties": {
         "turnstile": {
          "type": "string"
         }
        }
       }
      }
     }
    },
    "responses": {
     "201": {
      "description": "The session ticket",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/SessionTicket"
        },
        "example": {
         "session": "cks_v1.Hq2…",
         "expiresAt": "2026-10-26T10:10:00Z",
         "creates": 1,
         "generatorCalls": 3
        }
       }
      }
     },
     "400": {
      "description": "Ckrets-App names no first-party app",
      "$ref": "#/components/responses/Problem"
     },
     "403": {
      "description": "Turnstile failed, was spent or could not be verified; solve it again (type proof-required)",
      "$ref": "#/components/responses/Problem"
     },
     "429": {
      "$ref": "#/components/responses/RateLimited"
     }
    }
   }
  },
  "/me/secrets/adopt": {
   "post": {
    "tags": [
     "secrets"
    ],
    "operationId": "adoptSecret",
    "x-ckrets-stage": "beta",
    "summary": "Take ownership of an anonymously made secret with its manage token, signed in",
    "description": "Any template. The caller is a key with the manage scope (every default key has it) or a signed-in ckrets.ai account, and the body carries a secret's manage token: the caller becomes the secret's owner (its status and revoke follow), and the manage token is retired, so it no longer reads status or revokes. An anonymous creator who held only that token loses status and revoke; a keyed or CI creator keeps its own credential. So anyone who holds a manage token can take the secret over: keep it like a key. At the passkey upgrade the signed-in maker sends each manage token the device holds. A receipt (typ adopt) records the change. A manage token already retired, expired or never issued is 410 token-gone; a key without manage is 403 scope-missing; no key or account session is 401 invalid-credentials.",
    "security": [
     {
      "accountSession": []
     },
     {
      "bearerKey": []
     }
    ],
    "requestBody": {
     "required": true,
     "content": {
      "application/json": {
       "schema": {
        "type": "object",
        "required": [
         "manageToken"
        ],
        "properties": {
         "manageToken": {
          "type": "string"
         }
        }
       }
      }
     }
    },
    "responses": {
     "200": {
      "description": "The secret's status under its new owner and a receipt",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "properties": {
          "status": {
           "$ref": "#/components/schemas/SecretStatus"
          },
          "receipt": {
           "type": "string"
          }
         }
        }
       }
      }
     },
     "403": {
      "$ref": "#/components/responses/Problem"
     },
     "410": {
      "$ref": "#/components/responses/Problem"
     }
    }
   }
  },
  "/accounts/{flow}/{step}": {
   "parameters": [
    {
     "name": "flow",
     "in": "path",
     "required": true,
     "schema": {
      "type": "string",
      "enum": [
       "register",
       "auth"
      ]
     }
    },
    {
     "name": "step",
     "in": "path",
     "required": true,
     "schema": {
      "type": "string",
      "enum": [
       "options",
       "verify"
      ]
     }
    }
   ],
   "post": {
    "tags": [
     "apps"
    ],
    "operationId": "accountPasskey",
    "x-ckrets-stage": "beta",
    "summary": "Passkey registration and sign-in for the ckrets.ai account page (/account) only: not an agent API",
    "description": "WebAuthn with RP id ckrets.ai: options returns the WebAuthn options, verify takes the authenticator's response and returns an account session (cka_…, 12 hours) for the first-party app named in the body, whose return address must be on the app's allow-list (origin and path, e.g. https://pleaseunbox.com/boxes); anything else is refused, so the page is no open redirect. The account page sends the session back to the app in the URL fragment. Agents never call these.",
    "security": [
     {}
    ],
    "requestBody": {
     "required": true,
     "content": {
      "application/json": {
       "schema": {
        "type": "object",
        "required": [
         "app",
         "returnTo"
        ],
        "properties": {
         "app": {
          "type": "string",
          "example": "puzzlebox"
         },
         "returnTo": {
          "type": "string",
          "format": "uri"
         },
         "response": {
          "type": "object",
          "description": "verify: the WebAuthn response"
         }
        }
       }
      }
     }
    },
    "responses": {
     "200": {
      "description": "options: WebAuthn options; verify: { session, returnTo, account }",
      "content": {
       "application/json": {
        "schema": {
         "type": "object"
        }
       }
      }
     },
     "400": {
      "$ref": "#/components/responses/Problem"
     },
     "401": {
      "$ref": "#/components/responses/Problem"
     }
    }
   }
  },
  "/batches": {
   "post": {
    "tags": [
     "batches"
    ],
    "operationId": "createBatch",
    "x-ckrets-stage": "planned",
    "summary": "Create many secrets for many named recipients in one call (rotation after an incident)",
    "description": "Keyed and signed tiers. Up to 100 items on a free key, 5,000 on a paid key. Every item needs an `audience`; a batch never mints bearer tokens. The response carries a manifest receipt (JWS over the list of token digests) and, if the key has webhooks, the batch emits `batch.completed` when every item is revealed or expired.",
    "security": [
     {
      "bearerKey": []
     },
     {
      "httpSignature": []
     }
    ],
    "parameters": [
     {
      "name": "Idempotency-Key",
      "in": "header",
      "required": false,
      "schema": {
       "type": "string",
       "maxLength": 64
      },
      "description": "Optional, 1 to 64 characters (longer is 400, never cut). Scoped to you (your address when anonymous, else your key or CI identity) and bound to the request (value, ttl, reveals, audience, whether a passphrase is set, clientEncrypted and wrappedKey). For 24 hours the same key and the same request return the first answer again (Idempotent-Replay: true, nothing counted); the same key with a different request is 409 idempotency-mismatch, nothing made and one create counted; after three, the key is 409 idempotency-key-retired for every request. Another caller never sees your answer."
     }
    ],
    "requestBody": {
     "required": true,
     "content": {
      "application/json": {
       "schema": {
        "$ref": "#/components/schemas/CreateBatch"
       },
       "example": {
        "label": "rotation after nx s1ngularity",
        "items": [
         {
          "label": "payments-api",
          "value": "LAB_a1…",
          "ttl": "2h",
          "audience": {
           "type": "oidc",
           "issuer": "https://token.actions.githubusercontent.com",
           "claims": {
            "repository": "acme/payments-api",
            "ref": "refs/heads/main"
           }
          }
         },
         {
          "label": "alice laptop",
          "value": "LAB_b2…",
          "ttl": "2h",
          "audience": {
           "type": "key",
           "id": "key_7TQM4…"
          }
         }
        ]
       }
      }
     }
    },
    "responses": {
     "201": {
      "description": "Created",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/BatchCreated"
        }
       }
      }
     },
     "400": {
      "$ref": "#/components/responses/Problem"
     },
     "413": {
      "description": "Batch too large (type batch-too-large)",
      "$ref": "#/components/responses/Problem"
     },
     "429": {
      "$ref": "#/components/responses/RateLimited"
     },
     "501": {
      "$ref": "#/components/responses/NotInV1"
     }
    }
   }
  },
  "/batches/{batchId}": {
   "parameters": [
    {
     "name": "batchId",
     "in": "path",
     "required": true,
     "schema": {
      "type": "string"
     }
    }
   ],
   "get": {
    "tags": [
     "batches"
    ],
    "operationId": "getBatch",
    "x-ckrets-stage": "planned",
    "summary": "Batch summary and per-item status (never the values)",
    "security": [
     {
      "bearerKey": []
     },
     {
      "httpSignature": []
     }
    ],
    "parameters": [
     {
      "name": "cursor",
      "in": "query",
      "required": false,
      "schema": {
       "type": "string"
      }
     }
    ],
    "responses": {
     "200": {
      "description": "Status",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/BatchStatus"
        }
       }
      }
     },
     "404": {
      "$ref": "#/components/responses/Problem"
     },
     "501": {
      "$ref": "#/components/responses/NotInV1"
     }
    }
   },
   "delete": {
    "tags": [
     "batches"
    ],
    "operationId": "revokeBatch",
    "x-ckrets-stage": "planned",
    "summary": "Revoke every unrevealed item in the batch",
    "security": [
     {
      "bearerKey": []
     },
     {
      "httpSignature": []
     }
    ],
    "responses": {
     "204": {
      "description": "Revoked"
     },
     "404": {
      "$ref": "#/components/responses/Problem"
     },
     "501": {
      "$ref": "#/components/responses/NotInV1"
     }
    }
   }
  },
  "/requests": {
   "post": {
    "tags": [
     "requests"
    ],
    "operationId": "createRequest",
    "x-ckrets-stage": "beta",
    "summary": "Ask a person for a password, a code or a line of text, through a page on ckrets that names your key",
    "description": "For when an agent needs a credential from a person: never ask for it in chat (the MCP specification sends\ncredentials through a web page). Keyed callers only (a free key or lease is fine): the page shows who asks. The\nanswer's `url` (`/q/<requestId>`) is for the person. The page shows a fixed notice (\"This request was made by an\nAPI key, not by ckrets…\"), your key's label, its domain (unverified in v1) and its id, the prompt and one input,\nand takes one answer. That answer becomes a secret addressed to your key (`reveals: 1`, an hour), and costs one\ncreate from your key's daily creates. Poll `GET /v1/requests/{requestId}`: once `fulfilled` it carries the\n`token`, which only your key reveals (the helper puts the value straight into a process). No webhooks in v1.\nLimits per key: a free key 5 an hour, 20 a day and 3 open at once (paid keys, once they exist: 20, 100 and 5).\nThe prompt is at most 140 characters, one line, with no link, domain or email address.\n",
    "security": [
     {
      "bearerKey": []
     }
    ],
    "parameters": [
     {
      "name": "Idempotency-Key",
      "in": "header",
      "required": false,
      "schema": {
       "type": "string",
       "maxLength": 64
      },
      "description": "Optional, 1 to 64 characters. Scoped to your key and bound to the request (prompt, kind, ttl, encJwk): for 24 hours the same key and request return the first answer again (Idempotent-Replay: true, nothing counted); the same key with a different request is 409 idempotency-mismatch, nothing made."
     }
    ],
    "requestBody": {
     "required": true,
     "content": {
      "application/json": {
       "schema": {
        "$ref": "#/components/schemas/CreateRequest"
       },
       "example": {
        "prompt": "Enter the 6-digit code from your authenticator",
        "kind": "otp",
        "ttl": "10m"
       }
      }
     }
    },
    "responses": {
     "201": {
      "description": "Created. Show `url` to the person.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/RequestCreated"
        },
        "example": {
         "requestId": "ckq_SRG75K2M8D4QW7N3T6HYB1XZ9C",
         "url": "https://ckrets.ai/q/ckq_SRG75K2M8D4QW7N3T6HYB1XZ9C",
         "manageToken": "ckm_SRG75W7R8D4QW7N3T6HYB1XZ9C",
         "expiresAt": "2026-11-06T01:21:09Z",
         "state": "pending",
         "kind": "otp",
         "receipt": "eyJ…"
        }
       }
      }
     },
     "400": {
      "$ref": "#/components/responses/Problem"
     },
     "401": {
      "$ref": "#/components/responses/Problem"
     },
     "403": {
      "$ref": "#/components/responses/Problem"
     },
     "409": {
      "$ref": "#/components/responses/Problem"
     },
     "429": {
      "$ref": "#/components/responses/RateLimited"
     }
    }
   }
  },
  "/requests/{requestId}": {
   "parameters": [
    {
     "name": "requestId",
     "in": "path",
     "required": true,
     "schema": {
      "type": "string",
      "pattern": "^ckq_[0-9A-HJKMNP-TV-Z]{26}$"
     }
    }
   ],
   "get": {
    "tags": [
     "requests"
    ],
    "operationId": "getRequest",
    "x-ckrets-stage": "beta",
    "summary": "Whether the person has answered (pending, fulfilled with the token, expired, frozen)",
    "description": "The asking key, or the request's manage token, as `Authorization: Bearer`. Never the value.",
    "security": [
     {
      "bearerKey": []
     },
     {
      "bearerManage": []
     }
    ],
    "responses": {
     "200": {
      "description": "State",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/RequestStatus"
        },
        "example": {
         "requestId": "ckq_SRG75K2M8D4QW7N3T6HYB1XZ9C",
         "state": "fulfilled",
         "kind": "otp",
         "prompt": "Enter the 6-digit code from your authenticator",
         "createdAt": "2026-11-06T01:11:09Z",
         "expiresAt": "2026-11-06T01:21:09Z",
         "fulfilledAt": "2026-11-06T01:12:40Z",
         "answeredBy": "a person on the request page",
         "token": "ck_SRG75X8D4QW7N3T6HYB1XZ9CA",
         "receiptId": "rcp_SRG75Y8D4QW7N3T6HYB1XZ9CA"
        }
       }
      }
     },
     "403": {
      "$ref": "#/components/responses/Problem"
     },
     "404": {
      "$ref": "#/components/responses/Problem"
     }
    }
   },
   "delete": {
    "tags": [
     "requests"
    ],
    "operationId": "cancelRequest",
    "x-ckrets-stage": "beta",
    "summary": "Cancel a pending request (it becomes expired; the page says it is no longer open)",
    "security": [
     {
      "bearerKey": []
     },
     {
      "bearerManage": []
     }
    ],
    "responses": {
     "204": {
      "description": "Cancelled, or already closed (idempotent)"
     },
     "403": {
      "$ref": "#/components/responses/Problem"
     }
    }
   }
  },
  "/requests/{requestId}/public": {
   "parameters": [
    {
     "name": "requestId",
     "in": "path",
     "required": true,
     "schema": {
      "type": "string"
     }
    }
   ],
   "get": {
    "tags": [
     "requests"
    ],
    "operationId": "getRequestPublic",
    "x-ckrets-stage": "beta",
    "summary": "What the request page shows (no credential); never the value",
    "security": [],
    "responses": {
     "200": {
      "description": "The public view",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/RequestPublic"
        },
        "example": {
         "requestId": "ckq_SRG75K2M8D4QW7N3T6HYB1XZ9C",
         "state": "pending",
         "kind": "otp",
         "prompt": "Enter the 6-digit code from your authenticator",
         "sender": {
          "kind": "key",
          "id": "key_7TQM4C8D4QW7N3T6HYB1XZ9C",
          "label": "deploy agent",
          "domain": {
           "name": "agents.example.com",
           "verified": false
          }
         },
         "expiresAt": "2026-11-06T01:21:09Z",
         "maxLength": 12
        }
       }
      }
     },
     "404": {
      "$ref": "#/components/responses/Problem"
     }
    }
   }
  },
  "/requests/{requestId}/answer": {
   "parameters": [
    {
     "name": "requestId",
     "in": "path",
     "required": true,
     "schema": {
      "type": "string"
     }
    }
   ],
   "post": {
    "tags": [
     "requests"
    ],
    "operationId": "answerRequest",
    "x-ckrets-stage": "beta",
    "summary": "The page's one answer (same origin only; agents never call this)",
    "description": "Taken only from the request page (the request's Origin must be this site's). One answer per request: the first\nthat fits is kept and the rest are 410. An answer that does not fit the kind is 400 (or 413) and the request\nstays open; the third closes it. Ten answers an hour per address. Success is `{state: fulfilled}`, never the value.\nFor a request made with encJwk the page sends the answer encrypted (value v1.<nonce>.<ciphertext>, clientEncrypted\ntrue, wrappedKey hpke.v1.<enc>.<ct>), checks its shape itself first, and a plain answer is 400.\n",
    "security": [],
    "requestBody": {
     "required": true,
     "content": {
      "application/json": {
       "schema": {
        "type": "object",
        "additionalProperties": false,
        "required": [
         "value"
        ],
        "properties": {
         "value": {
          "type": "string"
         },
         "clientEncrypted": {
          "type": "boolean"
         },
         "wrappedKey": {
          "type": "string"
         }
        }
       }
      }
     }
    },
    "responses": {
     "200": {
      "description": "Taken",
      "headers": {
       "Ckrets-Encryption": {
        "$ref": "#/components/headers/Ckrets-Encryption"
       }
      },
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "properties": {
          "state": {
           "type": "string",
           "enum": [
            "fulfilled"
           ]
          }
         }
        }
       }
      }
     },
     "400": {
      "$ref": "#/components/responses/Problem"
     },
     "403": {
      "$ref": "#/components/responses/Problem"
     },
     "404": {
      "$ref": "#/components/responses/Problem"
     },
     "410": {
      "$ref": "#/components/responses/Problem"
     },
     "413": {
      "$ref": "#/components/responses/Problem"
     },
     "429": {
      "$ref": "#/components/responses/RateLimited"
     }
    }
   }
  },
  "/keys": {
   "post": {
    "tags": [
     "keys"
    ],
    "operationId": "createKey",
    "x-ckrets-stage": "beta",
    "summary": "Mint a free key instantly",
    "description": "Reads no credential: a valid bearer of any kind (a key, a manage token) is ignored and a new key is minted (an\ninvalid one is still 401 invalid-credentials, as on every route); an Idempotency-Key is ignored too.\nIn v1 every key is a lease: 24 h, extended at any authenticated call made with it, whatever the answer (GET\n/me/key included; at most once a minute), to 24 h from that call, never past seven days from minting. An ended\nlease is 401 invalid-credentials on every route; its secrets still reveal to their TTL and their manage tokens\nstill work. A lease is revocable, named with the minting principal. Long-lived scoped keys,\nminted by a passkey-signed human on ckrets.ai, are planned. An email is optional, kept with the key for abuse notices only (not verified; no mail is sent in v1; v1 has no key recovery, so a lost key is gone: mint another); a `domain` lets the key verify webhooks and\nshow a name to humans answering requests. The key is shown once.\nDo not store a lease in CI: it ends within seven days. A CI job creates with its OIDC token instead (once built (planned) it\nmay hold a long-lived key a human minted with a passkey, restricted with `scope` to create, status and revoke).\n",
    "requestBody": {
     "required": false,
     "content": {
      "application/json": {
       "schema": {
        "type": "object",
        "additionalProperties": false,
        "description": "Any other field is 400 invalid-request naming it, and no key is minted.",
        "properties": {
         "clientEncryptionRequired": {
          "type": "boolean",
          "default": false,
          "description": "Refuse every value this key would make that ckrets seals itself: a create without clientEncrypted, a create through the hosted MCP server and a request are 400 plaintext-refused, nothing made or counted. Off by default; once on it stays on (PATCH /me/key turns it on; to be without it, mint a new key)"
         },
         "email": {
          "type": "string",
          "format": "email",
          "maxLength": 254,
          "description": "Abuse notices only; not verified, and v1 has no key recovery. Kept until 31 days after the lease's hard end"
         },
         "label": {
          "type": "string",
          "maxLength": 140,
          "description": "At most 140 characters, counted as Unicode code points; longer is 400 invalid-request, never cut"
         },
         "scope": {
          "type": "array",
          "items": {
           "$ref": "#/components/schemas/Scope"
          },
          "description": "Optional; defaults to every scope except delegate"
         },
         "domain": {
          "type": "string",
          "format": "hostname",
          "description": "Verified with POST /me/domain (planned); shown to humans on request pages; required for webhooks"
         }
        }
       }
      }
     }
    },
    "responses": {
     "201": {
      "description": "The key and its limits",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "required": [
          "key",
          "keyId",
          "tier",
          "lease",
          "limits"
         ],
         "properties": {
          "key": {
           "type": "string",
           "description": "Shown once. Bearer token."
          },
          "keyId": {
           "type": "string"
          },
          "tier": {
           "type": "string",
           "enum": [
            "free"
           ]
          },
          "clientEncryptionRequired": {
           "type": "boolean"
          },
          "lease": {
           "$ref": "#/components/schemas/Lease"
          },
          "limits": {
           "$ref": "#/components/schemas/TierLimits"
          }
         }
        }
       }
      }
     },
     "429": {
      "description": "quota-exhausted: 10 keys an hour or 30 a day per address, or 100 a day per network (limit mintsPerHour, mintsPerDay or mintsPerNetworkDay; resetsAt). A refused mint spends nothing",
      "$ref": "#/components/responses/RateLimited"
     }
    }
   }
  },
  "/me/key": {
   "get": {
    "tags": [
     "keys"
    ],
    "operationId": "getMyKey",
    "x-ckrets-stage": "beta",
    "summary": "This key's tier, lease window, scopes, verified domain and webhooks",
    "security": [
     {
      "bearerKey": []
     }
    ],
    "responses": {
     "200": {
      "description": "The key",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/KeyInfo"
        }
       }
      }
     },
     "401": {
      "description": "invalid-credentials: no key as the bearer. A manage token or a CI token is not a key here, and neither is no header",
      "$ref": "#/components/responses/Problem"
     }
    }
   },
   "delete": {
    "tags": [
     "keys"
    ],
    "operationId": "revokeMyKey",
    "x-ckrets-stage": "beta",
    "summary": "Revoke this key or lease now; every token and child lease under it dies with it",
    "description": "Felt everywhere within 5 seconds. Its unrevealed tokens answer 410 (title The creator's key was revoked); revealed ones stay revealed and receipts stand; a reveal already claimed completes. Its manage tokens still read status (state dead) and revoke. The key itself is then 401 invalid-credentials on every route.",
    "security": [
     {
      "bearerKey": []
     }
    ],
    "responses": {
     "204": {
      "description": "Revoked"
     },
     "401": {
      "$ref": "#/components/responses/Problem"
     }
    }
   },
   "patch": {
    "tags": [
     "keys"
    ],
    "operationId": "updateMyKey",
    "x-ckrets-stage": "beta",
    "summary": "Require client encryption for this key (one way)",
    "description": "The one change a key makes to itself. clientEncryptionRequired true makes every value this key would hand to ckrets in the clear 400 plaintext-refused from then on (within 5 seconds everywhere), including the answer to a request still open. It cannot be turned off: false is 400; mint a new key to be without it. Any other field is 400 invalid-request naming it.",
    "security": [
     {
      "bearerKey": []
     }
    ],
    "requestBody": {
     "required": true,
     "content": {
      "application/json": {
       "schema": {
        "type": "object",
        "additionalProperties": false,
        "required": [
         "clientEncryptionRequired"
        ],
        "properties": {
         "clientEncryptionRequired": {
          "type": "boolean",
          "enum": [
           true
          ]
         }
        }
       }
      }
     }
    },
    "responses": {
     "200": {
      "description": "The key, as GET /me/key shows it",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/KeyInfo"
        }
       }
      }
     },
     "400": {
      "description": "invalid-request: another field, or clientEncryptionRequired false (it cannot be turned off)",
      "$ref": "#/components/responses/Problem"
     },
     "401": {
      "$ref": "#/components/responses/Problem"
     }
    }
   }
  },
  "/me/domain": {
   "post": {
    "tags": [
     "keys"
    ],
    "operationId": "startDomainVerification",
    "x-ckrets-stage": "planned",
    "summary": "Start verifying a domain for this key (request pages show it; webhooks may only post to it)",
    "security": [
     {
      "bearerKey": []
     }
    ],
    "requestBody": {
     "required": true,
     "content": {
      "application/json": {
       "schema": {
        "type": "object",
        "required": [
         "domain"
        ],
        "properties": {
         "domain": {
          "type": "string",
          "format": "hostname"
         }
        }
       }
      }
     }
    },
    "responses": {
     "201": {
      "description": "Serve the token as text/plain at the path on the domain over HTTPS, then call /me/domain/verify within 24 h",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "properties": {
          "domain": {
           "type": "string"
          },
          "path": {
           "type": "string",
           "example": "/.well-known/ckrets-verify.txt"
          },
          "token": {
           "type": "string"
          },
          "expiresAt": {
           "type": "string",
           "format": "date-time"
          }
         }
        }
       }
      }
     },
     "401": {
      "$ref": "#/components/responses/Problem"
     },
     "501": {
      "$ref": "#/components/responses/NotInV1"
     }
    }
   }
  },
  "/me/domain/verify": {
   "post": {
    "tags": [
     "keys"
    ],
    "operationId": "finishDomainVerification",
    "x-ckrets-stage": "planned",
    "summary": "ckrets fetches the verification file from the domain over HTTPS and marks the domain verified if the token matches",
    "security": [
     {
      "bearerKey": []
     }
    ],
    "responses": {
     "200": {
      "description": "Verified",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "properties": {
          "domain": {
           "type": "string"
          },
          "verified": {
           "type": "boolean"
          }
         }
        }
       }
      }
     },
     "422": {
      "description": "Not found or token mismatch (type domain-unverified)",
      "$ref": "#/components/responses/Problem"
     },
     "501": {
      "$ref": "#/components/responses/NotInV1"
     }
    }
   }
  },
  "/me/key/delegate": {
   "post": {
    "tags": [
     "keys"
    ],
    "operationId": "delegateLease",
    "x-ckrets-stage": "planned",
    "summary": "Mint a narrower child lease for another agent (one level)",
    "description": "Only a lease whose scope includes `delegate`. The child's scope is a subset, its TTL at most the parent's remaining time, its audience fixed, and it cannot delegate further. Revoking the parent revokes the child.",
    "security": [
     {
      "bearerKey": []
     }
    ],
    "requestBody": {
     "required": true,
     "content": {
      "application/json": {
       "schema": {
        "type": "object",
        "required": [
         "scope",
         "audience"
        ],
        "properties": {
         "scope": {
          "type": "array",
          "items": {
           "$ref": "#/components/schemas/Scope"
          }
         },
         "ttl": {
          "type": "string"
         },
         "audience": {
          "$ref": "#/components/schemas/Audience"
         },
         "label": {
          "type": "string",
          "maxLength": 140,
          "description": "At most 140 characters (code points), as on POST /keys"
         }
        }
       }
      }
     }
    },
    "responses": {
     "201": {
      "description": "The child lease (shown once)",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "properties": {
          "key": {
           "type": "string"
          },
          "keyId": {
           "type": "string"
          },
          "lease": {
           "$ref": "#/components/schemas/Lease"
          }
         }
        }
       }
      }
     },
     "403": {
      "$ref": "#/components/responses/Problem"
     },
     "501": {
      "$ref": "#/components/responses/NotInV1"
     }
    }
   }
  },
  "/me/webhooks": {
   "post": {
    "tags": [
     "webhooks"
    ],
    "operationId": "createWebhook",
    "x-ckrets-stage": "planned",
    "summary": "Register a signed webhook on this key",
    "description": "The URL's host must be the key's verified domain (or, for a signed agent, the signing directory's host); ckrets never\nposts to an unverified host, so a prompt-injected agent cannot point alarms at an attacker. Deliveries are a compact JWS\n(ES256, same keys as receipts) with the event as payload, retried with backoff for 24 h. Events: secret.created,\nsecret.revealed, secret.reveal_denied, secret.consumed_attempt (a reveal tried after the last one), secret.expired_unrevealed,\nsecret.revoked, batch.completed, request.fulfilled, request.expired, lease.renewed, lease.revoked.\n",
    "security": [
     {
      "bearerKey": []
     },
     {
      "httpSignature": []
     }
    ],
    "requestBody": {
     "required": true,
     "content": {
      "application/json": {
       "schema": {
        "type": "object",
        "required": [
         "url",
         "events"
        ],
        "properties": {
         "url": {
          "type": "string",
          "format": "uri"
         },
         "events": {
          "type": "array",
          "items": {
           "type": "string"
          }
         }
        }
       }
      }
     }
    },
    "responses": {
     "201": {
      "description": "Registered",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "properties": {
          "webhookId": {
           "type": "string"
          },
          "url": {
           "type": "string"
          },
          "events": {
           "type": "array",
           "items": {
            "type": "string"
           }
          }
         }
        }
       }
      }
     },
     "422": {
      "description": "Host not verified for this key (type webhook-unverified)",
      "$ref": "#/components/responses/Problem"
     },
     "501": {
      "$ref": "#/components/responses/NotInV1"
     }
    }
   }
  },
  "/me/webhooks/{webhookId}": {
   "parameters": [
    {
     "name": "webhookId",
     "in": "path",
     "required": true,
     "schema": {
      "type": "string"
     }
    }
   ],
   "delete": {
    "tags": [
     "webhooks"
    ],
    "operationId": "deleteWebhook",
    "x-ckrets-stage": "planned",
    "summary": "Remove a webhook",
    "security": [
     {
      "bearerKey": []
     }
    ],
    "responses": {
     "204": {
      "description": "Removed"
     },
     "401": {
      "$ref": "#/components/responses/Problem"
     },
     "501": {
      "$ref": "#/components/responses/NotInV1"
     }
    }
   }
  },
  "/limits": {
   "get": {
    "tags": [
     "limits"
    ],
    "operationId": "getLimits",
    "x-ckrets-stage": "beta",
    "summary": "The caller's tier, limits, remaining creates, reset times, the stage and the stamp parameters",
    "responses": {
     "200": {
      "description": "Limits",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Limits"
        },
        "example": {
         "tier": "anonymous",
         "limits": {
          "valueBytes": 1024,
          "ttlMax": "P7D",
          "reveals": 1,
          "createsPerHour": 60,
          "createsPerDay": 300,
          "netCreatesPerHour": 240,
          "netCreatesPerDay": 1200,
          "revealAttempts": 5
         },
         "remaining": {
          "createsThisHour": 59,
          "createsToday": 299
         },
         "resetsAt": {
          "hour": "2026-10-24T02:00:00Z",
          "day": "2026-10-25T00:00:00Z"
         },
         "stamp": {
          "alg": "sha256",
          "difficulty": 11,
          "nonce": "Q4M8V2…",
          "expiresAt": "2026-10-24T01:16:09Z"
         }
        }
       }
      }
     },
     "401": {
      "description": "A bearer that is not accepted (type invalid-credentials)",
      "$ref": "#/components/responses/Problem"
     }
    }
   }
  },
  "/report": {
   "post": {
    "tags": [
     "trust"
    ],
    "operationId": "reportToken",
    "x-ckrets-stage": "beta",
    "summary": "Report a token or URL for abuse; takedown is by token",
    "requestBody": {
     "required": true,
     "content": {
      "application/json": {
       "schema": {
        "type": "object",
        "required": [
         "reason"
        ],
        "properties": {
         "token": {
          "type": "string"
         },
         "url": {
          "type": "string",
          "format": "uri"
         },
         "reason": {
          "type": "string",
          "maxLength": 2000
         },
         "contact": {
          "type": "string",
          "format": "email"
         }
        }
       }
      }
     }
    },
    "responses": {
     "202": {
      "description": "Received, with a case id; the same answer whether or not the token exists. A bearer token is frozen pending review, so anyone holding its URL can freeze it (name an audience when that matters); an audience-bound token is recorded, not frozen (its audience already keeps strangers out). The creator sees it in status (reports, frozen, lastCase); webhooks are planned. One report per token per address counts once",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "properties": {
          "caseId": {
           "type": "string"
          }
         }
        }
       }
      }
     },
     "400": {
      "description": "No reason given (type invalid-request)",
      "$ref": "#/components/responses/Problem"
     },
     "429": {
      "description": "More than 10 reports an hour from this address (type quota-exhausted, limit reportsPerHour)",
      "$ref": "#/components/responses/Problem"
     }
    }
   }
  },
  "/receipts/verify": {
   "post": {
    "tags": [
     "trust"
    ],
    "operationId": "verifyReceipt",
    "x-ckrets-stage": "beta",
    "summary": "Convenience check of a receipt; verify offline against /.well-known/jwks.json when you can",
    "requestBody": {
     "required": true,
     "content": {
      "application/json": {
       "schema": {
        "type": "object",
        "required": [
         "receipt"
        ],
        "properties": {
         "receipt": {
          "type": "string"
         }
        }
       }
      }
     }
    },
    "responses": {
     "200": {
      "description": "Always 200: {valid: true, claims} if valid; {valid: false} for a tampered, malformed or missing receipt",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "properties": {
          "valid": {
           "type": "boolean"
          },
          "claims": {
           "$ref": "#/components/schemas/ReceiptClaims"
          }
         }
        }
       }
      }
     },
     "401": {
      "description": "invalid-credentials: an Authorization header that is not a valid credential (refused on every route, even where none is needed); send none here",
      "$ref": "#/components/responses/Problem"
     }
    }
   }
  }
 },
 "components": {
  "securitySchemes": {
   "bearerKey": {
    "type": "http",
    "scheme": "bearer",
    "description": "Authorization: Bearer ck_live_…  The key from POST /keys, shown once; leases look the same. Give senders your keyId (key_…), never the key. One header for every credential; the kind is read from the prefix."
   },
   "bearerManage": {
    "type": "http",
    "scheme": "bearer",
    "description": "Authorization: Bearer ckm_…  The manage token from a create response: revoke, status and policy changes for that one secret, the anonymous creator's only credential; retired when the secret is adopted (POST /me/secrets/adopt) by a key or an account. Not a reveal credential: on GET /v1/secrets/{token} a ckm_ bearer is treated as no credential (anonymous)."
   },
   "oidcToken": {
    "type": "http",
    "scheme": "bearer",
    "bearerFormat": "JWT",
    "description": "A CI identity token (GitHub Actions, GitLab, CircleCI) with aud https://ckrets.ai: at create, the creator identity (free-key limits per repository, nothing stored in CI); at reveal, the proof for an audience of type oidc."
   },
   "layerIam": {
    "type": "apiKey",
    "in": "header",
    "name": "Authorization",
    "description": "AWS Signature Version 4 (service execute-api) by a first-party app's caller role in the API's account (puzzlebox-layer-caller, assumable only by the app layer's own role): the private API's IAM authorizer checks it. Nothing to mint, store or paste"
   },
   "anonymousProof": {
    "type": "apiKey",
    "in": "header",
    "name": "Ckrets-Proof",
    "description": "The anonymous proof, kind=value: `stamp=<nonce>:<counter>` (the agent API's proof of work), `session=<ticket>` from POST /sessions (one Turnstile solve, verified there and only there, covers a create and up to three generator calls). A first-party app sends Ckrets-App beside it"
   },
   "accountSession": {
    "type": "http",
    "scheme": "bearer",
    "description": "A ckrets.ai account session (cka_…: passkey sign-in on ckrets.ai), issued to a first-party app by redirect and scoped to that app; 12 hours, no refresh"
   },
   "httpSignature": {
    "type": "apiKey",
    "in": "header",
    "name": "Signature-Input",
    "description": "Profile: /docs/signing.md. RFC 9421 HTTP message signatures (Web Bot Auth): Signature-Input and Signature headers; keyid resolvable in the operator's /.well-known/http-message-signatures-directory. Satisfies audiences of type publicKey now; the signed tier and httpsig audiences are planned."
   }
  },
  "headers": {
   "RateLimit-Limit": {
    "schema": {
     "type": "string"
    },
    "description": "IETF RateLimit header fields; the tightest active limit"
   },
   "RateLimit-Remaining": {
    "schema": {
     "type": "integer"
    }
   },
   "RateLimit-Reset": {
    "schema": {
     "type": "integer"
    },
    "description": "Seconds until the tightest active limit resets"
   },
   "Ckrets-Request-Id": {
    "schema": {
     "type": "string"
    },
    "description": "On every answer: this request's id, the same as a problem's requestId. Quote it when you report a problem"
   },
   "Ckrets-Version": {
    "schema": {
     "type": "string",
     "pattern": "^\\d{4}-\\d{2}-\\d{2}$"
    },
    "description": "On every answer: the dated contract it follows. A request may send it to name a contract; an unserved date is 400 version-unknown; no header means the original contract. 2026-11-06, the beta launch date, is the first"
   },
   "Ckrets-Tier": {
    "schema": {
     "$ref": "#/components/schemas/Tier"
    },
    "description": "On every response: the tier row that applied"
   },
   "Ckrets-Encryption": {
    "schema": {
     "type": "string",
     "x-extensible-enum": true,
     "enum": [
      "client",
      "server"
     ]
    },
    "description": "On every create's 201 (a replay too) and a request answer's 200: client when the value arrived encrypted on the sender's side (clientEncrypted true), server when ckrets sealed it, so ckrets could read it. The receipt's ce claim says the same"
   },
   "Deprecation": {
    "schema": {
     "type": "string"
    },
    "description": "RFC 9745, on every response to a call that uses a retired feature: `@<unix seconds>` when it was deprecated"
   },
   "Sunset": {
    "schema": {
     "type": "string"
    },
    "description": "RFC 8594, beside Deprecation: the HTTP date after which the feature answers 410 feature-sunset"
   },
   "Link": {
    "schema": {
     "type": "string"
    },
    "description": "Beside Deprecation: `<replacement docs url>; rel=\"deprecation\"`"
   },
   "Ckrets-App": {
    "schema": {
     "type": "string",
     "example": "puzzlebox"
    },
    "description": "On every response: the app whose tier row applied, api (the agent API, the default) or the first-party app the request named"
   }
  },
  "schemas": {
   "State": {
    "type": "string",
    "x-extensible-enum": true,
    "enum": [
     "scheduled",
     "pending",
     "partially_revealed",
     "revealed",
     "expired_unrevealed",
     "revoked",
     "dead"
    ],
    "description": "One vocabulary for a secret's state everywhere (status, the list filter, the public view): scheduled (a dated box before opensAt), pending (open, nothing revealed yet), partially_revealed, revealed (fully consumed), expired_unrevealed, revoked, dead (no one can open it again: its attempts are exhausted, a report froze it, or its creator's key was revoked). Scheduled, pending and partially_revealed can still change (partially_revealed is also where a secret ends that expired after some of its reveals); revealed, expired_unrevealed and revoked are final; dead is final unless a review lifts a report's freeze. New values may be added; accept unknown ones"
   },
   "Tier": {
    "type": "string",
    "x-extensible-enum": true,
    "enum": [
     "anonymous",
     "signed",
     "free",
     "passkey",
     "plus",
     "paid"
    ],
    "description": "The tier row that applied, per (app, tier). New tiers may be added; accept unknown ones"
   },
   "Audience": {
    "description": "Who may reveal, discriminated by `type`. Without one the token is bearer; anyone with the URL reveals it once. A type the server does not know is refused with 400 audience-type-unknown; new types are added within v1",
    "oneOf": [
     {
      "type": "object",
      "required": [
       "type",
       "id"
      ],
      "properties": {
       "type": {
        "const": "key"
       },
       "id": {
        "type": "string",
        "description": "A ckrets key or lease id: key_ and 26 uppercase Crockford base32 characters (another form is 400 invalid-request); the recipient reveals with exactly that key (v1 has no child leases). Checked for form only: a well-formed id that does not exist makes the secret unrevealable (the creator's status shows audienceHint)"
       }
      }
     },
     {
      "type": "object",
      "required": [
       "type",
       "issuer"
      ],
      "properties": {
       "type": {
        "const": "oidc"
       },
       "issuer": {
        "type": "string",
        "format": "uri",
        "description": "GitHub Actions, GitLab.com or a CircleCI org issuer; any other is 400 invalid-request"
       },
       "claims": {
        "type": "object",
        "additionalProperties": {
         "type": "string"
        },
        "description": "Each listed claim must equal the presented JWT's exactly (a case-sensitive string compare); claims not listed are ignored. Values must be strings, and aud may not be listed (400 invalid-request): the JWT's aud must include https://ckrets.ai. They must name one job: sub, or repository (or GitHub's repository_id, which survives a rename or transfer) with ref or job_workflow_ref; an issuer alone, or repository alone, is 400 audience-invalid. repository with ref still admits every workflow on that ref, so prefer job_workflow_ref; Under GitLab or CircleCI the job is named by sub: repository, repository_id and job_workflow_ref exist only in GitHub Actions tokens, so naming either there is 400 audience-invalid (other claims, such as GitLab's ref or project_path, may narrow sub further)"
       }
      }
     },
     {
      "type": "object",
      "required": [
       "type",
       "keyid"
      ],
      "description": "Planned, with the signed tier; until then a create naming it is 400 audience-type-unknown",
      "properties": {
       "type": {
        "const": "httpsig"
       },
       "keyid": {
        "type": "string"
       },
       "directory": {
        "type": "string",
        "format": "uri",
        "description": "The recipient's /.well-known/http-message-signatures-directory"
       }
      }
     },
     {
      "type": "object",
      "required": [
       "type",
       "jwk"
      ],
      "properties": {
       "type": {
        "const": "publicKey"
       },
       "jwk": {
        "type": "object",
        "description": "Ed25519 or P-256 public signing key; the reveal request is signed with it per /docs/signing.md (keyid = its RFC 7638 thumbprint)"
       },
       "encJwk": {
        "type": "object",
        "description": "Optional X25519 public key; with clientEncrypted the SDK wraps the data key to it with HPKE (/docs/client-encryption.md)"
       }
      }
     }
    ]
   },
   "Scope": {
    "type": "string",
    "x-extensible-enum": true,
    "enum": [
     "create",
     "reveal",
     "revoke",
     "status",
     "manage",
     "batch",
     "request",
     "webhooks",
     "delegate"
    ],
    "description": "What a key may do, checked on every route the key is presented to: create needs create, reveal needs reveal (also for a secret addressed to that key), a secret's status needs status and its revoke needs revoke, manage covers PATCH policy and adopt; a missing scope is 403 scope-missing, nothing read or spent. The manage token needs no scope for status and revoke."
   },
   "CreateSecret": {
    "type": "object",
    "required": [
     "value"
    ],
    "additionalProperties": false,
    "description": "Any other top-level field is 400 invalid-request naming it, and nothing is made: a misspelt field (audeince for audience) is refused, never ignored. On a onetime create, public is accepted and ignored. A puzzle create takes template, value, clientEncrypted, wrappedKey, purpose (the box's title: at most 140 code points, shown to its owner, never at /public), public and puzzle; its expiry is puzzle.window, so a top-level ttl is refused there.",
    "properties": {
     "value": {
      "type": "string",
      "minLength": 1,
      "maxLength": 1048576,
      "description": "Text; send binary as base64 (1 KB of base64 holds 768 raw bytes). Missing, empty or not a string is 400 invalid-request. Measured in UTF-8 bytes of the decoded string (after JSON escapes, not the request body; the ciphertext string when client-encrypted; wrappedKey not counted); 1 KB is 1,024 bytes. Up to 1 KB anonymous, 4 KB signed (planned), 64 KB free, 1 MB paid; over it is 413 value-too-large"
     },
     "ttl": {
      "type": "string",
      "default": "7d",
      "description": "One whole number and one unit: <n>m, <n>h or <n>d with a lowercase unit, or an ISO 8601 duration P[nD][T[nH][nM][nS]] with uppercase letters. Malformed, so 400 invalid-request (never ttl-too-long, even when also over the maximum): combined shorthand (1h30m, 4h1m, 4h1s), a fraction (1.5h, PT0.5S), seconds shorthand (60s), the wrong letter case (15M, 1H, pt1m), years, months or weeks (P1Y, P1M, P1W). A well-formed TTL over the maximum (8d or P7DT1M anonymous) is ttl-too-long, and so is \"never\" (no expiry) on every tier but paid. At least 60 s (less, zero or negative is 400 invalid-request); starts at create and a reveal never extends it. 7d by default on every tier. The maximum is allowed (inclusive). Max 7d anonymous, 7d signed (planned), 365d free; a paid key (later) may also send never, for no expiry"
     },
     "reveals": {
      "type": "integer",
      "minimum": 1,
      "maximum": 1000,
      "default": 1,
      "description": "Up to 1 anonymous, 3 signed (planned), 10 free, 1,000 paid; over the tier's maximum is 400 invalid-request, never clamped"
     },
     "passphrase": {
      "type": "string",
      "minLength": 1,
      "maxLength": 256,
      "description": "1 to 256 characters, counted as Unicode code points (an emoji counts 1), as maxLength counts; well-formed Unicode only (a lone surrogate is 400). Compared exactly as sent: case-sensitive, no Unicode normalisation; stored only as a salted scrypt hash, compared in constant time"
     },
     "audience": {
      "$ref": "#/components/schemas/Audience"
     },
     "template": {
      "type": "string",
      "enum": [
       "onetime",
       "puzzle"
      ],
      "x-extensible-enum": true,
      "default": "onetime",
      "description": "onetime: reveal once to whoever proves the policy (the agent default). puzzle: a riddle, game or dated box"
     },
     "puzzle": {
      "$ref": "#/components/schemas/PuzzlePolicy"
     },
     "public": {
      "type": "object",
      "description": "Puzzle template only: ignored on a onetime create (not stored, never shown at /public). Up to 4 KB of metadata anyone with the token may read at GET /secrets/{token}/public (a riddle, the answer shape: a string or an array of part labels). Never put the value, the salt, a commitment or a hint here (a hint costs an attempt)"
     },
     "purpose": {
      "type": "string",
      "maxLength": 140,
      "description": "Why this hand-off exists; at most 140 characters, counted as Unicode code points. Goes into the receipt and the status, never shown to the recipient before reveal"
     },
     "clientEncrypted": {
      "type": "boolean",
      "default": false
     },
     "wrappedKey": {
      "type": "string",
      "maxLength": 4096,
      "description": "Opaque: never parsed, so a malformed one is stored and returned as sent; at most 4,096 characters (400 beyond), not counted in the value's size. Required when clientEncrypted is true: hpke.v1.<enc>.<ct>, fragment.v1, pass.v1.<salt>.<m>.<t>.<p>, or answer.v1 for riddle boxes (the wraps are in puzzle.wraps) (/docs/client-encryption.md)"
     }
    },
    "if": {
     "properties": {
      "clientEncrypted": {
       "const": true
      }
     },
     "required": [
      "clientEncrypted"
     ]
    },
    "then": {
     "required": [
      "wrappedKey"
     ]
    }
   },
   "SecretCreated": {
    "type": "object",
    "required": [
     "token",
     "url",
     "expiresAt",
     "revealsLeft",
     "digest",
     "digestKey",
     "receipt"
    ],
    "properties": {
     "token": {
      "type": "string"
     },
     "url": {
      "type": "string",
      "format": "uri"
     },
     "manageToken": {
      "type": "string"
     },
     "expiresAt": {
      "type": "string",
      "format": "date-time"
     },
     "revealsLeft": {
      "type": "integer"
     },
     "digest": {
      "type": "string",
      "description": "hmac-sha256: + base64url HMAC-SHA256(digestKey, stored value), the stored value being the ciphertext string when clientEncrypted. Unguessable without digestKey, so it can sit in status, receipts and approval dialogs"
     },
     "digestKey": {
      "type": "string",
      "description": "32 random bytes, base64url, made per secret. Returned to the creator here and to the recipient at reveal, never in status or receipts. With the value it recomputes digest, proving which value a receipt refers to"
     },
     "audience": {
      "$ref": "#/components/schemas/Audience",
      "description": "Echoed only when the create set an audience. A 201 without audience made a bearer secret: anyone with the URL reveals it"
     },
     "receipt": {
      "type": "string",
      "description": "Compact JWS (ES256) with ReceiptClaims typ create; key in /.well-known/jwks.json"
     }
    }
   },
   "SecretRevealed": {
    "type": "object",
    "required": [
     "value",
     "revealsLeft",
     "receipt"
    ],
    "properties": {
     "value": {
      "type": "string"
     },
     "digestKey": {
      "type": "string",
      "description": "The secret's digestKey, so the recipient can check the receipt's dig against the value"
     },
     "revealsLeft": {
      "type": "integer"
     },
     "sender": {
      "type": "object",
      "description": "A secret's reveal (not a puzzle box's): who created it, exactly as GET /public shows it, set from the creator's credential (the receipt's cre): {kind: anonymous} | {kind: key, id} | {kind: oidc, issuer, subject}. It says nothing more than that credential proved",
      "properties": {
       "kind": {
        "type": "string",
        "x-extensible-enum": true,
        "enum": [
         "anonymous",
         "key",
         "oidc",
         "other"
        ]
       },
       "id": {
        "type": "string"
       },
       "issuer": {
        "type": "string"
       },
       "subject": {
        "type": "string"
       }
      }
     },
     "clientEncrypted": {
      "type": "boolean"
     },
     "wrappedKey": {
      "type": "string"
     },
     "wrap": {
      "type": "string",
      "description": "riddle boxes: the wrap whose commitment matched; the browser unwraps it with the answer-derived key"
     },
     "receipt": {
      "type": "string",
      "description": "Compact JWS (ES256) with ReceiptClaims typ reveal"
     }
    }
   },
   "SecretStatus": {
    "type": "object",
    "required": [
     "state",
     "reveals",
     "revealsLeft",
     "attempts",
     "createdAt",
     "expiresAt"
    ],
    "properties": {
     "encryption": {
      "type": "object",
      "description": "Read-only: who holds the key that wraps this secret's data key, set on the account (never on a create). custody is ckrets (our KMS key) or customer (the customer's own KMS key); dedicatedKey says the account has a KMS key of its own. Revoking or disabling the key takes effect everywhere within the key cache's lifetime: about 5 minutes for ckrets, about 60 seconds for customer",
      "properties": {
       "custody": {
        "type": "string",
        "x-extensible-enum": true,
        "enum": [
         "ckrets",
         "customer"
        ]
       },
       "dedicatedKey": {
        "type": "boolean"
       }
      }
     },
     "state": {
      "$ref": "#/components/schemas/State"
     },
     "reveals": {
      "type": "integer"
     },
     "revealsLeft": {
      "type": "integer"
     },
     "attempts": {
      "type": "integer",
      "description": "Counted attempts. Onetime secrets: wrong passphrases only; a success, a cooldown refusal and a wrong audience proof are not counted. Puzzle boxes: every claim counts when made, the one that opens the box included (and every hint)"
     },
     "denials": {
      "type": "integer",
      "description": "Every refused proof: wrong passphrases and wrong audience proofs (each with a deny receipt)"
     },
     "consumedAttempts": {
      "type": "integer",
      "description": "Reveal attempts made after the last permitted reveal (an alarm condition)"
     },
     "reports": {
      "type": "integer",
      "description": "Abuse reports on this token (one per reporting address)"
     },
     "frozen": {
      "type": "boolean",
      "description": "A report froze it (bearer tokens only; an audience-bound token is recorded, not frozen); lifting a freeze is a review's act"
     },
     "lastCase": {
      "type": "string",
      "description": "The latest report's case id, to quote to the review"
     },
     "audienceHint": {
      "type": "string",
      "description": "Present only while the secret is live and its audience key does not exist or is revoked: no one can open it. Revoke it and create it again for a key that exists. Shown in status only (the creator), never in /public"
     },
     "createdAt": {
      "type": "string",
      "format": "date-time"
     },
     "expiresAt": {
      "type": "string",
      "format": "date-time"
     },
     "revealedAt": {
      "type": "array",
      "items": {
       "type": "string",
       "format": "date-time"
      }
     },
     "revealedBy": {
      "type": "array",
      "items": {
       "$ref": "#/components/schemas/Audience"
      },
      "description": "The proven recipient of each reveal that proved an audience (a key as {type: key, id}; a CI job as {type: oidc, issuer, claims: {sub}}; a signature as {type: publicKey, jwk}, the audience's public key); a bearer reveal or a puzzle open appears in revealedAt only"
     },
     "lastDeniedAt": {
      "type": "string",
      "format": "date-time"
     },
     "digest": {
      "type": "string"
     },
     "purpose": {
      "type": "string"
     },
     "audience": {
      "$ref": "#/components/schemas/Audience"
     },
     "id": {
      "type": "string",
      "description": "The secret's id: sha256: + hex SHA-256 of its token, as receipts show it (tok); manage by it at /me/secrets/{id}"
     },
     "app": {
      "type": "string",
      "description": "api or a first-party app"
     },
     "template": {
      "type": "string",
      "x-extensible-enum": true,
      "enum": [
       "onetime",
       "puzzle"
      ]
     },
     "mode": {
      "type": "string",
      "description": "puzzle boxes: riddle, game or dated"
     },
     "opensAt": {
      "type": "string",
      "format": "date-time"
     },
     "title": {
      "type": "string",
      "description": "puzzle boxes: the start of public.riddle, for a list"
     }
    }
   },
   "PuzzlePolicy": {
    "type": "object",
    "description": "The puzzle template's policy. Key derivation is the client's: the salt and the link token travel in the share link's fragment, so the API stores commitments and ciphertext, never an answer or a salt",
    "required": [
     "mode"
    ],
    "properties": {
     "mode": {
      "type": "string",
      "enum": [
       "riddle",
       "game",
       "dated"
      ]
     },
     "strictness": {
      "type": "string",
      "enum": [
       "strict",
       "standard",
       "relaxed"
      ],
      "description": "riddle boxes: strict compares commitments with no model; standard and relaxed go through an attempt ticket and the puzzlebox normaliser"
     },
     "attempts": {
      "type": "integer",
      "minimum": 1,
      "maximum": 50
     },
     "cooldown": {
      "type": "string",
      "description": "Optional override of the default schedule, which runs from each claim: claim n+1 waits 2^(n-1) s after claim n; ISO 8601 base duration"
     },
     "hints": {
      "type": "array",
      "maxItems": 5,
      "items": {
       "type": "string",
       "maxLength": 280
      },
      "description": "At most 5, each at most 280 characters (code points); more, or longer, is 400 invalid-request, never cut. Platform-held, never public; hint n is released only by a counted claim (POST /secrets/{token}/attempts {hint: n})"
     },
     "revealOnce": {
      "type": "boolean",
      "default": true
     },
     "window": {
      "type": "string",
      "pattern": "^P[1-9][0-9]*D$",
      "description": "How long the box stays open after it opens, in whole days (P7D, the default). Past the tier's maximum (P7D anonymous, P30D signed in) is 400 ttl-too-long naming the longest allowed, and nothing is created or counted: never clamped"
     },
     "opensAt": {
      "type": "string",
      "format": "date-time",
      "description": "dated boxes: nothing reveals before this, on the service clock. Must be a future time less than 366 days away (a past or later time is 400 invalid-request, nothing created); kept to the whole second, rounded up"
     },
     "commitments": {
      "type": "array",
      "maxItems": 12,
      "items": {
       "type": "string"
      },
      "description": "base64url commitments, one per accepted answer; never returned by any call"
     },
     "wraps": {
      "type": "array",
      "maxItems": 12,
      "items": {
       "type": "string"
      },
      "description": "riddle boxes: the data key wrapped once per accepted answer, by that answer's derived key, in the same order as commitments; each <iv>.<ciphertext>, base64url (16 and 64 characters); a reveal returns only the wrap whose commitment matched (wrappedKey answer.v1, /docs/client-encryption.md)"
     },
     "judge": {
      "type": "string",
      "description": "game boxes: the verifier id whose signed verdict the policy accepts"
     },
     "rubric": {
      "type": "string",
      "maxLength": 800,
      "description": "game boxes (required there): what the judge reads to decide a verdict. At most 800 characters (code points); longer is 400 invalid-request, never cut. Platform-held: never public, read only by the app's own layer with a live ticket"
     }
    }
   },
   "PublicView": {
    "type": "object",
    "description": "What anyone holding the token may read before revealing: a puzzle box's public metadata, or for a onetime secret what the reveal page at /r/<token> shows (sender, state, expiry, size, passphrase or audience). Never the value, the purpose, the digest or a wrapped key.",
    "properties": {
     "template": {
      "type": "string",
      "x-extensible-enum": true,
      "enum": [
       "onetime",
       "puzzle"
      ]
     },
     "sender": {
      "type": "object",
      "description": "onetime: who created it: {kind: anonymous} | {kind: key, id} | {kind: oidc, issuer, subject}",
      "properties": {
       "kind": {
        "type": "string",
        "x-extensible-enum": true,
        "enum": [
         "anonymous",
         "key",
         "oidc",
         "other"
        ]
       },
       "id": {
        "type": "string"
       },
       "issuer": {
        "type": "string"
       },
       "subject": {
        "type": "string"
       }
      }
     },
     "reveals": {
      "type": "integer",
      "description": "onetime: reveals in all"
     },
     "revealsLeft": {
      "type": "integer"
     },
     "size": {
      "type": "integer",
      "description": "onetime: the value's size in bytes"
     },
     "passphrase": {
      "type": "boolean",
      "description": "onetime: a passphrase is required"
     },
     "audience": {
      "type": "object",
      "description": "onetime: who may reveal (a public key by its thumbprint, keyid, only)"
     },
     "clientEncrypted": {
      "type": "boolean"
     },
     "wrappedKey": {
      "type": "string",
      "description": "onetime, client-encrypted: the wrapped-key kind only (fragment.v1, hpke.v1, pass.v1), never the wrapped key"
     },
     "mode": {
      "type": "string"
     },
     "strictness": {
      "type": "string"
     },
     "state": {
      "$ref": "#/components/schemas/State"
     },
     "attemptBudget": {
      "type": "integer",
      "description": "The box's attempts in all (the padlocks)"
     },
     "attemptsLeft": {
      "type": "integer"
     },
     "hintCount": {
      "type": "integer",
      "description": "How many hints the box has; the hints themselves cost an attempt each"
     },
     "cooldownUntil": {
      "type": [
       "string",
       "null"
      ],
      "format": "date-time"
     },
     "cooldownMs": {
      "type": "integer",
      "description": "Milliseconds until the next claim may be made (0 when none)"
     },
     "opensAt": {
      "type": [
       "string",
       "null"
      ],
      "format": "date-time"
     },
     "expiresAt": {
      "type": "string",
      "format": "date-time"
     },
     "revealOnce": {
      "type": "boolean"
     },
     "now": {
      "type": "string",
      "format": "date-time",
      "description": "The service clock, for a countdown (dated boxes open on it)"
     },
     "public": {
      "type": "object"
     }
    }
   },
   "AttemptTicket": {
    "type": "object",
    "description": "A ticket, or with {hint: n} the hint instead",
    "required": [
     "attempt",
     "attemptsLeft"
    ],
    "properties": {
     "ticket": {
      "type": "string",
      "description": "KMS HMAC over the token's hash, the attempt number and the expiry; valid 120 s, for one reveal; resend the same ticket after a busy answer"
     },
     "attempt": {
      "type": "integer"
     },
     "attemptsLeft": {
      "type": "integer"
     },
     "expiresAt": {
      "type": "string",
      "format": "date-time"
     },
     "hint": {
      "type": "string"
     },
     "hintNumber": {
      "type": "integer"
     },
     "cooldownMs": {
      "type": "integer",
      "description": "Milliseconds until the next claim may be made"
     }
    }
   },
   "SessionTicket": {
    "type": "object",
    "required": [
     "session",
     "expiresAt"
    ],
    "properties": {
     "session": {
      "type": "string",
      "description": "KMS HMAC; one create and up to three generator calls within 10 minutes; sent as Ckrets-Proof: session=<ticket>"
     },
     "expiresAt": {
      "type": "string",
      "format": "date-time"
     },
     "creates": {
      "type": "integer"
     },
     "generatorCalls": {
      "type": "integer"
     }
    }
   },
   "VerifierClaim": {
    "type": "string",
    "description": "A compact JWS (ES256, typ ckrets-verdict+jwt) from the verifier the policy names (the puzzlebox judge for game boxes), signed with KMS Sign on the app's verdict key. Payload: pass (boolean), code (a VerdictCode: #/components/schemas/VerdictCode, never free text), verifier, nonce (the ticket's tid, from its payload, not the ticket string), sub (the box's token hash th), exp (no later than the ticket's expiry, at most 5 minutes out)"
   },
   "VerdictCode": {
    "type": "string",
    "x-extensible-enum": true,
    "enum": [
     "pass",
     "fail",
     "ambiguous",
     "manipulation",
     "one",
     "reused",
     "long",
     "unreadable",
     "busy"
    ],
    "description": "The code in a verifier claim and the outcome of a puzzle attempt: a fixed list the pages map to fixed sentences, never the judge's free text; busy is not counted. New codes may be added; show an unknown code as the generic miss"
   },
   "RevealProof": {
    "oneOf": [
     {
      "type": "object",
      "required": [
       "passphrase"
      ],
      "properties": {
       "passphrase": {
        "type": "string",
        "maxLength": 256
       }
      }
     },
     {
      "type": "object",
      "required": [
       "commitment"
      ],
      "properties": {
       "commitment": {
        "type": "string"
       }
      }
     },
     {
      "type": "object",
      "required": [
       "ticket",
       "commitment"
      ],
      "properties": {
       "ticket": {
        "type": "string"
       },
       "commitment": {
        "type": "string"
       }
      }
     },
     {
      "type": "object",
      "required": [
       "ticket",
       "verdict"
      ],
      "properties": {
       "ticket": {
        "type": "string"
       },
       "verdict": {
        "$ref": "#/components/schemas/VerifierClaim"
       }
      }
     }
    ]
   },
   "PolicyPatch": {
    "type": "object",
    "additionalProperties": false,
    "description": "Any other field is 400 invalid-request naming it, and nothing is changed.",
    "properties": {
     "window": {
      "type": "string"
     },
     "attempts": {
      "type": "integer",
      "minimum": 1
     },
     "cooldown": {
      "type": "string"
     },
     "revealOnce": {
      "type": "boolean",
      "const": true
     },
     "opensAt": {
      "type": "string",
      "format": "date-time"
     },
     "ttl": {
      "type": "string",
      "description": "An alias of window (one field; window is read when both are sent): P<n>D. The rule is the one on PATCH /secrets/{token}/policy"
     }
    }
   },
   "CreateBatch": {
    "type": "object",
    "required": [
     "items"
    ],
    "properties": {
     "label": {
      "type": "string",
      "maxLength": 140
     },
     "items": {
      "type": "array",
      "minItems": 1,
      "maxItems": 5000,
      "items": {
       "type": "object",
       "required": [
        "value",
        "audience"
       ],
       "properties": {
        "label": {
         "type": "string",
         "maxLength": 64
        },
        "value": {
         "type": "string"
        },
        "ttl": {
         "type": "string"
        },
        "reveals": {
         "type": "integer",
         "minimum": 1,
         "default": 1
        },
        "audience": {
         "$ref": "#/components/schemas/Audience"
        },
        "purpose": {
         "type": "string",
         "maxLength": 140
        }
       }
      }
     }
    }
   },
   "BatchCreated": {
    "type": "object",
    "required": [
     "batchId",
     "items",
     "receipt"
    ],
    "properties": {
     "batchId": {
      "type": "string"
     },
     "items": {
      "type": "array",
      "items": {
       "type": "object",
       "properties": {
        "label": {
         "type": "string"
        },
        "token": {
         "type": "string"
        },
        "url": {
         "type": "string",
         "format": "uri"
        },
        "expiresAt": {
         "type": "string",
         "format": "date-time"
        },
        "digest": {
         "type": "string"
        }
       }
      }
     },
     "receipt": {
      "type": "string",
      "description": "Manifest JWS over the ordered list of token digests and audiences"
     }
    }
   },
   "BatchStatus": {
    "type": "object",
    "properties": {
     "batchId": {
      "type": "string"
     },
     "label": {
      "type": "string"
     },
     "counts": {
      "type": "object",
      "properties": {
       "total": {
        "type": "integer"
       },
       "revealed": {
        "type": "integer"
       },
       "pending": {
        "type": "integer"
       },
       "expiredUnrevealed": {
        "type": "integer"
       },
       "revoked": {
        "type": "integer"
       },
       "denied": {
        "type": "integer"
       }
      }
     },
     "items": {
      "type": "array",
      "items": {
       "type": "object",
       "properties": {
        "label": {
         "type": "string"
        },
        "token": {
         "type": "string"
        },
        "status": {
         "$ref": "#/components/schemas/SecretStatus"
        }
       }
      }
     },
     "cursor": {
      "type": "string",
      "description": "Present when more items follow; pass it back as the cursor query parameter"
     }
    }
   },
   "CreateRequest": {
    "type": "object",
    "additionalProperties": false,
    "required": [
     "prompt"
    ],
    "properties": {
     "prompt": {
      "type": "string",
      "maxLength": 140,
      "description": "Shown to the person as plain text under your key's name: one line, no link, domain or email address"
     },
     "kind": {
      "type": "string",
      "enum": [
       "otp",
       "password",
       "text"
      ],
      "default": "text",
      "description": "otp: 4 to 12 letters or digits; password: one line, up to 1,024 bytes; text: up to 4 KB"
     },
     "ttl": {
      "type": "string",
      "default": "10m",
      "description": "How long the page stays open, at most 1h"
     },
     "encJwk": {
      "type": "object",
      "required": [
       "kty",
       "crv",
       "x"
      ],
      "properties": {
       "kty": {
        "const": "OKP"
       },
       "crv": {
        "const": "X25519"
       },
       "x": {
        "type": "string",
        "pattern": "^[A-Za-z0-9_-]{43}$"
       }
      },
      "description": "Your X25519 public key. With it the page encrypts the answer in the person's browser to this key (hpke.v1, /docs/client-encryption.md) and ckrets stores ciphertext it holds no key for; reveal the answer with the private key (the helper: CKRETS_ENC_KEY). Without it ckrets seals the answer and could read it. A key with clientEncryptionRequired must send it (400 plaintext-refused otherwise)"
     }
    }
   },
   "RequestCreated": {
    "type": "object",
    "required": [
     "requestId",
     "url",
     "manageToken",
     "expiresAt",
     "state",
     "kind",
     "receipt"
    ],
    "properties": {
     "requestId": {
      "type": "string"
     },
     "url": {
      "type": "string",
      "format": "uri",
      "description": "For the person: /q/<requestId>"
     },
     "manageToken": {
      "type": "string",
      "description": "Reads and cancels this request, like the asking key"
     },
     "expiresAt": {
      "type": "string",
      "format": "date-time"
     },
     "state": {
      "$ref": "#/components/schemas/RequestState"
     },
     "kind": {
      "type": "string"
     },
     "answerEncryption": {
      "type": "string",
      "x-extensible-enum": true,
      "enum": [
       "client",
       "server"
      ],
      "description": "client: the page will encrypt the answer to your encJwk; server: ckrets will seal it"
     },
     "receipt": {
      "type": "string",
      "description": "Compact JWS (typ request); its ce is true when the answer will be encrypted in the browser"
     }
    }
   },
   "RequestState": {
    "type": "string",
    "x-extensible-enum": true,
    "enum": [
     "pending",
     "fulfilled",
     "expired",
     "frozen"
    ],
    "description": "pending: waiting for the person; fulfilled: answered (token present); expired: the ttl passed, or cancelled, or closed after three answers that did not fit; frozen: reported, held for review"
   },
   "RequestStatus": {
    "type": "object",
    "required": [
     "requestId",
     "state",
     "kind",
     "prompt",
     "createdAt",
     "expiresAt"
    ],
    "properties": {
     "requestId": {
      "type": "string"
     },
     "state": {
      "$ref": "#/components/schemas/RequestState"
     },
     "kind": {
      "type": "string"
     },
     "prompt": {
      "type": "string"
     },
     "answerEncryption": {
      "type": "string",
      "x-extensible-enum": true,
      "enum": [
       "client",
       "server"
      ]
     },
     "createdAt": {
      "type": "string",
      "format": "date-time"
     },
     "expiresAt": {
      "type": "string",
      "format": "date-time"
     },
     "fulfilledAt": {
      "type": "string",
      "format": "date-time"
     },
     "answeredBy": {
      "type": "string",
      "description": "Who answered, as far as ckrets knows: 'a person on the request page' in v1, never an address"
     },
     "token": {
      "type": "string",
      "description": "When fulfilled: the secret, addressed to the asking key, one reveal, an hour"
     },
     "receiptId": {
      "type": "string",
      "description": "When fulfilled: the jti of that secret's create receipt (the reveal's receipt follows)"
     }
    }
   },
   "RequestPublic": {
    "type": "object",
    "properties": {
     "requestId": {
      "type": "string"
     },
     "state": {
      "$ref": "#/components/schemas/RequestState"
     },
     "kind": {
      "type": "string"
     },
     "prompt": {
      "type": "string"
     },
     "sender": {
      "type": "object",
      "properties": {
       "kind": {
        "type": "string"
       },
       "id": {
        "type": "string"
       },
       "label": {
        "type": "string"
       },
       "domain": {
        "type": "object",
        "properties": {
         "name": {
          "type": "string"
         },
         "verified": {
          "type": "boolean"
         }
        }
       }
      }
     },
     "expiresAt": {
      "type": "string",
      "format": "date-time"
     },
     "maxLength": {
      "type": "integer"
     },
     "encJwk": {
      "type": "object",
      "description": "Present when the asker sent one: the page seals the answer to it before sending"
     }
    }
   },
   "Lease": {
    "type": "object",
    "properties": {
     "kind": {
      "type": "string",
      "x-extensible-enum": true,
      "enum": [
       "lease",
       "long-lived"
      ]
     },
     "issuedAt": {
      "type": "string",
      "format": "date-time"
     },
     "windowEndsAt": {
      "type": "string",
      "format": "date-time",
      "description": "Moves to 24 h after any call made with the key (at most once a minute), never past hardEndsAt; past it the key is 401 everywhere"
     },
     "hardEndsAt": {
      "type": "string",
      "format": "date-time",
      "description": "Seven days from minting for a lease"
     },
     "scope": {
      "type": "array",
      "items": {
       "$ref": "#/components/schemas/Scope"
      }
     },
     "parent": {
      "type": "string",
      "description": "The parent lease id when delegated"
     },
     "sponsor": {
      "type": "string",
      "description": "The human the agent acts for, when known"
     }
    }
   },
   "KeyInfo": {
    "type": "object",
    "properties": {
     "keyId": {
      "type": "string"
     },
     "tier": {
      "$ref": "#/components/schemas/Tier"
     },
     "label": {
      "type": "string"
     },
     "domain": {
      "type": "object",
      "properties": {
       "name": {
        "type": "string"
       },
       "verified": {
        "type": "boolean"
       }
      }
     },
     "lease": {
      "$ref": "#/components/schemas/Lease"
     },
     "clientEncryptionRequired": {
      "type": "boolean",
      "default": false,
      "description": "Refuse every value this key would make that ckrets seals itself: a create without clientEncrypted, a create through the hosted MCP server and a request are 400 plaintext-refused, nothing made or counted. Off by default; once on it stays on (PATCH /me/key turns it on; to be without it, mint a new key)"
     },
     "allowRevealInContext": {
      "type": "boolean",
      "default": false,
      "description": "Reserved: the remote MCP server has no reveal in v1, so this has no effect; the stdio server's switch is CKRETS_ALLOW_REVEAL_IN_CONTEXT"
     },
     "webhooks": {
      "type": "array",
      "items": {
       "type": "object",
       "properties": {
        "webhookId": {
         "type": "string"
        },
        "url": {
         "type": "string"
        },
        "events": {
         "type": "array",
         "items": {
          "type": "string"
         }
        }
       }
      }
     },
     "limits": {
      "$ref": "#/components/schemas/TierLimits"
     }
    }
   },
   "TierLimits": {
    "type": "object",
    "properties": {
     "valueBytes": {
      "type": "integer"
     },
     "ttlDefault": {
      "type": "string"
     },
     "ttlMax": {
      "type": "string"
     },
     "reveals": {
      "type": "integer",
      "description": "The default reveals when a create names none (1)"
     },
     "revealsMax": {
      "type": "integer",
      "description": "The most reveals a create may ask for on this tier (1 anonymous, 10 with a key); more is 400 invalid-request"
     },
     "createsPerHour": {
      "type": "integer"
     },
     "createsPerDay": {
      "type": "integer"
     },
     "netCreatesPerHour": {
      "type": "integer",
      "description": "anonymous: creates an hour per network (IPv4 /24, IPv6 /48), four times one address's"
     },
     "netCreatesPerDay": {
      "type": "integer",
      "description": "anonymous: creates a day per network (IPv4 /24, IPv6 /48)"
     },
     "mintsPerHour": {
      "type": "integer",
      "description": "anonymous: free keys minted an hour per address (POST /v1/keys)"
     },
     "mintsPerDay": {
      "type": "integer",
      "description": "anonymous: free keys minted a day per address"
     },
     "mintsPerNetworkDay": {
      "type": "integer",
      "description": "anonymous: free keys minted a day per network (IPv4 /24, IPv6 /48)"
     },
     "revealAttempts": {
      "type": "integer"
     },
     "batchMax": {
      "type": "integer"
     },
     "requests": {
      "type": "boolean"
     },
     "webhooks": {
      "type": "boolean"
     }
    }
   },
   "Limits": {
    "type": "object",
    "required": [
     "tier",
     "limits",
     "remaining",
     "resetsAt"
    ],
    "properties": {
     "app": {
      "type": "string",
      "description": "api (the agent API) or a first-party app; tiers are rows per (app, tier)"
     },
     "stage": {
      "type": "string",
      "x-extensible-enum": true,
      "enum": [
       "prod",
       "staging",
       "lab",
       "offline"
      ],
      "description": "Which deployment answered: prod is https://ckrets.ai; any other value is a test deployment whose data and limits are not production's"
     },
     "tier": {
      "$ref": "#/components/schemas/Tier"
     },
     "limits": {
      "$ref": "#/components/schemas/TierLimits"
     },
     "remaining": {
      "type": "object",
      "additionalProperties": {
       "type": "integer"
      },
      "description": "Read live, never cached. Anonymous: what you can still create, the tighter of your address's and your network's counters (createsThisHour, createsToday). Keyed: the key's or CI repository's createsToday"
     },
     "resetsAt": {
      "type": "object",
      "additionalProperties": {
       "type": "string",
       "format": "date-time"
      }
     },
     "lease": {
      "$ref": "#/components/schemas/Lease"
     },
     "clientEncryptionRequired": {
      "type": "boolean",
      "description": "With a key: whether it requires client encryption (PATCH /me/key)"
     },
     "stamp": {
      "type": "object",
      "description": "Proof-of-work parameters for the anonymous tier (HTTP API only): find counter with sha256(nonce + ':' + counter) having `difficulty` leading zero bits; send Ckrets-Proof: stamp=<nonce>:<counter> before expiresAt",
      "properties": {
       "alg": {
        "type": "string",
        "enum": [
         "sha256"
        ]
       },
       "difficulty": {
        "type": "integer"
       },
       "nonce": {
        "type": "string"
       },
       "expiresAt": {
        "type": "string",
        "format": "date-time"
       }
      }
     }
    }
   },
   "ReceiptClaims": {
    "type": "object",
    "description": "Payload of every receipt and webhook (compact JWS, ES256, kid in /.well-known/jwks.json). Verify offline; the service need not be reachable.",
    "required": [
     "iss",
     "iat",
     "jti",
     "typ",
     "ver",
     "tok"
    ],
    "properties": {
     "iss": {
      "type": "string",
      "description": "The issuing environment's origin: https://ckrets.ai in production, the staging or lab host elsewhere. Reject receipts whose iss is not the one you expect"
     },
     "iat": {
      "type": "integer"
     },
     "jti": {
      "type": "string"
     },
     "typ": {
      "type": "string",
      "x-extensible-enum": true,
      "enum": [
       "create",
       "reveal",
       "deny",
       "revoke",
       "batch",
       "request",
       "webhook",
       "adopt",
       "policy"
      ]
     },
     "ver": {
      "type": "integer",
      "const": 1,
      "description": "The receipt format version; a verifier checks it before reading claims"
     },
     "tok": {
      "type": "string",
      "description": "sha256: + lowercase hex SHA-256 of the whole token string, ck_ included (never the token). ck_SRG73W7Q8R5T6V7W8X9Y0Z1A2B gives sha256:0a3af2067d501ccf12765aee0849380f2bf563350aacfcf438031ae1c4271321"
     },
     "dig": {
      "type": "string",
      "description": "The keyed digest (hmac-sha256:…), as returned with the secret; recompute it with the value and digestKey"
     },
     "pol": {
      "type": "string",
      "description": "sha256 of the policy (ttl, reveals, audience, passphrase present)"
     },
     "aud": {
      "$ref": "#/components/schemas/Audience"
     },
     "cre": {
      "type": "string",
      "description": "Creator: a key id, a signing key thumbprint, an OIDC issuer and subject, or 'anonymous'"
     },
     "spo": {
      "type": "string",
      "description": "Reserved for the sponsoring human; not issued in v1 (a v1 lease cannot name one)"
     },
     "rcp": {
      "type": "string",
      "description": "On reveal, who revealed (key id, OIDC sub, httpsig keyid)"
     },
     "pur": {
      "type": "string",
      "description": "The stated purpose"
     },
     "exp": {
      "type": "integer",
      "description": "The secret's expiry"
     },
     "siz": {
      "type": "integer",
      "description": "Value size in bytes"
     },
     "ce": {
      "type": "boolean",
      "description": "On every receipt: true when the value arrived client-encrypted (ckrets stored ciphertext it holds no key for), false when ckrets sealed it and could read it. On a request receipt: how the answer will be stored (false today). Matches the create's Ckrets-Encryption header. Receipts signed before 2026-11-06 may lack it"
     },
     "app": {
      "type": "string",
      "description": "The app the event belongs to (api, puzzlebox); every ledger event and access-record row carries it too"
     }
    }
   },
   "DailyCount": {
    "type": "object",
    "required": [
     "day",
     "created",
     "opened",
     "byMode"
    ],
    "properties": {
     "day": {
      "type": "string",
      "format": "date"
     },
     "created": {
      "type": "integer"
     },
     "opened": {
      "type": "integer"
     },
     "byMode": {
      "type": "object",
      "additionalProperties": {
       "type": "integer"
      },
      "description": "Creates per mode (riddle, game, dated)"
     }
    }
   },
   "Problem": {
    "type": "object",
    "required": [
     "type",
     "title",
     "status"
    ],
    "properties": {
     "type": {
      "type": "string",
      "format": "uri",
      "description": "One of https://ckrets.ai/problems/{rate-limited, quota-exhausted, tier-budget-exhausted, key-unavailable, wrong-host, version-unknown, value-too-large, ttl-too-long, token-gone, token-frozen, passphrase-required, attempts-exhausted, proof-of-work-required, audience-mismatch, webhook-unverified, domain-unverified, batch-too-large, not-yet-open, ticket-invalid, verdict-invalid, policy-widening, proof-required, session-invalid, hint-out-of-range, void-not-allowed, audience-type-unknown, feature-sunset, temporarily-unavailable, idempotency-mismatch, idempotency-key-retired, idempotency-in-progress, audience-invalid, plaintext-refused, unsupported-media-type, internal, invalid-credentials, invalid-request, not-creator, not-found, scope-missing, not-in-v1}"
     },
     "title": {
      "type": "string",
      "description": "For people: it varies with the cause (several titles share one type). Branch on type, which is stable"
     },
     "status": {
      "type": "integer"
     },
     "detail": {
      "type": "string"
     },
     "requestId": {
      "type": "string",
      "description": "The request's id at the door; quote it in a report or a support request"
     },
     "limit": {
      "type": "string",
      "description": "Which limit, for rate-limited and quota-exhausted"
     },
     "retryable": {
      "type": "boolean",
      "description": "On every 429 and 503, and on the two Idempotency-Key 409s: true for rate-limited, temporarily-unavailable, key-unavailable and idempotency-in-progress (all carry Retry-After); false for quota-exhausted, tier-budget-exhausted (its Retry-After is the reopening time), temporarily-unavailable with outcome unknown and idempotency-key-retired. Absent on every other answer, which is final for that request"
     },
     "outcome": {
      "type": "string",
      "x-extensible-enum": true,
      "enum": [
       "unknown"
      ],
      "description": "On temporarily-unavailable: present when a write timed out and may have happened; read `check` before sending it again (absent: nothing was done)"
     },
     "check": {
      "type": "string",
      "description": "With outcome unknown: the call that says what happened (e.g. GET /v1/secrets/{token}/public), or none when there is nothing to check (a create: create again)"
     },
     "replayed": {
      "type": "boolean",
      "description": "Puzzle reveals: the same commitment as the last counted claim, sent again (in the cooldown or after it); the first answer is repeated and nothing is spent"
     },
     "resetsAt": {
      "type": "string",
      "format": "date-time"
     },
     "retryAfter": {
      "type": "integer"
     },
     "stamp": {
      "type": "object",
      "description": "On proof-of-work-required: alg, difficulty, nonce, expiresAt (as in GET /limits)"
     },
     "receipt": {
      "type": "string",
      "description": "On a denial: the deny receipt (typ deny). A 401 wrong passphrase is counted against the token's reveal attempts; a 403 audience-mismatch is recorded (denials, the alarm) but spends no attempt"
     },
     "result": {
      "type": "string",
      "x-extensible-enum": true,
      "enum": [
       "wrong"
      ],
      "description": "Puzzle reveals: the proof did not open the box (the attempt was counted)"
     },
     "code": {
      "$ref": "#/components/schemas/VerdictCode"
     },
     "attempt": {
      "type": "integer",
      "description": "Puzzle reveals: which attempt this was"
     },
     "attemptsLeft": {
      "type": "integer"
     },
     "cooldownMs": {
      "type": "integer",
      "description": "Milliseconds until the next claim may be made"
     },
     "hintCount": {
      "type": "integer",
      "description": "On hint-out-of-range: how many hints the box has"
     }
    }
   }
  },
  "responses": {
   "NotInV1": {
    "description": "A planned path (x-ckrets-stage: planned): answers 501 not-in-v1 until it ships",
    "content": {
     "application/problem+json": {
      "schema": {
       "$ref": "#/components/schemas/Problem"
      }
     }
    }
   },
   "Problem": {
    "description": "Problem details",
    "content": {
     "application/problem+json": {
      "schema": {
       "$ref": "#/components/schemas/Problem"
      }
     }
    }
   },
   "RateLimited": {
    "description": "Slow down (rate-limited, with Retry-After) or stop until reset (quota-exhausted, no Retry-After, retryable false)",
    "headers": {
     "Retry-After": {
      "schema": {
       "type": "integer"
      },
      "description": "Present only for rate-limited"
     },
     "RateLimit-Reset": {
      "$ref": "#/components/headers/RateLimit-Reset"
     }
    },
    "content": {
     "application/problem+json": {
      "schema": {
       "$ref": "#/components/schemas/Problem"
      },
      "examples": {
       "slowDown": {
        "value": {
         "type": "https://ckrets.ai/problems/rate-limited",
         "title": "Wait before the next attempt",
         "status": 429,
         "limit": "revealAttempts",
         "retryable": true,
         "retryAfter": 2
        }
       },
       "stop": {
        "value": {
         "type": "https://ckrets.ai/problems/quota-exhausted",
         "title": "No creates left today",
         "status": 429,
         "limit": "createsPerDay",
         "retryable": false,
         "resetsAt": "2026-10-25T00:00:00Z",
         "detail": "Mint a free key at POST /v1/keys to continue now."
        }
       }
      }
     }
    }
   },
   "TierBudget": {
    "description": "type tier-budget-exhausted: the anonymous tier's daily budget is spent; retryable false, and Retry-After is the seconds until it reopens at 00:00 UTC (do not retry sooner; a key works now). Keyed tiers never see this",
    "headers": {
     "Retry-After": {
      "schema": {
       "type": "integer"
      }
     }
    },
    "content": {
     "application/problem+json": {
      "schema": {
       "$ref": "#/components/schemas/Problem"
      },
      "example": {
       "type": "https://ckrets.ai/problems/tier-budget-exhausted",
       "title": "Anonymous tier paused until 00:00 UTC",
       "status": 503,
       "retryable": false,
       "retryAfter": 5400,
       "detail": "Mint a free key at POST /v1/keys to continue now."
      }
     }
    }
   }
  }
 }
}