{"openapi":"3.1.0","info":{"title":"StackData API","version":"0.1.0","summary":"Public datasets from multilateral sources, searchable in any language, resolvable into tables and charts.","description":"StackData indexes open government and public data and resolves a plain-language\nquestion into a table or chart drawn from that data. Every answer links back to\nits source.\n\n## The contract this API keeps (and most don't)\n\n- **Unknown query parameters are a `400`, never silently ignored.** The error names\n  the offending parameter and lists what is allowed.\n- **Every JSON response carries `meta.harvest`** — the last successful harvest time of\n  each source, so staleness is visible without asking.\n- **Counts are typed** `{ \"value\": N, \"exact\": true|false }`, never a bare integer.\n- **Empty is distinguishable from failed.** An honest empty result has a `status`; a\n  missing resource is a `404`; an upstream failure is a `502`. A zero is never faked.\n\n## Authentication\n\nSend your key in the `X-API-Key` header. **It is optional** — with no key you are on\nthe free tier, identified by IP address, subject to a lower daily limit. A key raises\nyour limit and draws on prepaid credits.\n\n## Metering\n\nOnly two endpoints cost anything: `GET /v1/resolve` and `GET /v1/data`. The billed unit\nis a single *uncached upstream fetch* — cached answers, refusals, empties and errors are\nfree. All other endpoints (search, boards, coverage, health) are never metered.\n\n## Paying per answer as an agent (x402)\n\n**x402** is an open, HTTP-native payment standard — an [x402 Foundation](https://x402.org)\nproject under the Linux Foundation. A server answers a request it won't serve for free with\nHTTP `402 Payment Required` plus machine-readable payment terms; the caller pays from a\nwallet and retries the same request. No account, no API key, no card — the payment itself is\nthe authorization, which is why it suits autonomous agents.\n\n**The flow here.** When an **anonymous** caller is over the free daily quota, `GET /v1/data`\nreturns `402` (not a hard `429`) with the terms in the body — amount, asset, network, `payTo`\nand resource. The agent signs a stablecoin authorization and reissues the request with an\n`X-PAYMENT` header (base64-encoded JSON of the signed authorization). The server verifies it\nthrough a facilitator, serves the answer, and returns the settlement receipt in the\n`X-PAYMENT-RESPONSE` header (the JSON body also gains a `payment` object).\n\n- **Price** — $0.05 per uncached answer (one upstream pull).\n- **Asset & network** — USDC on Base, settled on-chain.\n- **You pay only for work** — cached answers, refusals, empties and errors are free; money moves only if the answer actually cost an uncached fetch.\n- **Single-use** — each signed authorization buys exactly one answer; replaying it returns `402`.\n\nThis flow is active only when the server has `X402_PAY_TO` configured.\n\n**Learn more:** [x402.org](https://x402.org) · [developer docs](https://docs.x402.org) · [reference implementation](https://github.com/x402-foundation/x402) · the HTTP status itself is [`402 Payment Required`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/402).\n","contact":{"name":"StackData","email":"hello@stackdata.io","url":"https://stackdata.io/contact.html"},"license":{"name":"Underlying data is licensed per-source (open government / CC BY 4.0 and equivalents)","url":"https://stackdata.io/attribution.html"}},"servers":[{"url":"https://api.stackdata.io","description":"Production (set to your deployed API host)"},{"url":"http://localhost:8080","description":"Local development (uvicorn api:app --app-dir serve --port 8080)"}],"security":[{},{"ApiKeyAuth":[]}],"tags":[{"name":"Discovery","description":"Find datasets and see what the catalog covers. Not metered."},{"name":"Resolution","description":"Turn a question into a plan, a table, or a chart. Metered."},{"name":"Boards","description":"Saved boards of answer-cards, owner-scoped by key. Pure DB, never metered."},{"name":"Auth","description":"Sign up, sign in (email or OAuth), sessions. Cookie or Bearer token."},{"name":"Account","description":"Usage, the signed-in user's profile, API key, and settings."},{"name":"Export","description":"Server-side chart export with tier-enforced watermark."},{"name":"System","description":"Health and cache state."}],"paths":{"/v1/search":{"get":{"tags":["Discovery"],"operationId":"search","summary":"Search the catalog","description":"Full-text and semantic search over dataset metadata. Works in **any language**\n(this is the multilingual arm — resolution below is English-only). Never metered.\n","parameters":[{"name":"q","in":"query","required":true,"description":"The search query, in any language.","schema":{"type":"string"},"example":"unemployment rate"},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","default":10,"minimum":1}},{"name":"country","in":"query","required":false,"description":"Filter to datasets scoped to this country.","schema":{"type":"string"}},{"name":"kind","in":"query","required":false,"description":"Filter by resource kind (e.g. a statistical dataflow vs. a document).","schema":{"type":"string"}},{"name":"open_only","in":"query","required":false,"description":"Return only datasets under an open licence.","schema":{"type":"boolean","default":false}}],"responses":{"200":{"description":"Ranked results.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SearchResponse"}}}},"400":{"$ref":"#/components/responses/UnknownParameter"}}}},"/v1/catalog":{"get":{"tags":["Discovery"],"operationId":"catalog","summary":"Page through the whole catalog","description":"The full public catalog as pageable metadata — for bulk consumers: static site\ngenerators, crawlers, mirrors. Returns **values, not provenance envelopes**\n(`/v1/datasets/{id}` is the full-envelope tier). Cursor pagination is keyset on\nthe stable dataset id: pass back `next_cursor` until it returns `null`. Never\nmetered.\n","parameters":[{"name":"cursor","in":"query","required":false,"description":"The `next_cursor` from the previous page; omit for the first page.","schema":{"type":"string"}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","default":500,"minimum":1,"maximum":1000}},{"name":"source","in":"query","required":false,"description":"Restrict to one source id (e.g. `ilo-sdmx`).","schema":{"type":"string"}}],"responses":{"200":{"description":"One page of the catalog.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CatalogResponse"}}}},"400":{"$ref":"#/components/responses/UnknownParameter"}}}},"/v1/datasets/{dataset_id}":{"get":{"tags":["Discovery"],"operationId":"getDataset","summary":"Full record for one dataset","description":"The full provenance envelope for a single dataset: publisher, cadence, status,\ndistribution and licence. A missing id is a `404` — **absent is not an empty success.**\n","parameters":[{"name":"dataset_id","in":"path","required":true,"description":"The dataset id. May itself contain slashes (it is matched as a path), so when calling by hand, URL-encode any `/` in the id.\n","schema":{"type":"string"}}],"responses":{"200":{"description":"The dataset record.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Dataset"}}}},"404":{"description":"No such dataset.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/coverage":{"get":{"tags":["Discovery"],"operationId":"coverage","summary":"Catalog coverage, bucketed","description":"Count of datasets grouped along one dimension. Not metered.","parameters":[{"name":"by","in":"query","required":false,"description":"The dimension to group by.","schema":{"type":"string","enum":["source","kind","publisher_type","licence"],"default":"source"}}],"responses":{"200":{"description":"Buckets with typed counts.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CoverageResponse"}}}},"400":{"description":"Unknown dimension, or an unknown query parameter.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/resolve":{"get":{"tags":["Resolution"],"operationId":"resolve","summary":"Resolve a question into a plan (no data fetched)","description":"Returns the cascade as a **plan**, not an answer — the model proposes, you inspect.\n`plan.status` is explicit: `ready` means it can be executed; anything else names\nexactly why not (`licence_forbids_render`, `licence_unverified`, `no_candidates`,\n`dsd_unavailable`). A blocked plan is still a `200` with an honest body.\n\n**Metered** (the embedding cost is paid on every call). May return `429`.\n","parameters":[{"name":"q","in":"query","required":true,"description":"The question, in English.","schema":{"type":"string"},"example":"unemployment rate in Japan"}],"responses":{"200":{"description":"The resolution plan.","content":{"application/json":{"schema":{"type":"object","properties":{"plan":{"type":"object","description":"The resolution plan. `status` is `ready` or a named block reason.","additionalProperties":true},"meta":{"$ref":"#/components/schemas/Meta"}}}}}},"400":{"$ref":"#/components/responses/UnknownParameter"},"429":{"$ref":"#/components/responses/QuotaExceeded"}}}},"/v1/data":{"get":{"tags":["Resolution"],"operationId":"getData","summary":"Resolve AND execute — return the actual table or chart","description":"Carries a ready plan through to the data. Everything the plan would refuse still\nrefuses here (a licence-blocked source never reaches the fetch).\n\nThe `200` body is a **status-tagged object**. Common `status` values:\n`ok`, `ok_correlation`, `ok_coverage` (success); `empty`, `no_candidates`,\n`dsd_unavailable`, `multi_concept_unparsed`, `language_unsupported` (honest\nnon-answers). With `format=html` the response is a self-contained HTML chart\n(`text/html`) instead of JSON.\n\n**Metered.** Over-quota keyed callers get `429`. Over-quota **anonymous** callers may\ninstead get `402` with x402 payment terms (only when the server has x402 configured).\n","parameters":[{"name":"q","in":"query","required":true,"description":"The question, in English (non-English prompts may be auto-translated by an LLM when configured).","schema":{"type":"string"},"example":"unemployment rate in Japan"},{"name":"start","in":"query","required":false,"description":"First year (inclusive) to include.","schema":{"type":"integer"},"example":2010},{"name":"end","in":"query","required":false,"description":"Last year (inclusive) to include.","schema":{"type":"integer"},"example":2023},{"name":"format","in":"query","required":false,"description":"`json` (default) returns data; `html` returns a self-contained chart.","schema":{"type":"string","enum":["json","html"],"default":"json"}},{"name":"dataset","in":"query","required":false,"description":"Pin resolution to this dataset id instead of the ranked default. An empty pinned dataset returns an honest empty, never a silent fallback.","schema":{"type":"string"}},{"name":"X-PAYMENT","in":"header","required":false,"description":"x402 payment payload (base64-encoded JSON of a signed stablecoin authorization). Send this to satisfy a prior `402`. Only relevant to anonymous, over-quota callers.\n","schema":{"type":"string"}}],"responses":{"200":{"description":"The answer. JSON by default; HTML when `format=html`. On a settled x402 payment,\nan `X-PAYMENT-RESPONSE` header carries the receipt and the body gains a `payment` object.\n","headers":{"X-PAYMENT-RESPONSE":{"description":"x402 settlement receipt (present only when a payment settled).","schema":{"type":"string"}},"X-Billing-Charged":{"description":"`true` when this answer cost an uncached upstream fetch (HTML responses only).","schema":{"type":"string"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/DataResponse"}},"text/html":{"schema":{"type":"string","description":"A self-contained HTML chart (inline styles, data-URI images, no scripts). Returned when `format=html`."}}}},"400":{"$ref":"#/components/responses/UnknownParameter"},"402":{"description":"Payment required (x402). Returned to anonymous, over-quota callers when the server\nhas x402 configured. The body carries the payment terms; reissue with an `X-PAYMENT`\nheader. Also returned if a supplied payment is invalid, already used, or fails to settle.\n","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"description":"x402 payment-terms body (accepts amount, asset, network, payTo, resource, …)."}}}},"429":{"$ref":"#/components/responses/QuotaExceeded"},"502":{"description":"Upstream source failed while fetching the data. Distinct from an honest empty.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/usage":{"get":{"tags":["Account"],"operationId":"usage","summary":"Current usage and limits","description":"Usage counts and remaining allowance for the caller (by key, or by IP on the free tier). Not metered. Takes no query parameters.","responses":{"200":{"description":"Usage status.","content":{"application/json":{"schema":{"type":"object","properties":{"usage":{"type":"object","additionalProperties":true,"description":"Tier, counts used, and remaining allowance."},"meta":{"$ref":"#/components/schemas/Meta"}}}}}},"400":{"$ref":"#/components/responses/UnknownParameter"}}}},"/v1/account/visibility":{"post":{"tags":["Account"],"operationId":"setAccountVisibility","summary":"Set the default visibility for new boards (paid tiers)","description":"Sets the account-wide default visibility applied to newly created boards. Only meaningful\non paid tiers — free-tier boards are always public regardless of this setting.\n","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["visibility"],"properties":{"visibility":{"type":"string","enum":["public","private"]}}}}}},"responses":{"200":{"description":"Updated.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Ok"}}}},"400":{"description":"Invalid visibility, or caller is a free/anonymous account.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Ok"}}}}}}},"/v1/dashboards":{"get":{"tags":["Boards"],"operationId":"listBoards","summary":"List your boards","responses":{"200":{"description":"Your boards, newest first.","content":{"application/json":{"schema":{"type":"object","properties":{"dashboards":{"type":"array","items":{"$ref":"#/components/schemas/BoardSummary"}},"meta":{"$ref":"#/components/schemas/Meta"}}}}}}}},"post":{"tags":["Boards"],"operationId":"createBoard","summary":"Create a board","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"title":{"type":"string","description":"Board title (defaults to \"Untitled\", trimmed to 120 chars).","example":"Labour market"}}}}}},"responses":{"200":{"description":"The created board.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Board"}}}}}}},"/v1/dashboards/{did}":{"parameters":[{"$ref":"#/components/parameters/BoardId"}],"get":{"tags":["Boards"],"operationId":"getBoard","summary":"Get one board","description":"The owner sees their board fully. Anyone may read a **public** board (`is_owner: false`).\nA private board owned by someone else returns `404` — its existence is never leaked.\n","responses":{"200":{"description":"The board and its items.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Board"}}}},"404":{"description":"No such board, or it is private and not yours.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"post":{"tags":["Boards"],"operationId":"updateBoard","summary":"Rename a board and/or set its visibility","description":"Send `title`, `visibility`, or both. A free-tier board stays public regardless of `visibility`.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"title":{"type":"string"},"visibility":{"type":"string","enum":["public","private"]}}}}}},"responses":{"200":{"description":"Updated.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Ok"}}}},"404":{"description":"Not yours, or not found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Ok"}}}}}},"delete":{"tags":["Boards"],"operationId":"deleteBoard","summary":"Delete a board","responses":{"200":{"description":"Deleted.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Deleted"}}}},"404":{"description":"Not yours, or not found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Deleted"}}}}}}},"/v1/dashboards/{did}/fork":{"parameters":[{"$ref":"#/components/parameters/BoardId"}],"post":{"tags":["Boards"],"operationId":"forkBoard","summary":"Fork a public board into your own","description":"Copies the QUESTIONS (prompt + params) of a public board — or your own — into a new board. Never copies data.","responses":{"200":{"description":"The new board (id, title, item count).","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"title":{"type":"string"},"items":{"type":"integer","description":"Number of items copied."}}}}}},"404":{"description":"Board not found or not public.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/dashboards/{did}/items":{"parameters":[{"$ref":"#/components/parameters/BoardId"}],"post":{"tags":["Boards"],"operationId":"addBoardItem","summary":"Add an answer-card to a board","description":"Stores the **question** (prompt + resolved params), never the data. On load the app\nre-runs `/v1/data` for each item, so a board is always live.\n","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["prompt"],"properties":{"prompt":{"type":"string","description":"The question to save (trimmed to 500 chars).","example":"unemployment rate in Japan"},"params":{"type":"object","description":"Resolved parameters for the prompt (start, end, dataset, …).","additionalProperties":true}}}}}},"responses":{"200":{"description":"The created item.","content":{"application/json":{"schema":{"type":"object","properties":{"item_id":{"type":"integer"},"position":{"type":"integer"}}}}}},"404":{"description":"No such board (or not yours), or an empty prompt.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/dashboards/{did}/items/{iid}":{"parameters":[{"$ref":"#/components/parameters/BoardId"},{"name":"iid","in":"path","required":true,"description":"The item id (integer).","schema":{"type":"integer"}}],"delete":{"tags":["Boards"],"operationId":"deleteBoardItem","summary":"Remove an item from a board","responses":{"200":{"description":"Deleted.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Deleted"}}}},"404":{"description":"Not found, or not yours.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Deleted"}}}}}}},"/v1/feed":{"get":{"tags":["Boards"],"operationId":"feed","summary":"The public feed","description":"Recent **public** boards from other users, for inspiration. Free-tier boards are always\nhere; paid boards only if their owner made them public. Authors are anonymised — no key\nor IP is ever exposed. Never your own boards.\n","responses":{"200":{"description":"Public boards, newest first.","content":{"application/json":{"schema":{"type":"object","properties":{"feed":{"type":"array","items":{"$ref":"#/components/schemas/FeedItem"}},"meta":{"$ref":"#/components/schemas/Meta"}}}}}}}}},"/v1/export":{"post":{"tags":["Export"],"operationId":"exportChart","summary":"Export a chart as an image","description":"Rasterises a client-supplied SVG chart to PNG (or returns watermarked SVG if no\nrasteriser is installed). The incoming SVG is **scrubbed** first; anything unsafe\n(scripts, external references, event handlers) is rejected with `400`.\n\nThe **watermark is decided server-side by the caller's tier** — free-tier exports always\ncarry the StackData mark and the client cannot opt out.\n","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["svg"],"properties":{"svg":{"type":"string","description":"The chart as an SVG string, with colours inlined."},"title":{"type":"string","description":"Title rendered above the chart.","default":"chart"},"theme":{"type":"object","description":"Optional theme tokens (background, ink, …).","additionalProperties":true}}}}}},"responses":{"200":{"description":"The image. PNG when a rasteriser is available, otherwise watermarked SVG.","content":{"image/png":{"schema":{"type":"string","format":"binary"}},"image/svg+xml":{"schema":{"type":"string"}}}},"400":{"description":"The SVG was invalid or unsafe and was rejected before rendering.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/health":{"get":{"tags":["System"],"operationId":"health","summary":"Liveness and model state","responses":{"200":{"description":"Health snapshot.","content":{"application/json":{"schema":{"type":"object","properties":{"datasets":{"$ref":"#/components/schemas/TypedCount"},"embedded":{"$ref":"#/components/schemas/TypedCount"},"uptime_s":{"type":"integer"},"model_loaded":{"type":"boolean","description":"Whether the embedding model is resident (first search loads it)."}}}}}}}}},"/v1/cache":{"get":{"tags":["System"],"operationId":"cache","summary":"Cache and learned-coverage state","description":"Cache hit-rate and learned coverage — exposed rather than guessed, because hit rate drives the cost model.","responses":{"200":{"description":"Cache state.","content":{"application/json":{"schema":{"type":"object","properties":{"cache":{"type":"object","additionalProperties":true},"learned_coverage":{"type":"object","additionalProperties":true},"meta":{"$ref":"#/components/schemas/Meta"}}}}}}}}},"/v1/auth/providers":{"get":{"tags":["Auth"],"operationId":"authProviders","summary":"Which sign-in options are available","description":"Email is always on; each OAuth provider appears only when its server-side secrets are configured.","responses":{"200":{"description":"Available options.","content":{"application/json":{"schema":{"type":"object","properties":{"email":{"type":"boolean"},"google":{"type":"boolean"},"github":{"type":"boolean"}},"example":{"email":true,"google":true,"github":false}}}}}}}},"/v1/auth/register":{"post":{"tags":["Auth"],"operationId":"authRegister","summary":"Create an account with email + password","description":"Mints a free-tier account and starts a session (cookie + `session` token in the body). A verification token is created; on a non-https host it is returned as `verify_token_dev` since no mailer is wired.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["email","password"],"properties":{"email":{"type":"string","format":"email"},"password":{"type":"string","minLength":8},"nickname":{"type":"string","maxLength":40}}}}}},"responses":{"200":{"description":"Account created and signed in.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AuthSession"}}}},"400":{"description":"Invalid email or too-short password.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"An account with this email already exists.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/auth/login":{"post":{"tags":["Auth"],"operationId":"authLogin","summary":"Sign in with email + password","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["email","password"],"properties":{"email":{"type":"string","format":"email"},"password":{"type":"string"}}}}}},"responses":{"200":{"description":"Signed in.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AuthSession"}}}},"401":{"description":"Wrong email or password.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/auth/logout":{"post":{"tags":["Auth"],"operationId":"authLogout","summary":"Sign out (destroys the current session)","responses":{"200":{"description":"Signed out.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Ok"}}}}}}},"/v1/auth/session":{"get":{"tags":["Auth"],"operationId":"authWhoami","summary":"The current user, or null","responses":{"200":{"description":"Whoami.","content":{"application/json":{"schema":{"type":"object","properties":{"user":{"$ref":"#/components/schemas/User"}}}}}}}}},"/v1/auth/{provider}/start":{"get":{"tags":["Auth"],"operationId":"authOAuthStart","summary":"Begin an OAuth flow","description":"Sets a CSRF state cookie and 302-redirects to the provider. Open this in a top-level browser navigation, not a fetch.","parameters":[{"name":"provider","in":"path","required":true,"schema":{"type":"string","enum":["google","github"]}}],"responses":{"302":{"description":"Redirect to the provider (or back to the app with an error if unconfigured)."}}}},"/v1/auth/{provider}/callback":{"get":{"tags":["Auth"],"operationId":"authOAuthCallback","summary":"OAuth redirect target (provider calls this)","description":"Verifies state, exchanges the code, upserts the account, starts a session, and 302-redirects to the app with the session token in the URL fragment.","parameters":[{"name":"provider","in":"path","required":true,"schema":{"type":"string","enum":["google","github"]}},{"name":"code","in":"query","required":false,"schema":{"type":"string"}},{"name":"state","in":"query","required":false,"schema":{"type":"string"}}],"responses":{"302":{"description":"Redirect back to the app (with `#session=…` on success or `#error=…` on failure)."}}}},"/v1/me":{"get":{"tags":["Account"],"operationId":"getMe","summary":"The signed-in user's profile","security":[{"SessionCookie":[]},{"BearerAuth":[]}],"responses":{"200":{"description":"The user.","content":{"application/json":{"schema":{"type":"object","properties":{"user":{"$ref":"#/components/schemas/User"}}}}}},"401":{"$ref":"#/components/responses/NotSignedIn"}}},"post":{"tags":["Account"],"operationId":"updateMe","summary":"Update nickname and/or default board visibility","security":[{"SessionCookie":[]},{"BearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"nickname":{"type":"string","maxLength":40},"default_visibility":{"type":"string","enum":["public","private"]}}}}}},"responses":{"200":{"description":"Updated; returns the user.","content":{"application/json":{"schema":{"type":"object","properties":{"user":{"$ref":"#/components/schemas/User"}}}}}},"401":{"$ref":"#/components/responses/NotSignedIn"}}},"delete":{"tags":["Account"],"operationId":"deleteMe","summary":"Delete the account (confirm-gated, irreversible)","security":[{"SessionCookie":[]},{"BearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["confirm"],"properties":{"confirm":{"type":"string","description":"Must exactly equal the account's nickname."}}}}}},"responses":{"200":{"description":"Deleted.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Deleted"}}}},"400":{"description":"Confirmation did not match the nickname.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/NotSignedIn"}}}},"/v1/me/password":{"post":{"tags":["Account"],"operationId":"changePassword","summary":"Set or change the password","description":"If a password already exists, `current_password` must be correct. OAuth-only accounts set a first password with no current one.","security":[{"SessionCookie":[]},{"BearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["new_password"],"properties":{"current_password":{"type":"string"},"new_password":{"type":"string","minLength":8}}}}}},"responses":{"200":{"description":"Updated.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Ok"}}}},"400":{"description":"New password too short.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Current password is wrong.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/me/avatar/shuffle":{"post":{"tags":["Account"],"operationId":"shuffleAvatar","summary":"Re-roll the generated avatar","security":[{"SessionCookie":[]},{"BearerAuth":[]}],"responses":{"200":{"description":"New seed.","content":{"application/json":{"schema":{"type":"object","properties":{"avatar_seed":{"type":"string"}}}}}},"401":{"$ref":"#/components/responses/NotSignedIn"}}}},"/v1/me/avatar/reset":{"post":{"tags":["Account"],"operationId":"resetAvatar","summary":"Reset the avatar to the stable default (user id seed)","security":[{"SessionCookie":[]},{"BearerAuth":[]}],"responses":{"200":{"description":"Reset seed.","content":{"application/json":{"schema":{"type":"object","properties":{"avatar_seed":{"type":"string"}}}}}},"401":{"$ref":"#/components/responses/NotSignedIn"}}}},"/v1/me/api-key":{"get":{"tags":["Account"],"operationId":"getMyKey","summary":"Reveal the account's API key","security":[{"SessionCookie":[]},{"BearerAuth":[]}],"responses":{"200":{"description":"The key.","content":{"application/json":{"schema":{"type":"object","properties":{"api_key":{"type":"string"},"tier":{"type":"string"}}}}}},"401":{"$ref":"#/components/responses/NotSignedIn"}}}},"/v1/me/api-key/rotate":{"post":{"tags":["Account"],"operationId":"rotateMyKey","summary":"Issue a new API key (old one stops working immediately)","security":[{"SessionCookie":[]},{"BearerAuth":[]}],"responses":{"200":{"description":"The new key; boards move with it.","content":{"application/json":{"schema":{"type":"object","properties":{"api_key":{"type":"string"}}}}}},"401":{"$ref":"#/components/responses/NotSignedIn"}}}},"/v1/me/sessions":{"get":{"tags":["Account"],"operationId":"listMySessions","summary":"Active sessions for this account","security":[{"SessionCookie":[]},{"BearerAuth":[]}],"responses":{"200":{"description":"Sessions (the current one flagged).","content":{"application/json":{"schema":{"type":"object","properties":{"sessions":{"type":"array","items":{"type":"object","properties":{"created_at":{"type":"string"},"last_seen_at":{"type":"string"},"user_agent":{"type":"string"},"current":{"type":"boolean"}}}}}}}}},"401":{"$ref":"#/components/responses/NotSignedIn"}}}},"/v1/me/sessions/revoke-others":{"post":{"tags":["Account"],"operationId":"revokeOtherSessions","summary":"Sign out every other device","security":[{"SessionCookie":[]},{"BearerAuth":[]}],"responses":{"200":{"description":"Count revoked.","content":{"application/json":{"schema":{"type":"object","properties":{"revoked":{"type":"integer"}}}}}},"401":{"$ref":"#/components/responses/NotSignedIn"}}}},"/v1/avatar/{seed}":{"get":{"tags":["Account"],"operationId":"avatar","summary":"Generated mesh avatar for a seed","description":"A deterministic SVG gradient-mesh avatar — a pure function of the seed, cached immutably. Point an `<img>` at it. No auth.","parameters":[{"name":"seed","in":"path","required":true,"schema":{"type":"string"}},{"name":"size","in":"query","required":false,"schema":{"type":"integer","default":160,"minimum":24,"maximum":512}}],"responses":{"200":{"description":"The avatar.","content":{"image/svg+xml":{"schema":{"type":"string"}}}}}}}},"components":{"securitySchemes":{"ApiKeyAuth":{"type":"apiKey","in":"header","name":"X-API-Key","description":"Your StackData API key. Optional — omit it to use the free tier (identified by IP)."},"SessionCookie":{"type":"apiKey","in":"cookie","name":"sd_session","description":"The httpOnly session cookie set at sign-in (production, same-site)."},"BearerAuth":{"type":"http","scheme":"bearer","description":"The session token returned in the login body, sent as `Authorization: Bearer <token>` (the app's cross-origin path)."}},"parameters":{"BoardId":{"name":"did","in":"path","required":true,"description":"Board id, of the form `db_` followed by hex.","schema":{"type":"string","pattern":"^db_[0-9a-f]+$"},"example":"db_1a2b3c4d5e"}},"responses":{"UnknownParameter":{"description":"An unknown query parameter was supplied. Unknown parameters are **rejected, never\nsilently ignored** — the error names the offenders and lists what is allowed.\n","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"allowed":{"type":"array","items":{"type":"string"}},"note":{"type":"string"}},"example":{"error":"unknown parameter(s): limitt","allowed":["country","kind","limit","open_only","q"],"note":"unknown parameters are rejected, never silently ignored"}}}}},"QuotaExceeded":{"description":"Daily free-tier limit reached (or credits exhausted). The body includes current usage.","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"properties":{"usage":{"type":"object","additionalProperties":true}}}}}},"NotSignedIn":{"description":"No valid session (cookie or Bearer token).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"schemas":{"User":{"type":"object","description":"The signed-in user. Never includes the password hash or raw session.","properties":{"id":{"type":"string"},"nickname":{"type":"string"},"email":{"type":"string","description":"May be null for an OAuth-only account."},"email_verified":{"type":"boolean"},"avatar_seed":{"type":"string","description":"Seed for GET /v1/avatar/{seed}."},"has_password":{"type":"boolean"},"tier":{"type":"string"},"default_visibility":{"type":"string","enum":["public","private"]},"providers":{"type":"array","items":{"type":"string","enum":["google","github"]},"description":"Linked OAuth providers."},"created_at":{"type":"string"}}},"AuthSession":{"type":"object","description":"Returned by register/login. The `session` token also arrives as an httpOnly cookie.","properties":{"user":{"$ref":"#/components/schemas/User"},"session":{"type":"string","description":"Bearer token — send as Authorization for cross-origin calls."},"verify_token_dev":{"type":"string","description":"Dev-only — the email-verification token, present on non-https hosts since no mailer is wired."}}},"TypedCount":{"type":"object","description":"A count that states whether it is exact — never a bare integer.","required":["value","exact"],"properties":{"value":{"type":"integer"},"exact":{"type":"boolean"}},"example":{"value":2807,"exact":true}},"Meta":{"type":"object","description":"Attached to every JSON response. `harvest` maps each source id to its last successful harvest timestamp.","properties":{"harvest":{"type":"object","additionalProperties":{"type":"string"}},"api_version":{"type":"string"}},"example":{"harvest":{"ilo":"2026-08-10 03:14:22","wb":"2026-08-10 03:15:01"},"api_version":"0.1"}},"Error":{"type":"object","properties":{"error":{"type":"string"},"note":{"type":"string"}},"required":["error"],"example":{"error":"no dataset 'x'","note":"absent is not an empty success"}},"Ok":{"type":"object","properties":{"ok":{"type":"boolean"}}},"Deleted":{"type":"object","properties":{"deleted":{"type":"boolean"}}},"SearchResponse":{"type":"object","properties":{"query":{"type":"string"},"returned":{"$ref":"#/components/schemas/TypedCount"},"applied_filters":{"type":"object","description":"Echo of the filters that actually ran (a filter that could not run is a 400, never a silent drop).","additionalProperties":true},"results":{"type":"array","items":{"type":"object","description":"A matched dataset (id, title, publisher, score, …).","additionalProperties":true}},"meta":{"$ref":"#/components/schemas/Meta"}}},"DataResponse":{"type":"object","description":"Status-tagged answer. `status` names the outcome — success (`ok`, `ok_correlation`,\n`ok_coverage`) or an honest non-answer (`empty`, `no_candidates`, `dsd_unavailable`,\n`multi_concept_unparsed`, `language_unsupported`, …). `billing.charged` says whether an\nuncached fetch was paid for. `interpreted_as` appears when an LLM rewrote the prompt.\n","properties":{"status":{"type":"string"},"billing":{"type":"object","properties":{"charged":{"type":"boolean"}}},"coverage":{"type":"object","additionalProperties":true},"interpreted_as":{"type":"object","additionalProperties":true},"payment":{"type":"object","description":"Present only on the x402 path — settlement outcome for the answer.","additionalProperties":true},"meta":{"$ref":"#/components/schemas/Meta"}},"additionalProperties":true},"CatalogResponse":{"type":"object","properties":{"returned":{"$ref":"#/components/schemas/TypedCount"},"total":{"$ref":"#/components/schemas/TypedCount"},"next_cursor":{"type":["string","null"],"description":"Pass back as `cursor` for the next page; `null` means exhausted (explicitly, never by absence)."},"applied_filters":{"type":"object","additionalProperties":true},"datasets":{"type":"array","items":{"type":"object","description":"One catalog record as plain values: id, source, title {value, lang, machine}, description, publisher, resource_kind, cadence, status, temporal, formats, licence {spdx, label, is_open}, endpoint, coverage (country list, when observed), first/last seen.\n","additionalProperties":true}},"meta":{"$ref":"#/components/schemas/Meta"}}},"Dataset":{"type":"object","properties":{"id":{"type":"string"},"title":{"type":"string"},"title_lang":{"type":"string"},"description":{"type":"string"},"resource_kind":{"type":"string"},"publisher":{"type":"object","properties":{"name":{"type":"string"},"publisher_type":{"type":"string"}}},"cadence":{"type":"string"},"status":{"type":"string"},"native_theme":{"type":"string"},"access_rights":{"type":"string"},"distribution":{"type":"object","properties":{"access_service":{"type":"string"},"conforms_to":{"type":"string"},"format":{"type":"string"},"licence":{"type":"object","additionalProperties":true}}},"first_seen_at":{"type":"string"},"last_seen_at":{"type":"string"},"source":{"type":"string"},"meta":{"$ref":"#/components/schemas/Meta"}}},"CoverageResponse":{"type":"object","properties":{"by":{"type":"string"},"buckets":{"type":"array","items":{"type":"object","properties":{"key":{"type":"string"},"datasets":{"$ref":"#/components/schemas/TypedCount"}}}},"total":{"$ref":"#/components/schemas/TypedCount"},"meta":{"$ref":"#/components/schemas/Meta"}}},"BoardSummary":{"type":"object","properties":{"id":{"type":"string"},"title":{"type":"string"},"updated_at":{"type":"string"},"items":{"type":"integer","description":"Number of items on the board."},"visibility":{"type":"string","enum":["public","private"]},"public":{"type":"boolean","description":"Effective public state (a forced-public free board is public even if visibility says otherwise)."},"forced_public":{"type":"boolean","description":"True when the free-tier rule forces this board public."}}},"Board":{"type":"object","properties":{"id":{"type":"string"},"title":{"type":"string"},"updated_at":{"type":"string"},"visibility":{"type":"string","enum":["public","private"]},"public":{"type":"boolean"},"forced_public":{"type":"boolean"},"is_owner":{"type":"boolean"},"author":{"type":"string","description":"Anonymised author handle (user-<hash>)."},"items":{"type":"array","items":{"$ref":"#/components/schemas/BoardItem"}}}},"BoardItem":{"type":"object","properties":{"item_id":{"type":"integer"},"position":{"type":"integer"},"prompt":{"type":"string"},"params":{"type":"object","additionalProperties":true}}},"FeedItem":{"type":"object","properties":{"id":{"type":"string"},"title":{"type":"string"},"author":{"type":"string","description":"Anonymised handle — never a key or IP."},"updated_at":{"type":"string"},"items":{"type":"integer"},"preview":{"type":"string","description":"First prompt on the board, as a teaser."}}}}}}