{
  "openapi": "3.1.0",
  "info": {
    "title": "PrefsDB API",
    "version": "1",
    "summary": "Key/value, list and file storage for web apps, with no accounts or sign-up.",
    "description": "Store values, lists and files for a web app without user accounts. Plain HTTP, JSON out.\n\n**Getting a key.** Either call `getkey` with a secret `seed` and a `name` (from a browser, the site's `Origin` keeps keys apart; from a server or script, add a long random `namespace`), or create a key in the dashboard (https://prefsdb.com/dashboard/). Send the key with every other call as `X-Authorization: Token <key>` or `Authorization: Bearer <key>`.\n\n**Restricted keys.** A dashboard key can be made read-only (`403 KEY_READ_ONLY` on any change), limited to a scope (every request must name exactly its `project`, and `domain`/`subdomain` if set: `403 SCOPE_NOT_ALLOWED` otherwise) or set to expire (`403 KEY_EXPIRED`). Use the narrowest key that does the job, especially for an AI agent.\n\n**No key yet? (agents)** Call `POST /pair/start/` with a `name`, show the user the returned code and address, and poll `POST /pair/poll/` until they approve: the response carries the key once. The key is limited to project `agent-state` unless the user changes it.\n\n**Sessions.** A key from `getkey`/`createkey` also needs the session that call returned: send its `session.token` as an `X-Prefs-Session` header on every call, and call `getkey` again when it expires (60 seconds by default; `offline=1` adds `session.offlineToken`, 8 hours, for read, write, delete and undo, sent as `X-Prefs-Offline-Session`). The same tokens are also set as cookies, which only a page on prefsdb.com or a cookie jar sends. Dashboard keys need no session.\n\n**Key names are query parameters.** `read/?theme=` reads the key `theme`; `write/?theme=` with the value as a `text/plain` POST body writes it. So these words can't be key names (`400 RESERVED_KEY_NAME`): domain, subdomain, project, type, name, list, ttl, key, hash, token, keyhash, __map, __filter, __history, __limit, __start, __after, __since, __ifVersion.\n\n**Scope.** `domain`, `subdomain` and `project` are labels of your own that group values; a level you don't send means \"none\", not \"any\" (except in `expire`).\n\n**Errors.** Every failure has `success: false`, a stable `error` code (see ErrorCode; check this, not `message`) and a `message` for people. Don't retry a 4xx unchanged. Every 429 has a `Retry-After` header (seconds, also `retryAfter` in the body): wait that long, then retry. A 500 has a `ref` to quote to support.\n\nAlso published: https://prefsdb.com/llms.txt (a guide for AI agents) and the JavaScript client https://prefsdb.com/prefsdb.v1.js.\n",
    "contact": {
      "url": "https://prefsdb.com/"
    }
  },
  "servers": [
    {
      "url": "https://prefsdb.com/api/v1"
    }
  ],
  "externalDocs": {
    "description": "Guide for AI agents (llms.txt)",
    "url": "https://prefsdb.com/llms.txt"
  },
  "tags": [
    {
      "name": "channels",
      "description": "Broadcast messages between keys."
    },
    {
      "name": "pairing",
      "description": "How an agent with no key gets one, with a human's approval."
    }
  ],
  "paths": {
    "/getkey/": {
      "get": {
        "operationId": "getKey",
        "summary": "Get (or create) the API key for a seed and name",
        "description": "Returns the key for `seed` + `name` + the calling site (the `Origin` header, else `namespace`/`appcred`), creating it if there is none. Calling again returns the same key. Returns the session (`session.token`) that calls with this key need for the next 60 seconds, sent as `X-Prefs-Session`. Needs HTTPS. Limited per IP address (`RATE_LIMITED`).",
        "parameters": [
          {
            "$ref": "#/components/parameters/Seed"
          },
          {
            "$ref": "#/components/parameters/Name"
          },
          {
            "$ref": "#/components/parameters/Email"
          },
          {
            "$ref": "#/components/parameters/Namespace"
          },
          {
            "$ref": "#/components/parameters/Appcred"
          },
          {
            "$ref": "#/components/parameters/Offline"
          },
          {
            "$ref": "#/components/parameters/OfflineTtl"
          },
          {
            "$ref": "#/components/parameters/SessionTtl"
          },
          {
            "$ref": "#/components/parameters/Renew"
          },
          {
            "$ref": "#/components/parameters/Remember"
          },
          {
            "$ref": "#/components/parameters/RememberDays"
          },
          {
            "$ref": "#/components/parameters/Device"
          }
        ],
        "responses": {
          "200": {
            "description": "The key.",
            "headers": {
              "Set-Cookie": {
                "description": "`prefs_token` (HttpOnly, Secure, SameSite=Strict), the same session as `session.token`, for pages on prefsdb.com and cookie jars; with `offline`, also `prefs_offline_token`.",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/KeyResponse"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Error"
          },
          "401": {
            "$ref": "#/components/responses/Error"
          },
          "403": {
            "$ref": "#/components/responses/Error"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          },
          "500": {
            "$ref": "#/components/responses/Error"
          }
        },
        "security": []
      },
      "post": {
        "operationId": "getKeyWithSignIn",
        "summary": "Get (or create) a key by signing in with Google, Microsoft, Apple or GitHub",
        "description": "Trades a sign-in for the key that belongs to that account, this site and `name`. Google, Microsoft and Apple: an ID token containing a nonce from `GET /getkey/nonce/`. GitHub: the one-time `signin_code` from `PrefsDB.signInWithGitHub()` with its `code_verifier`. Everything goes in the form body, never the URL. The same seed-form fields (`seed`, `name`, ...) are also accepted here.",
        "parameters": [],
        "requestBody": {
          "required": true,
          "content": {
            "application/x-www-form-urlencoded": {
              "schema": {
                "type": "object",
                "properties": {
                  "id_token": {
                    "type": "string",
                    "description": "The provider's ID token (Google, Microsoft, Apple)."
                  },
                  "provider": {
                    "type": "string",
                    "enum": [
                      "google",
                      "microsoft",
                      "apple",
                      "github"
                    ],
                    "default": "google"
                  },
                  "signin_code": {
                    "type": "string",
                    "description": "GitHub only: the code from the sign-in popup."
                  },
                  "code_verifier": {
                    "type": "string",
                    "description": "GitHub only: the PKCE verifier the page kept."
                  },
                  "name": {
                    "type": "string",
                    "default": "default",
                    "description": "Which of the account's keys (one per app)."
                  },
                  "offline": {
                    "type": "string"
                  },
                  "offlinettl": {
                    "type": "integer"
                  },
                  "sessionttl": {
                    "type": "integer"
                  },
                  "remember": {
                    "type": "string",
                    "enum": [
                      "1"
                    ],
                    "description": "As the GET parameter: also return a `deviceGrant`."
                  },
                  "rememberdays": {
                    "type": "integer"
                  },
                  "device": {
                    "type": "string"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The key.",
            "headers": {
              "Set-Cookie": {
                "description": "`prefs_token` (HttpOnly, Secure, SameSite=Strict), the same session as `session.token`, for pages on prefsdb.com and cookie jars; with `offline`, also `prefs_offline_token`.",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/KeyResponse"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Error"
          },
          "401": {
            "$ref": "#/components/responses/Error"
          },
          "403": {
            "$ref": "#/components/responses/Error"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          },
          "500": {
            "$ref": "#/components/responses/Error"
          },
          "503": {
            "$ref": "#/components/responses/Error"
          }
        },
        "security": []
      }
    },
    "/getkey/nonce/": {
      "get": {
        "operationId": "getSignInNonce",
        "summary": "A nonce for a sign-in",
        "description": "One sign-in, valid 10 minutes. Put it in the provider's sign-in request.",
        "parameters": [],
        "responses": {
          "200": {
            "description": "Success.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/NonceResponse"
                }
              }
            }
          },
          "405": {
            "$ref": "#/components/responses/Error"
          }
        },
        "security": []
      }
    },
    "/createkey/": {
      "get": {
        "operationId": "createKey",
        "summary": "Create a new API key for a seed and name",
        "description": "Like getkey, but refuses (`403 KEY_EXISTS`) if the seed and name already have a key on this site.",
        "parameters": [
          {
            "$ref": "#/components/parameters/Seed"
          },
          {
            "$ref": "#/components/parameters/Name"
          },
          {
            "$ref": "#/components/parameters/Email"
          },
          {
            "$ref": "#/components/parameters/Namespace"
          },
          {
            "$ref": "#/components/parameters/Appcred"
          },
          {
            "$ref": "#/components/parameters/Offline"
          },
          {
            "$ref": "#/components/parameters/OfflineTtl"
          },
          {
            "$ref": "#/components/parameters/SessionTtl"
          },
          {
            "$ref": "#/components/parameters/Remember"
          },
          {
            "$ref": "#/components/parameters/RememberDays"
          },
          {
            "$ref": "#/components/parameters/Device"
          }
        ],
        "responses": {
          "200": {
            "description": "The new key.",
            "headers": {
              "Set-Cookie": {
                "description": "`prefs_token` (HttpOnly, Secure, SameSite=Strict), the same session as `session.token`, for pages on prefsdb.com and cookie jars; with `offline`, also `prefs_offline_token`.",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/KeyResponse"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Error"
          },
          "403": {
            "$ref": "#/components/responses/Error"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          },
          "500": {
            "$ref": "#/components/responses/Error"
          }
        },
        "security": []
      }
    },
    "/rotatekey/": {
      "get": {
        "operationId": "rotateKey",
        "summary": "Replace the API key, keeping its data",
        "description": "Makes a new key (from the new `seed` and `name`; a key from a sign-in keeps its identity) and moves all data and files to it. The key sent stops working at once. A key from getkey needs a fresh session (the last 60 seconds).",
        "parameters": [
          {
            "name": "seed",
            "in": "query",
            "required": false,
            "description": "The new seed. Not for a key from a sign-in.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "name",
            "in": "query",
            "required": false,
            "description": "The new name. Not for a key from a sign-in.",
            "schema": {
              "type": "string"
            }
          },
          {
            "$ref": "#/components/parameters/Namespace"
          },
          {
            "$ref": "#/components/parameters/Appcred"
          }
        ],
        "responses": {
          "200": {
            "description": "Success.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RotateKeyResponse"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Error"
          },
          "401": {
            "$ref": "#/components/responses/Error"
          },
          "403": {
            "$ref": "#/components/responses/Error"
          },
          "409": {
            "$ref": "#/components/responses/Error"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          },
          "500": {
            "$ref": "#/components/responses/Error"
          }
        },
        "security": [
          {
            "XAuthorization": [],
            "Session": []
          },
          {
            "Bearer": [],
            "Session": []
          },
          {
            "XAuthorization": []
          },
          {
            "Bearer": []
          }
        ]
      }
    },
    "/migrateorigin/": {
      "get": {
        "operationId": "migrateOrigin",
        "summary": "Move a key to another site, namespace or app credential",
        "description": "Exactly one of `origin`, `namespace`, `appcred`. Afterwards getkey finds the key (with its data) from there, e.g. after a domain move. Not for keys from a sign-in (`409 IDTOKEN_KEY`).",
        "parameters": [
          {
            "name": "origin",
            "in": "query",
            "required": false,
            "description": "The site to bind the key to: `scheme://host[:port]`, no path.",
            "schema": {
              "type": "string"
            }
          },
          {
            "$ref": "#/components/parameters/Namespace"
          },
          {
            "$ref": "#/components/parameters/Appcred"
          }
        ],
        "responses": {
          "200": {
            "description": "Success.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MigrateOriginResponse"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Error"
          },
          "401": {
            "$ref": "#/components/responses/Error"
          },
          "403": {
            "$ref": "#/components/responses/Error"
          },
          "405": {
            "$ref": "#/components/responses/Error"
          },
          "409": {
            "$ref": "#/components/responses/Error"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        },
        "security": [
          {
            "XAuthorization": [],
            "Session": []
          },
          {
            "Bearer": [],
            "Session": []
          },
          {
            "XAuthorization": []
          },
          {
            "Bearer": []
          }
        ]
      }
    },
    "/read/": {
      "get": {
        "operationId": "readValues",
        "summary": "Read values",
        "description": "Reads the named keys (or a list, a map, or everything in a scope with `*`) of this API key, newest first. A key never written is not an error: `value` \"\" and `count` 0. For many key names, POST them as a form instead.",
        "parameters": [
          {
            "$ref": "#/components/parameters/KeyNames"
          },
          {
            "$ref": "#/components/parameters/Domain"
          },
          {
            "$ref": "#/components/parameters/Subdomain"
          },
          {
            "$ref": "#/components/parameters/Project"
          },
          {
            "$ref": "#/components/parameters/Type"
          },
          {
            "$ref": "#/components/parameters/List"
          },
          {
            "$ref": "#/components/parameters/Map"
          },
          {
            "$ref": "#/components/parameters/Filter"
          },
          {
            "$ref": "#/components/parameters/Limit"
          },
          {
            "$ref": "#/components/parameters/After"
          },
          {
            "$ref": "#/components/parameters/Start"
          },
          {
            "$ref": "#/components/parameters/History"
          },
          {
            "$ref": "#/components/parameters/Since"
          }
        ],
        "responses": {
          "200": {
            "description": "Success.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ReadResponse"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Error"
          },
          "401": {
            "$ref": "#/components/responses/Error"
          },
          "403": {
            "$ref": "#/components/responses/Error"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          },
          "410": {
            "$ref": "#/components/responses/Error"
          }
        },
        "security": [
          {
            "XAuthorization": [],
            "Session": []
          },
          {
            "Bearer": [],
            "Session": []
          },
          {
            "XAuthorization": []
          },
          {
            "Bearer": []
          }
        ]
      },
      "post": {
        "operationId": "readValuesForm",
        "summary": "Read values (key names in a form body)",
        "description": "Same as GET, with the key names and the other parameters as form fields, for more names than fit a URL.",
        "parameters": [],
        "requestBody": {
          "required": true,
          "content": {
            "application/x-www-form-urlencoded": {
              "schema": {
                "type": "object",
                "additionalProperties": {
                  "type": "string"
                },
                "description": "Key names with empty values, plus any of the GET parameters."
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Success.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ReadResponse"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Error"
          },
          "401": {
            "$ref": "#/components/responses/Error"
          },
          "403": {
            "$ref": "#/components/responses/Error"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          },
          "410": {
            "$ref": "#/components/responses/Error"
          }
        },
        "security": [
          {
            "XAuthorization": [],
            "Session": []
          },
          {
            "Bearer": [],
            "Session": []
          },
          {
            "XAuthorization": []
          },
          {
            "Bearer": []
          }
        ]
      }
    },
    "/write/": {
      "post": {
        "operationId": "writeValue",
        "summary": "Write one value (the request body)",
        "description": "The body is the value, stored exactly as sent (up to the plan's limit: 1 MB free, 4 MB Plus, 8 MB Pro; at most 8 MB of values per write in total; binary is kept as base64). Name the key as a query parameter with an empty value: `POST /write/?theme=`. Writing a key again overwrites it; in a `list` it appends. With form content types the form fields are key=value pairs instead, like GET.",
        "parameters": [
          {
            "$ref": "#/components/parameters/OneKeyName"
          },
          {
            "$ref": "#/components/parameters/Domain"
          },
          {
            "$ref": "#/components/parameters/Subdomain"
          },
          {
            "$ref": "#/components/parameters/Project"
          },
          {
            "$ref": "#/components/parameters/Type"
          },
          {
            "$ref": "#/components/parameters/List"
          },
          {
            "$ref": "#/components/parameters/Map"
          },
          {
            "$ref": "#/components/parameters/Ttl"
          },
          {
            "$ref": "#/components/parameters/IfVersion"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "text/plain": {
              "schema": {
                "type": "string"
              },
              "example": "dark"
            },
            "application/x-www-form-urlencoded": {
              "schema": {
                "type": "object",
                "additionalProperties": {
                  "type": "string"
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Success.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/WriteResponse"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Error"
          },
          "401": {
            "$ref": "#/components/responses/Error"
          },
          "403": {
            "$ref": "#/components/responses/Error"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          },
          "500": {
            "$ref": "#/components/responses/Error"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          }
        },
        "security": [
          {
            "XAuthorization": [],
            "Session": []
          },
          {
            "Bearer": [],
            "Session": []
          },
          {
            "XAuthorization": []
          },
          {
            "Bearer": []
          }
        ]
      },
      "get": {
        "operationId": "writeValuesQuery",
        "summary": "Write short values in the query string",
        "description": "`?theme=dark&font=mono` writes both. Fine for short, plain values; for anything with `+`, `%`, `&`, line breaks or surrounding spaces, use POST with the value as the body.",
        "parameters": [
          {
            "name": "values",
            "in": "query",
            "required": true,
            "style": "form",
            "explode": true,
            "description": "key=value pairs, each its own query parameter (the name `values` is never sent).",
            "schema": {
              "type": "object",
              "additionalProperties": {
                "type": "string"
              }
            },
            "example": {
              "theme": "dark"
            }
          },
          {
            "$ref": "#/components/parameters/Domain"
          },
          {
            "$ref": "#/components/parameters/Subdomain"
          },
          {
            "$ref": "#/components/parameters/Project"
          },
          {
            "$ref": "#/components/parameters/Type"
          },
          {
            "$ref": "#/components/parameters/List"
          },
          {
            "$ref": "#/components/parameters/Map"
          },
          {
            "$ref": "#/components/parameters/Ttl"
          },
          {
            "$ref": "#/components/parameters/IfVersion"
          }
        ],
        "responses": {
          "200": {
            "description": "Success.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/WriteResponse"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Error"
          },
          "401": {
            "$ref": "#/components/responses/Error"
          },
          "403": {
            "$ref": "#/components/responses/Error"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          },
          "500": {
            "$ref": "#/components/responses/Error"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          }
        },
        "security": [
          {
            "XAuthorization": [],
            "Session": []
          },
          {
            "Bearer": [],
            "Session": []
          },
          {
            "XAuthorization": []
          },
          {
            "Bearer": []
          }
        ]
      }
    },
    "/delete/": {
      "get": {
        "operationId": "deleteValues",
        "summary": "Delete values",
        "description": "Deletes the named keys (or list entries, map entries, or everything in a scope with `*`/`**`) of this API key. Files are never touched (use deleteobject).",
        "parameters": [
          {
            "$ref": "#/components/parameters/KeyNames"
          },
          {
            "$ref": "#/components/parameters/Domain"
          },
          {
            "$ref": "#/components/parameters/Subdomain"
          },
          {
            "$ref": "#/components/parameters/Project"
          },
          {
            "$ref": "#/components/parameters/Type"
          },
          {
            "$ref": "#/components/parameters/List"
          },
          {
            "$ref": "#/components/parameters/Map"
          },
          {
            "$ref": "#/components/parameters/Filter"
          },
          {
            "$ref": "#/components/parameters/IfVersion"
          }
        ],
        "responses": {
          "200": {
            "description": "Success.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DeleteResponse"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Error"
          },
          "401": {
            "$ref": "#/components/responses/Error"
          },
          "403": {
            "$ref": "#/components/responses/Error"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          }
        },
        "security": [
          {
            "XAuthorization": [],
            "Session": []
          },
          {
            "Bearer": [],
            "Session": []
          },
          {
            "XAuthorization": []
          },
          {
            "Bearer": []
          }
        ]
      }
    },
    "/undo/": {
      "get": {
        "operationId": "undoValue",
        "summary": "Restore a key's previous value",
        "description": "Puts back the value before the last write or delete of one key (call again to go further back). Only when value history is on for the server; otherwise `restored` is false. Not for lists.",
        "parameters": [
          {
            "$ref": "#/components/parameters/OneKeyName"
          },
          {
            "$ref": "#/components/parameters/Domain"
          },
          {
            "$ref": "#/components/parameters/Subdomain"
          },
          {
            "$ref": "#/components/parameters/Project"
          },
          {
            "$ref": "#/components/parameters/Type"
          },
          {
            "$ref": "#/components/parameters/Map"
          }
        ],
        "responses": {
          "200": {
            "description": "Success.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/UndoResponse"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Error"
          },
          "401": {
            "$ref": "#/components/responses/Error"
          },
          "403": {
            "$ref": "#/components/responses/Error"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        },
        "security": [
          {
            "XAuthorization": [],
            "Session": []
          },
          {
            "Bearer": [],
            "Session": []
          },
          {
            "XAuthorization": []
          },
          {
            "Bearer": []
          }
        ]
      }
    },
    "/expire/": {
      "get": {
        "operationId": "expireScope",
        "summary": "Set or remove the expiry of everything in a scope",
        "description": "`days` > 0: everything already stored in the scope is deleted after that many days; 0 or less: never expires. At least one of `domain`, `subdomain`, `project`, `list`, `__map`; levels left out match any value.",
        "parameters": [
          {
            "name": "days",
            "in": "query",
            "required": true,
            "description": "Days until expiry; 0 or less removes it.",
            "schema": {
              "type": "integer"
            }
          },
          {
            "$ref": "#/components/parameters/Domain"
          },
          {
            "$ref": "#/components/parameters/Subdomain"
          },
          {
            "$ref": "#/components/parameters/Project"
          },
          {
            "$ref": "#/components/parameters/List"
          },
          {
            "$ref": "#/components/parameters/Map"
          }
        ],
        "responses": {
          "200": {
            "description": "Success.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ExpireResponse"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Error"
          },
          "401": {
            "$ref": "#/components/responses/Error"
          },
          "403": {
            "$ref": "#/components/responses/Error"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        },
        "security": [
          {
            "XAuthorization": [],
            "Session": []
          },
          {
            "Bearer": [],
            "Session": []
          },
          {
            "XAuthorization": []
          },
          {
            "Bearer": []
          }
        ]
      }
    },
    "/putobject/": {
      "post": {
        "operationId": "putObject",
        "summary": "Upload a file",
        "description": "Stores the file (encrypted at rest) in this key's storage. Uploading the same file name to the same scope replaces it (the share links stay). Limits: 12 MB a file, 50 MB a key on free.",
        "parameters": [],
        "requestBody": {
          "required": true,
          "content": {
            "multipart/form-data": {
              "schema": {
                "type": "object",
                "required": [
                  "file"
                ],
                "properties": {
                  "file": {
                    "type": "string",
                    "format": "binary"
                  },
                  "domain": {
                    "type": "string",
                    "pattern": "^[A-Za-z0-9_-]{1,128}$"
                  },
                  "subdomain": {
                    "type": "string",
                    "pattern": "^[A-Za-z0-9_-]{1,128}$"
                  },
                  "project": {
                    "type": "string",
                    "pattern": "^[A-Za-z0-9_-]{1,128}$"
                  },
                  "list": {
                    "type": "string",
                    "pattern": "^[A-Za-z0-9_-]{1,128}$"
                  },
                  "path": {
                    "type": "string",
                    "description": "A folder to show it under in the dashboard."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Success.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PutObjectResponse"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Error"
          },
          "401": {
            "$ref": "#/components/responses/Error"
          },
          "403": {
            "$ref": "#/components/responses/Error"
          },
          "405": {
            "$ref": "#/components/responses/Error"
          },
          "413": {
            "$ref": "#/components/responses/Error"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          },
          "500": {
            "$ref": "#/components/responses/Error"
          }
        },
        "security": [
          {
            "XAuthorization": [],
            "Session": []
          },
          {
            "Bearer": [],
            "Session": []
          },
          {
            "XAuthorization": []
          },
          {
            "Bearer": []
          }
        ]
      }
    },
    "/getobject/": {
      "get": {
        "operationId": "getObject",
        "summary": "Download a file, or get its share links",
        "description": "Returns the file itself (always as an attachment). Pass the scope it was uploaded with. With `url=1`, returns its public share links as JSON instead. A plain `<img>` or `<a>` can't send headers: put the key in a `key` parameter.",
        "parameters": [
          {
            "$ref": "#/components/parameters/FileId"
          },
          {
            "$ref": "#/components/parameters/FileDomain"
          },
          {
            "$ref": "#/components/parameters/FileSubdomain"
          },
          {
            "$ref": "#/components/parameters/FileProject"
          },
          {
            "$ref": "#/components/parameters/FileList"
          },
          {
            "name": "url",
            "in": "query",
            "required": false,
            "description": "`1`: the share links as JSON instead of the file.",
            "schema": {
              "type": "string",
              "enum": [
                "1"
              ]
            }
          },
          {
            "name": "key",
            "in": "query",
            "required": false,
            "description": "The API key, for links that can't send a header.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The file, or with `url=1` its share links.",
            "content": {
              "application/octet-stream": {
                "schema": {
                  "type": "string",
                  "format": "binary"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ShareLinksResponse"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Error"
          },
          "401": {
            "$ref": "#/components/responses/Error"
          },
          "403": {
            "$ref": "#/components/responses/Error"
          },
          "404": {
            "$ref": "#/components/responses/Error"
          },
          "405": {
            "$ref": "#/components/responses/Error"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          },
          "500": {
            "$ref": "#/components/responses/Error"
          }
        },
        "security": [
          {
            "XAuthorization": [],
            "Session": []
          },
          {
            "Bearer": [],
            "Session": []
          },
          {
            "XAuthorization": []
          },
          {
            "Bearer": []
          }
        ]
      }
    },
    "/deleteobject/": {
      "get": {
        "operationId": "deleteObject",
        "summary": "Delete a file",
        "description": "Deletes the file and frees its space. Pass the scope it was uploaded with.",
        "parameters": [
          {
            "$ref": "#/components/parameters/FileId"
          },
          {
            "$ref": "#/components/parameters/FileDomain"
          },
          {
            "$ref": "#/components/parameters/FileSubdomain"
          },
          {
            "$ref": "#/components/parameters/FileProject"
          },
          {
            "$ref": "#/components/parameters/FileList"
          }
        ],
        "responses": {
          "200": {
            "description": "Success.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DeleteObjectResponse"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Error"
          },
          "401": {
            "$ref": "#/components/responses/Error"
          },
          "403": {
            "$ref": "#/components/responses/Error"
          },
          "404": {
            "$ref": "#/components/responses/Error"
          },
          "405": {
            "$ref": "#/components/responses/Error"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        },
        "security": [
          {
            "XAuthorization": [],
            "Session": []
          },
          {
            "Bearer": [],
            "Session": []
          },
          {
            "XAuthorization": []
          },
          {
            "Bearer": []
          }
        ]
      }
    },
    "/channel/publish/": {
      "get": {
        "operationId": "publishMessage",
        "summary": "Publish a message to a channel",
        "description": "Channels are shared by name across all keys: anyone who knows the name can read them. POST with a raw body to send a longer message.",
        "tags": [
          "channels"
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/Channel"
          },
          {
            "name": "message",
            "in": "query",
            "required": false,
            "description": "The message (or POST it as the body).",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Success.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublishResponse"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Error"
          },
          "401": {
            "$ref": "#/components/responses/Error"
          },
          "403": {
            "$ref": "#/components/responses/Error"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        },
        "security": [
          {
            "XAuthorization": [],
            "Session": []
          },
          {
            "Bearer": [],
            "Session": []
          },
          {
            "XAuthorization": []
          },
          {
            "Bearer": []
          }
        ]
      }
    },
    "/channel/poll/": {
      "get": {
        "operationId": "pollChannel",
        "summary": "Read new messages from a channel",
        "description": "Messages after `since`, oldest first. Keep the response's `cursor` and pass it as `since` next time.",
        "tags": [
          "channels"
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/Channel"
          },
          {
            "name": "since",
            "in": "query",
            "required": false,
            "description": "Message id to read after. Default 0.",
            "schema": {
              "type": "integer",
              "minimum": 0
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "At most this many messages, 1 to 200. Default 50.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 200
            }
          },
          {
            "name": "peek",
            "in": "query",
            "required": false,
            "description": "Any value: no messages, just the latest id as `cursor`.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Success.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PollResponse"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Error"
          },
          "401": {
            "$ref": "#/components/responses/Error"
          },
          "403": {
            "$ref": "#/components/responses/Error"
          },
          "405": {
            "$ref": "#/components/responses/Error"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        },
        "security": [
          {
            "XAuthorization": [],
            "Session": []
          },
          {
            "Bearer": [],
            "Session": []
          },
          {
            "XAuthorization": []
          },
          {
            "Bearer": []
          }
        ]
      }
    },
    "/verify/": {
      "get": {
        "operationId": "verifyEmail",
        "summary": "Confirm a new key's email address",
        "description": "What the link in the verification email calls. A browser (Accept: text/html) gets a page instead of JSON.",
        "parameters": [
          {
            "name": "token",
            "in": "query",
            "required": true,
            "description": "The token from the emailed link.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Success.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/VerifyResponse"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Error"
          },
          "500": {
            "$ref": "#/components/responses/Error"
          }
        },
        "security": []
      }
    },
    "/verify/status/": {
      "get": {
        "operationId": "verificationStatus",
        "summary": "Whether a key's email has been confirmed",
        "description": "For an app waiting for its user to click the link in the verification email: poll this every few seconds (the JS client's `waitForVerification()` does). Needs only the key, no session; not counted against the key's quota. `verified` is true when the key can be used, including keys that never needed verifying.",
        "parameters": [],
        "responses": {
          "200": {
            "description": "Success.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/VerificationStatusResponse"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Error"
          },
          "403": {
            "$ref": "#/components/responses/Error"
          },
          "500": {
            "$ref": "#/components/responses/Error"
          }
        },
        "security": [
          {
            "XAuthorization": []
          },
          {
            "Bearer": []
          }
        ]
      }
    },
    "/logout/": {
      "get": {
        "operationId": "logout",
        "summary": "End this browser's session",
        "description": "Clears the session cookies set by getkey/createkey. The key keeps working; calls need a new getkey first.",
        "parameters": [],
        "responses": {
          "200": {
            "description": "Success.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/LogoutResponse"
                }
              }
            }
          },
          "403": {
            "$ref": "#/components/responses/Error"
          }
        },
        "security": []
      }
    },
    "/pair/start/": {
      "post": {
        "operationId": "startPairing",
        "summary": "Ask a human for an API key (agent pairing)",
        "tags": [
          "pairing"
        ],
        "description": "For an agent with no key. Answers a short code: ask the user to open `verification_uri`, sign in to their PrefsDB dashboard and type the code. Then poll `pair/poll/` every `interval` seconds. The user decides what the key may do; it is limited to project `agent-state` unless you ask otherwise. Only works where the server has agent pairing switched on (404 otherwise).",
        "parameters": [],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "name": {
                    "type": "string",
                    "minLength": 1,
                    "maxLength": 60,
                    "description": "What the user sees, e.g. \"Claude Code on build-box-2\". The key is named \"agent: <name>\"."
                  },
                  "access": {
                    "type": "string",
                    "enum": [
                      "read-write",
                      "read-only"
                    ],
                    "default": "read-write"
                  },
                  "project": {
                    "type": "string",
                    "default": "agent-state",
                    "description": "The project the key should be limited to; \"\" for none. The user can change it."
                  },
                  "days": {
                    "type": "integer",
                    "minimum": 1,
                    "maximum": 365,
                    "description": "Ask for the key to expire after this many days. By default it does not expire."
                  }
                },
                "required": [
                  "name"
                ]
              }
            },
            "application/x-www-form-urlencoded": {
              "schema": {
                "type": "object",
                "properties": {
                  "name": {
                    "type": "string",
                    "minLength": 1,
                    "maxLength": 60,
                    "description": "What the user sees, e.g. \"Claude Code on build-box-2\". The key is named \"agent: <name>\"."
                  },
                  "access": {
                    "type": "string",
                    "enum": [
                      "read-write",
                      "read-only"
                    ],
                    "default": "read-write"
                  },
                  "project": {
                    "type": "string",
                    "default": "agent-state",
                    "description": "The project the key should be limited to; \"\" for none. The user can change it."
                  },
                  "days": {
                    "type": "integer",
                    "minimum": 1,
                    "maximum": 365,
                    "description": "Ask for the key to expire after this many days. By default it does not expire."
                  }
                },
                "required": [
                  "name"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Success.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PairStartResponse"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Error"
          },
          "404": {
            "$ref": "#/components/responses/Error"
          },
          "405": {
            "$ref": "#/components/responses/Error"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        },
        "security": []
      }
    },
    "/pair/poll/": {
      "post": {
        "operationId": "pollPairing",
        "summary": "Collect the key once the human approved",
        "tags": [
          "pairing"
        ],
        "description": "Poll with the `device_code` from `pair/start/` every `interval` seconds until `state` is final. `approved` carries the key, exactly once.",
        "parameters": [],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "device_code": {
                    "type": "string"
                  }
                },
                "required": [
                  "device_code"
                ]
              }
            },
            "application/x-www-form-urlencoded": {
              "schema": {
                "type": "object",
                "properties": {
                  "device_code": {
                    "type": "string"
                  }
                },
                "required": [
                  "device_code"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Success.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PairPollResponse"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Error"
          },
          "404": {
            "$ref": "#/components/responses/Error"
          },
          "405": {
            "$ref": "#/components/responses/Error"
          },
          "500": {
            "$ref": "#/components/responses/Error"
          }
        },
        "security": []
      }
    },
    "/pair/self/": {
      "get": {
        "operationId": "describeKey",
        "summary": "What this dashboard or agent key can do",
        "tags": [
          "pairing"
        ],
        "description": "Name, whether it was made by pairing, read-only, scope, expiry and storage, without spending a data request. Dashboard and agent keys only.",
        "parameters": [],
        "responses": {
          "200": {
            "description": "Success.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PairSelfResponse"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Error"
          },
          "403": {
            "$ref": "#/components/responses/Error"
          },
          "404": {
            "$ref": "#/components/responses/Error"
          }
        },
        "security": [
          {
            "XAuthorization": []
          },
          {
            "Bearer": []
          }
        ]
      }
    },
    "/pair/revoke/": {
      "post": {
        "operationId": "revokePairedKey",
        "summary": "Revoke this agent key (log out)",
        "tags": [
          "pairing"
        ],
        "description": "A key made by pairing revokes itself; it stops working at once. Its owner can restore it from the dashboard for a while.",
        "parameters": [],
        "responses": {
          "200": {
            "description": "Success.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PairRevokeResponse"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Error"
          },
          "403": {
            "$ref": "#/components/responses/Error"
          },
          "404": {
            "$ref": "#/components/responses/Error"
          },
          "405": {
            "$ref": "#/components/responses/Error"
          }
        },
        "security": [
          {
            "XAuthorization": []
          },
          {
            "Bearer": []
          }
        ]
      }
    },
    "/getkey/resume/": {
      "post": {
        "operationId": "resumeKey",
        "summary": "Get a remembered key back with its device grant",
        "description": "Trades a `deviceGrant` token (from getkey/createkey/a sign-in with `remember`) for the key and a new session, as getkey would after the seed or a sign-in. The grant stays as it was (same expiry). A revoked, expired or unknown grant is `401 DEVICE_GRANT_INVALID`: sign in again. The grant only in the form body, never the URL. Needs HTTPS.",
        "parameters": [],
        "requestBody": {
          "required": true,
          "content": {
            "application/x-www-form-urlencoded": {
              "schema": {
                "type": "object",
                "required": [
                  "grant"
                ],
                "properties": {
                  "grant": {
                    "type": "string"
                  },
                  "offline": {
                    "type": "string"
                  },
                  "offlinettl": {
                    "type": "integer"
                  },
                  "sessionttl": {
                    "type": "integer"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ResumeResponse"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Error"
          },
          "401": {
            "$ref": "#/components/responses/Error"
          },
          "403": {
            "$ref": "#/components/responses/Error"
          },
          "405": {
            "$ref": "#/components/responses/Error"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        },
        "security": []
      }
    },
    "/devices/": {
      "get": {
        "operationId": "listDevices",
        "summary": "The devices a key is remembered on",
        "description": "The key's active device grants (see getkey's `remember`), newest first. With the key (and its session), or from its review page. Send this device's grant as `X-Prefs-Device` to have it marked `current`.",
        "parameters": [
          {
            "name": "action",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "enum": [
                "list"
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The devices.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DevicesResponse"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Error"
          },
          "401": {
            "$ref": "#/components/responses/Error"
          },
          "403": {
            "$ref": "#/components/responses/Error"
          }
        },
        "security": [
          {
            "XAuthorization": [],
            "Session": []
          },
          {
            "Bearer": [],
            "Session": []
          },
          {
            "XAuthorization": []
          },
          {
            "Bearer": []
          }
        ]
      },
      "post": {
        "operationId": "revokeDevices",
        "summary": "Sign a key out on remembered devices",
        "description": "`action=revoke` with `id` (from the list), or `revoke_all`: those grants stop working at once (their current session runs out by itself). `revoke_current` needs only this device's grant as `X-Prefs-Device`, no key: signing out.",
        "parameters": [],
        "requestBody": {
          "required": true,
          "content": {
            "application/x-www-form-urlencoded": {
              "schema": {
                "type": "object",
                "required": [
                  "action"
                ],
                "properties": {
                  "action": {
                    "type": "string",
                    "enum": [
                      "revoke",
                      "revoke_all",
                      "revoke_current"
                    ]
                  },
                  "id": {
                    "type": "integer"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Done.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DevicesRevokeResponse"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Error"
          },
          "401": {
            "$ref": "#/components/responses/Error"
          },
          "403": {
            "$ref": "#/components/responses/Error"
          },
          "405": {
            "$ref": "#/components/responses/Error"
          }
        },
        "security": [
          {
            "XAuthorization": [],
            "Session": []
          },
          {
            "Bearer": [],
            "Session": []
          },
          {
            "XAuthorization": []
          },
          {
            "Bearer": []
          },
          {}
        ]
      }
    }
  },
  "components": {
    "securitySchemes": {
      "XAuthorization": {
        "type": "apiKey",
        "in": "header",
        "name": "X-Authorization",
        "description": "The API key as `Token <key>` (what the JS client sends). `Bearer <key>` works here too."
      },
      "Bearer": {
        "type": "http",
        "scheme": "bearer",
        "description": "`Authorization: Bearer <key>`."
      },
      "Session": {
        "type": "apiKey",
        "in": "header",
        "name": "X-Prefs-Session",
        "description": "Keys from getkey/createkey only (not dashboard keys): `session.token` from the getkey response, valid 60 seconds by default (`sessionttl`); for read/write/delete/undo/expire/channels, `session.offlineToken` in `X-Prefs-Offline-Session` also works. The `prefs_token`/`prefs_offline_token` cookies are accepted instead, but browsers only send them to a page on prefsdb.com itself. Missing: `401 SESSION_REQUIRED`; expired: `401 SESSION_EXPIRED`; call getkey again."
      }
    },
    "parameters": {
      "KeyNames": {
        "name": "keys",
        "in": "query",
        "required": false,
        "style": "form",
        "explode": true,
        "description": "Key names, each sent as its own query parameter with an empty value: `?theme=&font=` names the keys `theme` and `font`. (The parameter name `keys` itself is never sent; this is OpenAPI's way of describing free-form parameter names.) `*` matches the unnamed key, or with a scope everything in it; `**` matches every key in scope. A reserved word (see `RESERVED_KEY_NAME`) can't be a key name.",
        "schema": {
          "type": "object",
          "additionalProperties": {
            "type": "string",
            "enum": [
              ""
            ]
          }
        },
        "example": {
          "theme": "",
          "font": ""
        }
      },
      "OneKeyName": {
        "name": "keys",
        "in": "query",
        "required": false,
        "style": "form",
        "explode": true,
        "description": "Exactly one key name, sent as a query parameter with an empty value: `?theme=`. (The parameter name `keys` itself is never sent.) A reserved word can't be a key name.",
        "schema": {
          "type": "object",
          "additionalProperties": {
            "type": "string",
            "enum": [
              ""
            ]
          }
        },
        "example": {
          "theme": ""
        }
      },
      "Domain": {
        "name": "domain",
        "in": "query",
        "required": false,
        "description": "Scope level 1 (your own label). Unset means \"no domain\", not \"any\". An empty value means unset.",
        "schema": {
          "type": "string"
        }
      },
      "Subdomain": {
        "name": "subdomain",
        "in": "query",
        "required": false,
        "description": "Scope level 2 (your own label).",
        "schema": {
          "type": "string"
        }
      },
      "Project": {
        "name": "project",
        "in": "query",
        "required": false,
        "description": "Scope level 3 (your own label).",
        "schema": {
          "type": "string"
        }
      },
      "Type": {
        "name": "type",
        "in": "query",
        "required": false,
        "description": "Your own type label. `list` and `map` are reserved values (use `list`/`__map` instead).",
        "schema": {
          "type": "string"
        }
      },
      "List": {
        "name": "list",
        "in": "query",
        "required": false,
        "description": "Name of a list (append-only, duplicate values allowed) to use instead of plain keys. Not with `__map`.",
        "schema": {
          "type": "string"
        }
      },
      "Map": {
        "name": "__map",
        "in": "query",
        "required": false,
        "description": "Name of a map (named group of keys; a key written again overwrites) to use instead of plain keys. Not with `list`.",
        "schema": {
          "type": "string"
        }
      },
      "Filter": {
        "name": "__filter",
        "in": "query",
        "required": false,
        "description": "Only rows whose value matches: an SQL LIKE pattern (`%` any run of characters). Values stored encrypted (dashboard keys by default) match only an exact value; a `%` pattern there is `400 FILTER_UNAVAILABLE`.",
        "schema": {
          "type": "string"
        }
      },
      "Ttl": {
        "name": "ttl",
        "in": "query",
        "required": false,
        "description": "Days until the written rows expire. Absent, 0 or negative: never. Rewriting a key without `ttl` keeps its expiry.",
        "schema": {
          "type": "integer"
        }
      },
      "Limit": {
        "name": "__limit",
        "in": "query",
        "required": false,
        "description": "Page size, 1 up to the plan's `read_max_items` (1000 on free). Above it is a 400 (`LIMIT_TOO_LARGE`).",
        "schema": {
          "type": "integer",
          "minimum": 1
        }
      },
      "After": {
        "name": "__after",
        "in": "query",
        "required": false,
        "description": "Continue after a previous page: that response's `next`. Not with `__start`.",
        "schema": {
          "type": "string"
        }
      },
      "Start": {
        "name": "__start",
        "in": "query",
        "required": false,
        "description": "Skip this many rows (offset paging). Not with `__after`.",
        "schema": {
          "type": "integer",
          "minimum": 0
        }
      },
      "History": {
        "name": "__history",
        "in": "query",
        "required": false,
        "description": "`1` lists the key's archived earlier values (newest first) instead of its value. Exactly one key; not for lists.",
        "schema": {
          "type": "string",
          "enum": [
            "1"
          ]
        }
      },
      "Seed": {
        "name": "seed",
        "in": "query",
        "required": true,
        "description": "Your secret phrase. With `name` (and the calling site) it identifies the key. Treat it like a password.",
        "schema": {
          "type": "string"
        }
      },
      "Name": {
        "name": "name",
        "in": "query",
        "required": false,
        "description": "A label mixed into the key's identity, so one seed can have several keys (one per app). Default \"\".",
        "schema": {
          "type": "string"
        }
      },
      "Email": {
        "name": "email",
        "in": "query",
        "required": false,
        "description": "Needed when a new key is created and email verification applies (on by default): the new key works only after the link sent here is clicked (`403 UNVERIFIED` until then). Ignored for an existing key. An address at `sandbox.prefsdb.com` (any name before the @) makes a test key instead: no email is sent, and it works at once, for a day.",
        "schema": {
          "type": "string",
          "format": "email"
        }
      },
      "Namespace": {
        "name": "namespace",
        "in": "query",
        "required": false,
        "description": "For callers without an `Origin` header (servers, scripts): a long random string that keeps your keys apart from other callers' with the same seed and name. Ignored when an `Origin` header is sent. Max 255 characters. Not with `appcred`.",
        "schema": {
          "type": "string"
        }
      },
      "Appcred": {
        "name": "appcred",
        "in": "query",
        "required": false,
        "description": "An app credential from the dashboard: like `namespace`, but checked (unknown or revoked is `403 APPCRED_INVALID`). Max 128 characters.",
        "schema": {
          "type": "string"
        }
      },
      "Offline": {
        "name": "offline",
        "in": "query",
        "required": false,
        "description": "Any value also grants a longer session cookie (`prefs_offline_token`, 8 hours by default) for read/write/delete/undo/channels.",
        "schema": {
          "type": "string"
        }
      },
      "OfflineTtl": {
        "name": "offlinettl",
        "in": "query",
        "required": false,
        "description": "Lifetime of the offline session in seconds, 5 to 604800. Default 28800.",
        "schema": {
          "type": "integer"
        }
      },
      "SessionTtl": {
        "name": "sessionttl",
        "in": "query",
        "required": false,
        "description": "Lifetime of the `prefs_token` session in seconds, 5 to 86400. Default 60.",
        "schema": {
          "type": "integer"
        }
      },
      "FileId": {
        "name": "file",
        "in": "query",
        "required": true,
        "description": "The file id `putobject` returned.",
        "schema": {
          "type": "string",
          "pattern": "^[a-f0-9]{32}$"
        }
      },
      "FileDomain": {
        "name": "domain",
        "in": "query",
        "required": false,
        "description": "The `domain` the file was uploaded with, if any.",
        "schema": {
          "type": "string",
          "pattern": "^[A-Za-z0-9_-]{1,128}$"
        }
      },
      "FileSubdomain": {
        "name": "subdomain",
        "in": "query",
        "required": false,
        "description": "The `subdomain` the file was uploaded with, if any.",
        "schema": {
          "type": "string",
          "pattern": "^[A-Za-z0-9_-]{1,128}$"
        }
      },
      "FileProject": {
        "name": "project",
        "in": "query",
        "required": false,
        "description": "The `project` the file was uploaded with, if any.",
        "schema": {
          "type": "string",
          "pattern": "^[A-Za-z0-9_-]{1,128}$"
        }
      },
      "FileList": {
        "name": "list",
        "in": "query",
        "required": false,
        "description": "The `list` the file was uploaded with, if any.",
        "schema": {
          "type": "string",
          "pattern": "^[A-Za-z0-9_-]{1,128}$"
        }
      },
      "Channel": {
        "name": "channel",
        "in": "query",
        "required": true,
        "description": "Channel name. Anyone who knows it can publish and read, so make it unique to your app.",
        "schema": {
          "type": "string",
          "pattern": "^[A-Za-z0-9_-]{1,128}$"
        }
      },
      "Renew": {
        "name": "renew",
        "in": "query",
        "required": false,
        "description": "Renew the session of an existing key instead of signing in: never makes a key and returns no new `reviewUrl`. With `seed`+`name`, send the key too (`X-Authorization`) and the call isn't counted against the per-IP limit. For a key from a sign-in, send the key and its `session.renewToken` as `X-Prefs-Renew` instead of a seed; `401 SIGNIN_EXPIRED` when that no longer works.",
        "schema": {
          "type": "string",
          "enum": [
            "1"
          ]
        }
      },
      "IfVersion": {
        "name": "__ifVersion",
        "in": "query",
        "required": false,
        "description": "A conditional change: only if the key's `version` (from a read or the last write) is still this, or with `0` (writes only) only if the key doesn't exist yet. Otherwise `409 CONFLICT` with the key's `current` value and version, and nothing is changed. One key (or map entry) per request; not for lists (`CONDITION_NOT_SUPPORTED`).",
        "schema": {
          "type": "integer",
          "minimum": 0
        }
      },
      "Since": {
        "name": "__since",
        "in": "query",
        "required": false,
        "description": "The change feed: instead of the values, what changed in this scope after this `changeSeq` (from any read of keys or a map, or the previous feed call), oldest first, deletions included (`changes`, then the next `changeSeq`). `0` lists every value there is, without deletions. A `changeSeq` older than the deletions the server keeps (30 days) is `410 CHANGES_EXPIRED`: read the scope again. Keys and maps only. Pages with `__limit`; not with `__filter`, `__start` or `__after`.",
        "schema": {
          "type": "integer",
          "minimum": 0
        }
      },
      "Remember": {
        "name": "remember",
        "in": "query",
        "required": false,
        "description": "`1`: also return a `deviceGrant` for this browser, which `POST /getkey/resume/` trades for this key and a new session later (after a reload), with no seed or sign-in. Lasts `rememberdays` (default 30, at most 90) unless revoked (`/devices/`). Not on a `renew`. Anyone holding the grant can use the key while it lasts: keep it where only this user can read it.",
        "schema": {
          "type": "string",
          "enum": [
            "1"
          ]
        }
      },
      "RememberDays": {
        "name": "rememberdays",
        "in": "query",
        "required": false,
        "description": "With `remember`: days the grant lasts (1 to 90, default 30).",
        "schema": {
          "type": "integer",
          "minimum": 1,
          "maximum": 90
        }
      },
      "Device": {
        "name": "device",
        "in": "query",
        "required": false,
        "description": "With `remember`: a name for this device in the list (`/devices/`); default: browser and system from the User-Agent.",
        "schema": {
          "type": "string",
          "maxLength": 120
        }
      }
    },
    "schemas": {
      "ErrorCode": {
        "description": "Every failure's `error`. Stable: check this, not `message`.",
        "oneOf": [
          {
            "const": "BAD_REQUEST",
            "description": "400. A parameter is missing or malformed; `message` says which."
          },
          {
            "const": "MISSING_API_KEY",
            "description": "400. No API key was sent. Send `X-Authorization: Token <key>` or `Authorization: Bearer <key>`."
          },
          {
            "const": "INVALID_API_KEY",
            "description": "400. What was sent can't be an API key (too short, or \"null\")."
          },
          {
            "const": "UNKNOWN_API_KEY",
            "description": "400. No such key (or it was deleted in the dashboard)."
          },
          {
            "const": "RESERVED_KEY_NAME",
            "description": "400. A reserved word used as a key name; the body's `key` names it. Rename the key."
          },
          {
            "const": "KEY_TOO_LONG",
            "description": "400. A key name over 240 characters."
          },
          {
            "const": "VALUE_TOO_LARGE",
            "description": "400. A value over the plan's limit (1 MB free, 4 MB Plus, 8 MB Pro), or more than 8 MB of values in one write; the body has `limit`. Store it as a file (putobject)."
          },
          {
            "const": "FILTER_UNAVAILABLE",
            "description": "400. A `%` pattern on a key whose values are stored encrypted. Use an exact value."
          },
          {
            "const": "BAD_LIMIT",
            "description": "400. `__limit` isn't a whole number of 1 or more."
          },
          {
            "const": "BAD_CURSOR",
            "description": "400. `__after` isn't a `next` from a previous read."
          },
          {
            "const": "LIMIT_TOO_LARGE",
            "description": "400. `__limit` is above the plan's `read_max_items` (1000 on free)."
          },
          {
            "const": "BAD_START",
            "description": "400. `__start` isn't a whole number of 0 or more."
          },
          {
            "const": "BAD_PAGING",
            "description": "400. `__start` and `__after` together, or either (or `__filter`) with `__since`."
          },
          {
            "const": "BAD_VERSION",
            "description": "400. `__ifVersion` isn't a whole number (at least 1 on a delete)."
          },
          {
            "const": "CONDITION_NOT_SUPPORTED",
            "description": "400. `__ifVersion` on a list: list entries have no version."
          },
          {
            "const": "BAD_SINCE",
            "description": "400. `__since` isn't a `changeSeq` (a whole number)."
          },
          {
            "const": "SINCE_NOT_SUPPORTED",
            "description": "400. `__since` on a list: the change feed covers keys and maps."
          },
          {
            "const": "CHANGES_EXPIRED",
            "description": "410. `__since` is older than the deletions the server keeps (30 days), or not from this key. Read the whole scope again and continue from its `changeSeq`."
          },
          {
            "const": "GRANT_IN_QUERY",
            "description": "400. A device grant in the URL. Send it in the POST body."
          },
          {
            "const": "DEVICE_GRANT_INVALID",
            "description": "401. The device grant was revoked (signed out, or removed from the list of devices), ran out, or never existed. Sign in again."
          },
          {
            "const": "RESERVED_ORIGIN",
            "description": "400. An `Origin` header starting with `ns:`, `ac:` or `oidc:`, or not printable ASCII. Send none and use `namespace`/`appcred`."
          },
          {
            "const": "IDTOKEN_IN_QUERY",
            "description": "400. `id_token`, `signin_code` or `code_verifier` was in the URL. Send it in the POST body."
          },
          {
            "const": "IDTOKEN_CONFLICT",
            "description": "400. A sign-in request also had `seed`, `namespace`, `appcred` or `email`."
          },
          {
            "const": "IDTOKEN_PROVIDER",
            "description": "400. `provider` is unknown, switched off, or doesn't fit the request."
          },
          {
            "const": "IDTOKEN_NOT_SUPPORTED",
            "description": "400. createkey doesn't take sign-ins; use getkey."
          },
          {
            "const": "INVALID_DEVICE_CODE",
            "description": "400. pair/poll/: no pairing request has this device code (it ended and was removed, or never existed). Start again."
          },
          {
            "const": "SESSION_REQUIRED",
            "description": "401. A key from getkey/createkey used without its session (`X-Prefs-Session`, or the `prefs_token` cookie). Call getkey again and send `session.token`."
          },
          {
            "const": "SESSION_EXPIRED",
            "description": "401. The session expired (60 seconds by default). Call getkey again."
          },
          {
            "const": "IDTOKEN_INVALID",
            "description": "401. The ID token or sign-in code failed verification, or is too old. Sign in again."
          },
          {
            "const": "IDTOKEN_REPLAYED",
            "description": "401. That sign-in was already used. Sign in again."
          },
          {
            "const": "SESSION_MISMATCH",
            "description": "403. The session belongs to another key. Call getkey for this key."
          },
          {
            "const": "HTTPS_REQUIRED",
            "description": "403/401. getkey, createkey and logout need HTTPS."
          },
          {
            "const": "KEY_EXISTS",
            "description": "403. createkey for a seed and name that already have a key here. Use getkey."
          },
          {
            "const": "UNVERIFIED",
            "description": "403. A new key whose email address hasn't been confirmed yet."
          },
          {
            "const": "SPONSORSHIP_LAPSED",
            "description": "403. A key made without email on a site that no longer sponsors keys."
          },
          {
            "const": "KEY_SUSPENDED",
            "description": "403. A dashboard key that is suspended (new Live keys start this way)."
          },
          {
            "const": "ACCOUNT_SUSPENDED",
            "description": "403. The key's dashboard account is suspended."
          },
          {
            "const": "KEY_EXPIRED",
            "description": "403. The key's expiry date has passed. Its owner can create a new one in the dashboard."
          },
          {
            "const": "KEY_READ_ONLY",
            "description": "403. A read-only key was used to change something (write, delete, upload, publish, ...)."
          },
          {
            "const": "SCOPE_NOT_ALLOWED",
            "description": "403. The key is limited to a scope and the request didn't name exactly that scope; the body's `scope` says what to send."
          },
          {
            "const": "NOT_DASHBOARD_KEY",
            "description": "403. pair/self/ with a key from getkey/createkey; it describes dashboard and agent keys only."
          },
          {
            "const": "NOT_PAIRED_KEY",
            "description": "403. pair/revoke/ with a key not made by pairing; such keys are deleted in the dashboard."
          },
          {
            "const": "APPCRED_INVALID",
            "description": "403. Unknown or revoked `appcred`."
          },
          {
            "const": "IP_NOT_ALLOWED",
            "description": "403. The key only accepts requests from listed IP addresses."
          },
          {
            "const": "ORIGIN_NOT_ALLOWED",
            "description": "403. The key only accepts requests from listed sites."
          },
          {
            "const": "TOO_MANY_KEYS",
            "description": "403 when the email address already backs the most keys allowed (waiting doesn't help); 429 when a sponsored site reached its new keys for the day (see `Retry-After`)."
          },
          {
            "const": "IDTOKEN_EMAIL_UNVERIFIED",
            "description": "403. The sign-in account's email address isn't verified."
          },
          {
            "const": "NOT_FOUND",
            "description": "404. No such file in this key's storage (with this scope)."
          },
          {
            "const": "METHOD_NOT_ALLOWED",
            "description": "405. Wrong HTTP method; `message` names the right one."
          },
          {
            "const": "DISK_QUOTA_EXCEEDED",
            "description": "405. The upload would go over a storage limit; `quota.metric` says which (`disk`, `email_disk`, `account_disk`, `ip_disk`, `sponsor_disk`)."
          },
          {
            "const": "CONFLICT",
            "description": "409. A change that can't be made: `__ifVersion` didn't match (the body's `current` has the key's value and version now; nothing was changed), or a key can't be moved or rotated that way (`message` says why)."
          },
          {
            "const": "IDTOKEN_KEY",
            "description": "409. A key from a sign-in can't be moved to another origin."
          },
          {
            "const": "FILE_TOO_LARGE",
            "description": "413. A file over the plan's per-file limit (12 MB on free)."
          },
          {
            "const": "QUOTA_EXCEEDED",
            "description": "429. The key (or its account) used up its requests or transfer for the minute, day or month; `quota` says which. Wait `Retry-After` seconds."
          },
          {
            "const": "RATE_LIMITED",
            "description": "429. Too many attempts (getkey/createkey per IP address, verification emails). Wait `Retry-After` seconds."
          },
          {
            "const": "INTERNAL_ERROR",
            "description": "500. Server fault; the body has a `ref` to quote. Safe to retry once."
          },
          {
            "const": "WRITE_FAILED",
            "description": "500. Some rows of a write failed; `message` says how many."
          },
          {
            "const": "STORAGE_ERROR",
            "description": "500. A file couldn't be stored or read."
          },
          {
            "const": "KEY_GENERATION_FAILED",
            "description": "500. No unique key could be made. Retry."
          },
          {
            "const": "KEY_UNAVAILABLE",
            "description": "500. getkey found the key but can't hand it back right now. The key itself still works."
          },
          {
            "const": "IDTOKEN_KEYS_UNAVAILABLE",
            "description": "503. The sign-in provider's signing keys can't be fetched right now. Wait `Retry-After` seconds."
          },
          {
            "const": "SIGNIN_EXPIRED",
            "description": "401. `getkey?renew=1` with a sign-in key whose renewal token is missing, invalid or expired. Sign in again."
          },
          {
            "const": "TEST_KEY_NO_FILES",
            "description": "403. `putobject` with a test key (made with an address at `sandbox.prefsdb.com`). Test keys can't store files."
          }
        ]
      },
      "Storage": {
        "type": "object",
        "description": "The key's file storage.",
        "required": [
          "used",
          "max",
          "plan"
        ],
        "properties": {
          "used": {
            "type": "integer",
            "description": "Bytes used."
          },
          "max": {
            "type": [
              "integer",
              "null"
            ],
            "description": "The plan's limit in bytes; null means none."
          },
          "plan": {
            "type": "string",
            "examples": [
              "free"
            ]
          }
        }
      },
      "Error": {
        "type": "object",
        "required": [
          "success",
          "error",
          "message"
        ],
        "properties": {
          "success": {
            "const": false
          },
          "status": {
            "type": "string",
            "examples": [
              "error"
            ]
          },
          "error": {
            "$ref": "#/components/schemas/ErrorCode"
          },
          "message": {
            "type": "string",
            "description": "What went wrong, for people. May change; don't match on it."
          },
          "ts": {
            "type": "string",
            "description": "Server time, UTC, `YYYY-MM-DD HH:MM:SS`.",
            "examples": [
              "2026-10-23 12:00:00"
            ]
          },
          "ref": {
            "type": "string",
            "description": "500 only: quote it to support."
          },
          "retryAfter": {
            "type": "integer",
            "description": "429 (and 503): seconds to wait, the same as the `Retry-After` header."
          },
          "key": {
            "type": "string",
            "description": "`RESERVED_KEY_NAME`: the key name that was refused."
          },
          "limit": {
            "type": "integer",
            "description": "`VALUE_TOO_LARGE`: the largest value allowed, in bytes."
          },
          "quota": {
            "type": "object",
            "description": "Which limit was reached (`QUOTA_EXCEEDED`, `DISK_QUOTA_EXCEEDED`, `FILE_TOO_LARGE`).",
            "properties": {
              "metric": {
                "type": "string",
                "examples": [
                  "ops",
                  "bytes",
                  "disk",
                  "upload_size"
                ]
              },
              "window": {
                "type": "string",
                "enum": [
                  "minute",
                  "day",
                  "month"
                ]
              },
              "limit": {
                "type": "integer"
              },
              "used": {
                "type": "integer"
              },
              "scope": {
                "type": "string",
                "enum": [
                  "key",
                  "account"
                ]
              }
            }
          },
          "plan": {
            "type": "string",
            "description": "The plan whose limit applied."
          },
          "storage": {
            "$ref": "#/components/schemas/Storage"
          },
          "secure": {
            "type": "string",
            "description": "getkey/createkey: whether the request came over HTTPS."
          },
          "scope": {
            "type": "object",
            "description": "`SCOPE_NOT_ALLOWED`: the scope the key is limited to, e.g. `{\"project\": \"app\"}`. Send exactly these.",
            "additionalProperties": {
              "type": "string"
            }
          },
          "expectedVersion": {
            "type": "integer",
            "description": "`CONFLICT` for `__ifVersion`: the version that was sent."
          },
          "current": {
            "type": "object",
            "description": "`CONFLICT` for `__ifVersion`: the key now. `exists` false when there is no such key.",
            "properties": {
              "exists": {
                "type": "boolean"
              },
              "value": {
                "type": "string"
              },
              "version": {
                "type": "integer"
              },
              "uid": {
                "type": "string"
              },
              "modifiedt": {
                "type": "string",
                "description": "Server time, UTC, `YYYY-MM-DD HH:MM:SS`.",
                "examples": [
                  "2026-10-23 12:00:00"
                ]
              }
            }
          }
        }
      },
      "KeyResponse": {
        "type": "object",
        "required": [
          "success",
          "token"
        ],
        "properties": {
          "success": {
            "const": true
          },
          "status": {
            "type": "string"
          },
          "ts": {
            "type": "string",
            "description": "Server time, UTC, `YYYY-MM-DD HH:MM:SS`.",
            "examples": [
              "2026-10-23 12:00:00"
            ]
          },
          "token": {
            "type": "string",
            "description": "The API key. Send it as `X-Authorization: Token <token>` (or `Authorization: Bearer <token>`)."
          },
          "message": {
            "type": "string",
            "examples": [
              "Existing key found.",
              "New key generated."
            ]
          },
          "secure": {
            "type": "string"
          },
          "originScoped": {
            "type": "boolean",
            "description": "Whether the key is kept apart by origin, `namespace` or `appcred` (false: the shared bucket of callers without any)."
          },
          "emailVerificationRequired": {
            "type": "boolean",
            "description": "True while the key works only after its email is confirmed: a new key, or an existing one whose link hasn't been clicked yet. `verify/status/` tells when that has happened."
          },
          "sponsored": {
            "type": "boolean",
            "description": "True when the key was made without email because the calling site sponsors keys."
          },
          "storage": {
            "$ref": "#/components/schemas/Storage"
          },
          "reviewUrl": {
            "type": "string",
            "format": "uri",
            "description": "A page where the key's owner can see and download their data."
          },
          "session": {
            "$ref": "#/components/schemas/KeySession"
          },
          "testKey": {
            "type": "boolean",
            "description": "Present (true) when the key is a test key: made with an email address at `sandbox.prefsdb.com`, it works at once with no email sent, has small limits, can't store files and expires (`expires`). For checking an integration without a real inbox."
          },
          "expires": {
            "type": "string",
            "description": "With `testKey`: when the key stops working (UTC, `YYYY-MM-DD HH:MM:SS UTC`). Call getkey again after that for a new, empty one."
          },
          "deviceGrant": {
            "$ref": "#/components/schemas/DeviceGrant"
          }
        }
      },
      "ReadResponse": {
        "type": "object",
        "required": [
          "success",
          "count"
        ],
        "description": "Exactly one of `value` (one row matched, or none: then `value` is \"\" and `count` 0), `values` (several rows, or any paged read) or `history` (`__history=1`). The request's scope fields are echoed back. With `__since`: `changes` and `changeSeq` instead.",
        "properties": {
          "success": {
            "const": true
          },
          "status": {
            "type": "string"
          },
          "ts": {
            "type": "string",
            "description": "Server time, UTC, `YYYY-MM-DD HH:MM:SS`.",
            "examples": [
              "2026-10-23 12:00:00"
            ]
          },
          "value": {
            "type": "string",
            "description": "The value, when one row matched (\"\" when none did)."
          },
          "uid": {
            "type": "string",
            "description": "With `value`: the row's id."
          },
          "modifiedt": {
            "type": "string",
            "description": "With `value`: when the row was last written.",
            "examples": [
              "2026-10-23 12:00:00"
            ]
          },
          "values": {
            "type": "array",
            "items": {
              "type": "object",
              "description": "One row: the key name as a property holding its value (`{\"theme\": \"dark\", ...}`), the row's `uid` and `modifiedt`, and whichever scope fields (`domain`, `subdomain`, `project`, `type`, `name`, `list`) the request didn't fix.",
              "properties": {
                "uid": {
                  "type": "string"
                },
                "modifiedt": {
                  "type": "string",
                  "description": "Server time, UTC, `YYYY-MM-DD HH:MM:SS`.",
                  "examples": [
                    "2026-10-23 12:00:00"
                  ]
                },
                "version": {
                  "type": "integer",
                  "description": "The row's version: changes whenever it's written (not 1, 2, 3: one counter per API key). Send it as `__ifVersion` for a conditional change. List entries have none."
                }
              },
              "additionalProperties": {
                "type": "string"
              }
            }
          },
          "history": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "keyvalue": {
                  "type": "string"
                },
                "op": {
                  "type": "string",
                  "enum": [
                    "overwrite",
                    "delete"
                  ]
                },
                "archivedt": {
                  "type": "string",
                  "description": "Server time, UTC, `YYYY-MM-DD HH:MM:SS`.",
                  "examples": [
                    "2026-10-23 12:00:00"
                  ]
                }
              }
            }
          },
          "count": {
            "type": "integer",
            "description": "Rows in this response."
          },
          "more": {
            "type": "boolean",
            "description": "More rows match: read again with `__after` set to `next`. Decide on this, not on `count`."
          },
          "next": {
            "type": "string",
            "description": "With `more`: the cursor for `__after`."
          },
          "warning": {
            "type": "string",
            "description": "A read without `__limit` that was cut at the plan's maximum."
          },
          "listName": {
            "type": "string"
          },
          "mapName": {
            "type": "string"
          },
          "version": {
            "type": "integer",
            "description": "With `value`: the key's version (see `__ifVersion`)."
          },
          "changeSeq": {
            "type": "integer",
            "description": "Keys and maps: the API key's change number when this was read. Ask `__since=<changeSeq>` for what changes after it."
          },
          "changes": {
            "type": "array",
            "description": "`__since`: the changes, oldest first.",
            "items": {
              "$ref": "#/components/schemas/Change"
            }
          }
        },
        "additionalProperties": true
      },
      "WriteResponse": {
        "type": "object",
        "required": [
          "success",
          "updated"
        ],
        "properties": {
          "success": {
            "const": true
          },
          "status": {
            "type": "string"
          },
          "ts": {
            "type": "string",
            "description": "Server time, UTC, `YYYY-MM-DD HH:MM:SS`.",
            "examples": [
              "2026-10-23 12:00:00"
            ]
          },
          "message": {
            "type": "string"
          },
          "updated": {
            "type": "integer",
            "description": "Rows written. 0: unchanged."
          },
          "type": {
            "type": "string"
          },
          "listName": {
            "type": "string"
          },
          "mapName": {
            "type": "string"
          },
          "version": {
            "type": "integer",
            "description": "One key or map entry written: its new version (for a later `__ifVersion`)."
          }
        }
      },
      "DeleteResponse": {
        "type": "object",
        "required": [
          "success",
          "affectedCount"
        ],
        "properties": {
          "success": {
            "const": true
          },
          "status": {
            "type": "string"
          },
          "ts": {
            "type": "string",
            "description": "Server time, UTC, `YYYY-MM-DD HH:MM:SS`.",
            "examples": [
              "2026-10-23 12:00:00"
            ]
          },
          "affectedCount": {
            "type": "integer",
            "description": "Rows deleted."
          }
        },
        "additionalProperties": true
      },
      "UndoResponse": {
        "type": "object",
        "required": [
          "success",
          "restored"
        ],
        "properties": {
          "success": {
            "const": true
          },
          "status": {
            "type": "string"
          },
          "ts": {
            "type": "string",
            "description": "Server time, UTC, `YYYY-MM-DD HH:MM:SS`.",
            "examples": [
              "2026-10-23 12:00:00"
            ]
          },
          "restored": {
            "type": "boolean",
            "description": "False when there was nothing to restore (no history, or value history is off on this server)."
          },
          "value": {
            "type": "string",
            "description": "The value restored."
          },
          "restoredOp": {
            "type": "string",
            "enum": [
              "overwrite",
              "delete"
            ],
            "description": "What was undone."
          },
          "restoredFrom": {
            "type": "string",
            "description": "When the undone change happened.",
            "examples": [
              "2026-10-23 12:00:00"
            ]
          },
          "message": {
            "type": "string"
          },
          "version": {
            "type": "integer",
            "description": "The key's new version."
          }
        },
        "additionalProperties": true
      },
      "ExpireResponse": {
        "type": "object",
        "required": [
          "success",
          "ttl",
          "updated"
        ],
        "properties": {
          "success": {
            "const": true
          },
          "status": {
            "type": "string"
          },
          "ts": {
            "type": "string",
            "description": "Server time, UTC, `YYYY-MM-DD HH:MM:SS`.",
            "examples": [
              "2026-10-23 12:00:00"
            ]
          },
          "ttl": {
            "type": "integer",
            "description": "The expiry now set in days; -1: none."
          },
          "updated": {
            "type": "object",
            "description": "Rows changed per table (`pairs`: keys and map entries, `lists`: list entries).",
            "additionalProperties": {
              "type": "integer"
            }
          }
        }
      },
      "PutObjectResponse": {
        "type": "object",
        "required": [
          "success",
          "file"
        ],
        "properties": {
          "success": {
            "const": true
          },
          "ts": {
            "type": "string",
            "description": "Server time, UTC, `YYYY-MM-DD HH:MM:SS`.",
            "examples": [
              "2026-10-23 12:00:00"
            ]
          },
          "file": {
            "type": "string",
            "description": "The file id, for getobject and deleteobject (with the same scope)."
          },
          "origin": {
            "type": "string",
            "description": "The uploaded file's name."
          },
          "domain": {
            "type": "string"
          },
          "subdomain": {
            "type": "string"
          },
          "project": {
            "type": "string"
          },
          "list": {
            "type": "string"
          },
          "storage_used": {
            "type": "integer"
          },
          "storage_max": {
            "type": [
              "integer",
              "null"
            ]
          },
          "storage": {
            "$ref": "#/components/schemas/Storage"
          },
          "shareUrl": {
            "type": "string",
            "format": "uri",
            "description": "Public link that downloads the file. Anyone with it can fetch the file."
          },
          "shareViewUrl": {
            "type": "string",
            "format": "uri",
            "description": "Public link that shows images, audio, video, PDF and plain text in the browser (usable in `<img src>`)."
          }
        }
      },
      "ShareLinksResponse": {
        "type": "object",
        "required": [
          "success",
          "file",
          "shareUrl",
          "shareViewUrl"
        ],
        "properties": {
          "success": {
            "const": true
          },
          "ts": {
            "type": "string",
            "description": "Server time, UTC, `YYYY-MM-DD HH:MM:SS`.",
            "examples": [
              "2026-10-23 12:00:00"
            ]
          },
          "file": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "size": {
            "type": "integer"
          },
          "shareUrl": {
            "type": "string",
            "format": "uri",
            "description": "Public link that downloads the file. Anyone with it can fetch the file."
          },
          "shareViewUrl": {
            "type": "string",
            "format": "uri",
            "description": "Public link that shows images, audio, video, PDF and plain text in the browser (usable in `<img src>`)."
          }
        }
      },
      "DeleteObjectResponse": {
        "type": "object",
        "required": [
          "success",
          "file",
          "storage"
        ],
        "properties": {
          "success": {
            "const": true
          },
          "ts": {
            "type": "string",
            "description": "Server time, UTC, `YYYY-MM-DD HH:MM:SS`.",
            "examples": [
              "2026-10-23 12:00:00"
            ]
          },
          "file": {
            "type": "string"
          },
          "origin": {
            "type": "string"
          },
          "size": {
            "type": "integer"
          },
          "storage_used": {
            "type": "integer"
          },
          "storage_max": {
            "type": [
              "integer",
              "null"
            ]
          },
          "storage": {
            "$ref": "#/components/schemas/Storage"
          }
        }
      },
      "MigrateOriginResponse": {
        "type": "object",
        "required": [
          "success",
          "originScoped"
        ],
        "properties": {
          "success": {
            "const": true
          },
          "ts": {
            "type": "string",
            "description": "Server time, UTC, `YYYY-MM-DD HH:MM:SS`.",
            "examples": [
              "2026-10-23 12:00:00"
            ]
          },
          "originScoped": {
            "type": "boolean"
          },
          "message": {
            "type": "string"
          }
        }
      },
      "RotateKeyResponse": {
        "type": "object",
        "required": [
          "success",
          "token"
        ],
        "properties": {
          "success": {
            "const": true
          },
          "ts": {
            "type": "string",
            "description": "Server time, UTC, `YYYY-MM-DD HH:MM:SS`.",
            "examples": [
              "2026-10-23 12:00:00"
            ]
          },
          "token": {
            "type": "string",
            "description": "The new API key. The old one stops working at once."
          },
          "originScoped": {
            "type": "boolean"
          },
          "message": {
            "type": "string"
          },
          "error": {
            "const": ""
          },
          "session": {
            "$ref": "#/components/schemas/KeySession"
          }
        }
      },
      "NonceResponse": {
        "type": "object",
        "required": [
          "success",
          "nonce",
          "expires"
        ],
        "properties": {
          "success": {
            "const": true
          },
          "status": {
            "type": "string"
          },
          "ts": {
            "type": "string",
            "description": "Server time, UTC, `YYYY-MM-DD HH:MM:SS`.",
            "examples": [
              "2026-10-23 12:00:00"
            ]
          },
          "nonce": {
            "type": "string",
            "description": "Put it in the sign-in request (Google `nonce`, etc.). One sign-in, 10 minutes."
          },
          "expires": {
            "type": "integer",
            "description": "Unix time it expires."
          }
        }
      },
      "PublishResponse": {
        "type": "object",
        "required": [
          "success",
          "id"
        ],
        "properties": {
          "success": {
            "const": true
          },
          "status": {
            "type": "string"
          },
          "ts": {
            "type": "string",
            "description": "Server time, UTC, `YYYY-MM-DD HH:MM:SS`.",
            "examples": [
              "2026-10-23 12:00:00"
            ]
          },
          "id": {
            "type": "string",
            "description": "The message's id."
          },
          "channel": {
            "type": "string"
          }
        }
      },
      "PollResponse": {
        "type": "object",
        "required": [
          "success",
          "count",
          "cursor"
        ],
        "properties": {
          "success": {
            "const": true
          },
          "status": {
            "type": "string"
          },
          "ts": {
            "type": "string",
            "description": "Server time, UTC, `YYYY-MM-DD HH:MM:SS`.",
            "examples": [
              "2026-10-23 12:00:00"
            ]
          },
          "messages": {
            "type": "array",
            "description": "Oldest first.",
            "items": {
              "type": "object",
              "properties": {
                "id": {
                  "type": "string"
                },
                "sender": {
                  "type": "string",
                  "description": "Stable, anonymous id of the publishing key."
                },
                "value": {
                  "type": "string"
                },
                "createdt": {
                  "type": "string",
                  "description": "Server time, UTC, `YYYY-MM-DD HH:MM:SS`.",
                  "examples": [
                    "2026-10-23 12:00:00"
                  ]
                }
              }
            }
          },
          "count": {
            "type": "integer"
          },
          "cursor": {
            "type": "string",
            "description": "Pass as `since` next time."
          }
        }
      },
      "VerifyResponse": {
        "type": "object",
        "required": [
          "success"
        ],
        "properties": {
          "success": {
            "const": true
          },
          "ts": {
            "type": "string",
            "description": "Server time, UTC, `YYYY-MM-DD HH:MM:SS`.",
            "examples": [
              "2026-10-23 12:00:00"
            ]
          },
          "message": {
            "type": "string"
          }
        }
      },
      "LogoutResponse": {
        "type": "object",
        "required": [
          "success"
        ],
        "properties": {
          "success": {
            "const": true
          },
          "status": {
            "type": "string"
          },
          "ts": {
            "type": "string",
            "description": "Server time, UTC, `YYYY-MM-DD HH:MM:SS`.",
            "examples": [
              "2026-10-23 12:00:00"
            ]
          },
          "hadSession": {
            "type": "boolean",
            "description": "Whether a session cookie was there to clear."
          }
        }
      },
      "PairStartResponse": {
        "type": "object",
        "required": [
          "success",
          "device_code",
          "user_code",
          "verification_uri",
          "expires_in",
          "interval"
        ],
        "properties": {
          "success": {
            "const": true
          },
          "status": {
            "type": "string"
          },
          "ts": {
            "type": "string",
            "description": "Server time, UTC, `YYYY-MM-DD HH:MM:SS`."
          },
          "device_code": {
            "type": "string",
            "description": "Secret: poll pair/poll/ with it. Never show it to anyone."
          },
          "user_code": {
            "type": "string",
            "pattern": "^[A-Z]{4}-[A-Z]{4}$",
            "description": "Show this to the human, who types it at verification_uri."
          },
          "verification_uri": {
            "type": "string",
            "format": "uri"
          },
          "verification_uri_complete": {
            "type": "string",
            "format": "uri",
            "description": "RFC 8628 compatibility only. Show verification_uri and user_code separately; the page never takes the code from the link."
          },
          "expires_in": {
            "type": "integer",
            "description": "Seconds the human has to approve."
          },
          "interval": {
            "type": "integer",
            "description": "Seconds to wait between polls."
          },
          "message": {
            "type": "string",
            "description": "A sentence to relay to the human."
          }
        }
      },
      "PairPollResponse": {
        "type": "object",
        "required": [
          "success",
          "state"
        ],
        "properties": {
          "success": {
            "const": true
          },
          "status": {
            "type": "string"
          },
          "ts": {
            "type": "string",
            "description": "Server time, UTC, `YYYY-MM-DD HH:MM:SS`."
          },
          "state": {
            "type": "string",
            "enum": [
              "pending",
              "slow_down",
              "approved",
              "denied",
              "expired",
              "collected"
            ],
            "description": "`pending`/`slow_down`: poll again after `interval`. `approved`: the key is in this response, once. The rest are final."
          },
          "interval": {
            "type": "integer"
          },
          "message": {
            "type": "string"
          },
          "key": {
            "type": "string",
            "description": "`approved` only: the API key. Store it like a password; never print it."
          },
          "name": {
            "type": "string"
          },
          "readOnly": {
            "type": "boolean"
          },
          "scope": {
            "type": "object",
            "additionalProperties": {
              "type": "string"
            },
            "description": "The scope the key is limited to (send exactly these with every request), `{}` for none."
          },
          "expires": {
            "type": [
              "string",
              "null"
            ],
            "description": "When the key stops working (UTC), or null."
          },
          "plan": {
            "type": "string"
          }
        }
      },
      "PairSelfResponse": {
        "type": "object",
        "required": [
          "success",
          "name",
          "paired",
          "readOnly",
          "scope",
          "storage"
        ],
        "properties": {
          "success": {
            "const": true
          },
          "status": {
            "type": "string"
          },
          "ts": {
            "type": "string",
            "description": "Server time, UTC, `YYYY-MM-DD HH:MM:SS`."
          },
          "name": {
            "type": "string"
          },
          "paired": {
            "type": "boolean",
            "description": "Made by agent pairing."
          },
          "readOnly": {
            "type": "boolean"
          },
          "scope": {
            "type": "object",
            "additionalProperties": {
              "type": "string"
            },
            "description": "The scope the key is limited to (send exactly these with every request), `{}` for none."
          },
          "expires": {
            "type": [
              "string",
              "null"
            ]
          },
          "storage": {
            "$ref": "#/components/schemas/Storage"
          }
        }
      },
      "PairRevokeResponse": {
        "type": "object",
        "required": [
          "success",
          "revoked"
        ],
        "properties": {
          "success": {
            "const": true
          },
          "status": {
            "type": "string"
          },
          "ts": {
            "type": "string",
            "description": "Server time, UTC, `YYYY-MM-DD HH:MM:SS`."
          },
          "revoked": {
            "const": true
          },
          "message": {
            "type": "string"
          }
        }
      },
      "KeySession": {
        "type": "object",
        "required": [
          "token",
          "expiresIn"
        ],
        "description": "The key's session (keys from getkey/createkey only). Send `token` as the `X-Prefs-Session` header on every call with this key, and `offlineToken` (if any) as `X-Prefs-Offline-Session`. Works from any website; the cookies of the same name are only sent by a page on prefsdb.com or a cookie jar. When it runs out (`401 SESSION_EXPIRED`), call getkey again.",
        "properties": {
          "token": {
            "type": "string"
          },
          "expiresIn": {
            "type": "integer",
            "description": "Seconds the token is valid for (`sessionttl`, default 60)."
          },
          "offlineToken": {
            "type": "string",
            "description": "With `offline`: a longer grant for read, write, delete, undo, expire and channels."
          },
          "offlineExpiresIn": {
            "type": "integer",
            "description": "Seconds `offlineToken` is valid for (`offlinettl`, default 8 hours)."
          },
          "renewToken": {
            "type": "string",
            "description": "Keys from a sign-in (ID token or GitHub) only: send it as `X-Prefs-Renew`, with the key, to `getkey?renew=1` to get a new session without signing in again. Valid for 7 days from the sign-in, never extended."
          },
          "renewExpiresIn": {
            "type": "integer",
            "description": "Seconds `renewToken` is valid for."
          }
        }
      },
      "VerificationStatusResponse": {
        "type": "object",
        "required": [
          "success",
          "verified"
        ],
        "properties": {
          "success": {
            "const": true
          },
          "verified": {
            "type": "boolean",
            "description": "True when the key can be used."
          },
          "reason": {
            "type": "string",
            "enum": [
              "UNVERIFIED",
              "SPONSORSHIP_LAPSED"
            ],
            "description": "When `verified` is false: the email isn't confirmed yet, or the key was made without an email under a site sponsorship that no longer covers it."
          },
          "ts": {
            "type": "string"
          }
        }
      },
      "Change": {
        "type": "object",
        "required": [
          "key",
          "version"
        ],
        "description": "One change: a key written (`value`, `uid`, `modifiedt`) or deleted (`deleted: true`, `deletedt`), with the version of that change, plus whichever scope fields the request didn't fix.",
        "properties": {
          "key": {
            "type": "string"
          },
          "value": {
            "type": "string"
          },
          "deleted": {
            "const": true
          },
          "version": {
            "type": "integer"
          },
          "uid": {
            "type": "string"
          },
          "modifiedt": {
            "type": "string",
            "description": "Server time, UTC, `YYYY-MM-DD HH:MM:SS`.",
            "examples": [
              "2026-10-23 12:00:00"
            ]
          },
          "deletedt": {
            "type": "string",
            "description": "Server time, UTC, `YYYY-MM-DD HH:MM:SS`.",
            "examples": [
              "2026-10-23 12:00:00"
            ]
          },
          "domain": {
            "type": "string"
          },
          "subdomain": {
            "type": "string"
          },
          "project": {
            "type": "string"
          },
          "type": {
            "type": "string"
          },
          "name": {
            "type": "string"
          }
        }
      },
      "DeviceGrant": {
        "type": "object",
        "required": [
          "id",
          "expires"
        ],
        "description": "With `remember`: this device's grant. `token` (only in the response that issued it, never again) is what `POST /getkey/resume/` takes.",
        "properties": {
          "token": {
            "type": "string",
            "description": "64 hex characters. Keep it like a password."
          },
          "id": {
            "type": "integer"
          },
          "expires": {
            "type": "string",
            "description": "UTC, `YYYY-MM-DD HH:MM:SS UTC`."
          }
        }
      },
      "ResumeResponse": {
        "type": "object",
        "required": [
          "success",
          "token"
        ],
        "properties": {
          "success": {
            "const": true
          },
          "status": {
            "type": "string"
          },
          "ts": {
            "type": "string",
            "description": "Server time, UTC, `YYYY-MM-DD HH:MM:SS`.",
            "examples": [
              "2026-10-23 12:00:00"
            ]
          },
          "token": {
            "type": "string",
            "description": "The API key. Send it as `X-Authorization: Token <token>` (or `Authorization: Bearer <token>`)."
          },
          "message": {
            "type": "string",
            "examples": [
              "Existing key found.",
              "New key generated."
            ]
          },
          "secure": {
            "type": "string"
          },
          "originScoped": {
            "type": "boolean",
            "description": "Whether the key is kept apart by origin, `namespace` or `appcred` (false: the shared bucket of callers without any)."
          },
          "emailVerificationRequired": {
            "type": "boolean",
            "description": "True while the key works only after its email is confirmed: a new key, or an existing one whose link hasn't been clicked yet. `verify/status/` tells when that has happened."
          },
          "sponsored": {
            "type": "boolean",
            "description": "True when the key was made without email because the calling site sponsors keys."
          },
          "storage": {
            "$ref": "#/components/schemas/Storage"
          },
          "reviewUrl": {
            "type": "string",
            "format": "uri",
            "description": "A page where the key's owner can see and download their data."
          },
          "session": {
            "$ref": "#/components/schemas/KeySession"
          },
          "testKey": {
            "type": "boolean",
            "description": "Present (true) when the key is a test key: made with an email address at `sandbox.prefsdb.com`, it works at once with no email sent, has small limits, can't store files and expires (`expires`). For checking an integration without a real inbox."
          },
          "expires": {
            "type": "string",
            "description": "With `testKey`: when the key stops working (UTC, `YYYY-MM-DD HH:MM:SS UTC`). Call getkey again after that for a new, empty one."
          },
          "deviceGrant": {
            "$ref": "#/components/schemas/DeviceGrant"
          }
        },
        "description": "As getkey's answer, without `reviewUrl`/`storage`; `deviceGrant` without its token."
      },
      "Device": {
        "type": "object",
        "required": [
          "id",
          "label",
          "created",
          "expires",
          "current"
        ],
        "properties": {
          "id": {
            "type": "integer"
          },
          "label": {
            "type": "string",
            "description": "`device`, or browser and system (\"Firefox on Windows\")."
          },
          "origin": {
            "type": "string",
            "description": "The site that asked for it."
          },
          "created": {
            "type": "string"
          },
          "lastUsed": {
            "type": [
              "string",
              "null"
            ],
            "description": "The last `resume`, or null."
          },
          "expires": {
            "type": "string"
          },
          "current": {
            "type": "boolean",
            "description": "The grant sent as `X-Prefs-Device`."
          }
        }
      },
      "DevicesResponse": {
        "type": "object",
        "required": [
          "success",
          "devices"
        ],
        "properties": {
          "success": {
            "const": true
          },
          "status": {
            "type": "string"
          },
          "ts": {
            "type": "string",
            "description": "Server time, UTC, `YYYY-MM-DD HH:MM:SS`.",
            "examples": [
              "2026-10-23 12:00:00"
            ]
          },
          "devices": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Device"
            }
          },
          "count": {
            "type": "integer"
          }
        }
      },
      "DevicesRevokeResponse": {
        "type": "object",
        "required": [
          "success",
          "revoked"
        ],
        "properties": {
          "success": {
            "const": true
          },
          "status": {
            "type": "string"
          },
          "ts": {
            "type": "string",
            "description": "Server time, UTC, `YYYY-MM-DD HH:MM:SS`.",
            "examples": [
              "2026-10-23 12:00:00"
            ]
          },
          "revoked": {
            "type": "integer",
            "description": "Grants ended (0: none matched)."
          }
        }
      }
    },
    "responses": {
      "Error": {
        "description": "Failure. `error` says why (see ErrorCode); a 429 has `Retry-After`.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "TooManyRequests": {
        "description": "A rate limit or quota: wait the number of seconds in `Retry-After`, then retry.",
        "headers": {
          "Retry-After": {
            "description": "Seconds to wait.",
            "schema": {
              "type": "integer"
            }
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "Conflict": {
        "description": "`CONFLICT` for `__ifVersion`: the key changed (or was deleted, or exists) since that version, so nothing was changed. `current` has the key's value and version now.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      }
    }
  }
}
