{"openapi":"3.1.0","info":{"title":"Personal Info Hub API","version":"1.0.0","summary":"Read and add to one person's collected library (links, posts, videos, transcripts) and English-practice notes.","description":"Personal Info Hub (PIH) is a private library: everything its owner collects from the web, X, Xiaohongshu, YouTube, Bilibili and more, with fetched text, transcripts, summaries and stored media.\n\nAuthentication: send an API key as `Authorization: Bearer pih_...` (or `x-api-key: pih_...`). The owner creates keys on the hub's System page; each key carries scopes and a per-minute rate limit, and every call is logged. Rate limit headers: `x-ratelimit-limit`, `x-ratelimit-remaining`, `x-ratelimit-reset`.\n\nScopes: `items:read` (list and read items), `media:read` (download media files by their stable address), `search:read` (search), `collect:write` (add links), `notes:write` (star items, mark or clear 稍后读 / 稍后看 (read or watch later), set an item's note), `english:read` (English-practice words and records), `songbook:read` / `songbook:write` (the 点歌本 songs and their streaming links, and 图图乐谱: each song's sheet music; write also adds songs), `things:read` / `things:write` (物品: search products, record what the owner wants to buy), `restaurants:read` / `restaurants:write` (餐厅: restaurants to try or been to, and each visit), `books:read` / `books:write` (读书: the owner's WeRead books, highlights, thoughts and reading time; write only asks for a sync now), `people:read` / `people:write` (人物: people in the library, interviewed, hosting, writing or written about, with their items), `recipes:read` / `recipes:write` (菜谱: dishes worked out from collected posts and videos, with ingredients, steps and the owner's 想做 / 做过 marks; write marks 想做, logs 做过, plans meals, adds grocery lines, turns saved posts into recipes and writes your own recipe version), `todos:read` / `todos:write` (待办: the owner's todo list; add, change, complete or drop a todo), `card_notes:read` / `card_notes:write` (卡片笔记: the owner's 卡片盒 (card box) of short tagged notes; write adds and changes cards and takes one back, nothing deletes), `tutu:read` (图图: the owner's child's photos and videos from the growth timeline, his daycare days (arrival, pick-up, naps), the family's home logs (sleep, wake-up, meals, poops, photos) and his growth measurements' numbers, read-only), `tutu:write` (图图's home logs: log a tap, add its photos, change it or take it back; nothing deletes), `health:read` / `health:write` (图图健康: his health records, vaccines, attachments and the notes on measurements; write adds measurements and adds or changes records. Sensitive: never part of every-app access, ticked by name), `notify:write` (send the owner a notification, by default to the dev bot), `tikhub:call` (make a TikHub request through the hub, on the owner's TikHub balance and budget), `renders:read` / `renders:write` (渲染: Blender renders on the home machines, queued with the rest of the owner's background work), `feedback:read` (list and resolve agent feedback; sending it needs no scope), `admin` (everything). Nothing in this API deletes data (taking back a 想做 or a planned meal only clears that mark).\n\nMedia (images, video, audio) comes two ways, never as storage keys: `url`, a signed link that expires after one hour and is fetched without the key (for a tool that cannot send it); and `stableUrl` (`/items/{id}/media/{mediaId}`), which never changes and may be stored, fetched with the key and scope `media:read`. Times are ISO 8601 UTC.\n\nIds: `id` is the item's short public id (8 characters, the same one in the hub's item URLs); an older id an item was known by still resolves.\n\nAnswers: a success is the object itself, no envelope. A paged list is `{\"items\": [...], \"nextCursor\": \"...\"}`: send `nextCursor` back as `cursor` until it is null.\n\nErrors: `{\"error\": \"<code>\", \"message\": \"...\"}`; branch on `error` (stable snake_case), show `message` to people. Each operation lists its codes. A malformed parameter or body names the field in `message`; a body that fails its schema also lists every issue in `details`.\n\nCallback webhooks (service callers): the owner can register an HTTPS endpoint for a key. Items that key collects through `POST /collect` then produce signed events: `item.collected` (fetched), then exactly one of `item.ready` (all processing done: for a video, the file is stored and the transcript is final) or `item.failed` (with `reason`). Deliveries follow Standard Webhooks (https://www.standardwebhooks.com): headers `webhook-id` (the event id; deduplicate on it), `webhook-timestamp` (unix seconds) and `webhook-signature` (`v1,<base64 HMAC-SHA256 of id.timestamp.body>` with the endpoint's `whsec_` secret, space-separated during a secret rotation). Answer 2xx within 10 seconds; anything else is retried for about two days (1 min, 5 min, 15 min, 1 h, 3 h, 6 h, 12 h, 24 h). Payloads carry ids and links only: fetch content, timeline and signed media with the key. Without an endpoint, poll `GET /items/{id}` until `processing.status` is `ready` or `failed`.","contact":{"name":"Personal Info Hub owner"}},"servers":[{"url":"https://pih-api.jiajun.fyi"}],"security":[{"apiKey":[]}],"tags":[{"name":"items"},{"name":"collect"},{"name":"connectors"},{"name":"english"},{"name":"songbook"},{"name":"things"},{"name":"restaurants"},{"name":"books"},{"name":"people"},{"name":"recipes"},{"name":"feedback"},{"name":"todos"},{"name":"card_notes"},{"name":"tutu"},{"name":"notify"},{"name":"tikhub"},{"name":"renders"}],"paths":{"/search":{"get":{"operationId":"search","tags":["items"],"summary":"Search the library","description":"Search over titles, authors, summaries, bodies and notes, every transcript to its end (by its words and, unless `exact=true`, by meaning), plus text derived from items (summaries, translations, formatted transcripts); a pasted link finds the item it points at. Returns the best matches (no paging): results holding the query verbatim (`match.kind: exact`) first, then looser ones (`fuzzy`, and transcript passages matched by meaning, `semantic`), most relevant first. Every result says where it matched (`match.field`) with the original text around it (`match.snippet`), and for a transcript hit when it is said (`match.start` / `match.end`, seconds). `exact=true` returns only verbatim matches. Each result has a `type`; today every result is an `item`, other kinds (people, topics) may be added later, so skip types you do not know.\n\nRequires scope `search:read`.","x-scope":"search:read","security":[{"apiKey":[]}],"parameters":[{"name":"q","in":"query","required":true,"description":"Search words. Latin words match by prefix (and, in derived text, by word stem); other scripts match as substrings.","schema":{"type":"string","minLength":1,"maxLength":200}},{"name":"exact","in":"query","required":false,"description":"`true`: only results that hold the query verbatim (`match.kind: exact`; no prefixes, stems or parts of longer words): `3DS` will not find `3D printing` or `3dscene`.","schema":{"type":"boolean"}},{"name":"source","in":"query","required":false,"description":"Only this source, e.g. `twitter_bookmark`, `xiaohongshu`, `youtube`.","schema":{"type":"string"}},{"name":"include","in":"query","required":false,"description":"`content`: every item also carries its own text (`content`: body, note, how to read it), so one call reads a whole page of posts and articles. Transcripts are not included: `transcript` on each item says whether one exists and how long it is. At most 25 items a page with it.","schema":{"type":"string","enum":["content"]}},{"name":"limit","in":"query","required":false,"description":"Page size, 1-100 (default 20).","schema":{"default":20,"type":"integer","minimum":1,"maximum":100}}],"responses":{"200":{"description":"Matches.","content":{"application/json":{"schema":{"type":"object","properties":{"query":{"type":"string"},"results":{"type":"array","items":{"$ref":"#/components/schemas/SearchResult"}}},"required":["query","results"]}}}},"400":{"description":"`q` is missing (`q_required`), or another parameter is malformed (`invalid_query`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Missing, unknown, revoked or expired API key (`api_key_required`, `invalid_api_key`, `api_key_revoked`, `api_key_expired`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"The key lacks the scope this operation needs (`insufficient_scope`; `required` names it).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Over the key's per-minute limit (`rate_limited`). Wait for `Retry-After` seconds.","headers":{"Retry-After":{"description":"Seconds until the key may call again.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/items":{"get":{"operationId":"listItems","tags":["items"],"summary":"List items","description":"Items newest collected first, from every platform unless `source` narrows it. Page with `cursor`; `total` is how many items all pages hold. `since=7d` is the last week; add `include=content` to read the items in the same call. `readLater=true` lists the 稍后读 / 稍后看 (read or watch later) queue instead: articles, posts and videos together, only marked items, newest mark first (each item's `format` tells a video from an article), and `since` / `until` / `cursor` then mean when they were marked.\n\nRequires scope `items:read`.","x-scope":"items:read","security":[{"apiKey":[]}],"parameters":[{"name":"source","in":"query","required":false,"description":"Only this source code.","schema":{"type":"string"}},{"name":"tag","in":"query","required":false,"description":"Only items with this tag.","schema":{"type":"string"}},{"name":"author","in":"query","required":false,"description":"Only items by this author name or handle.","schema":{"type":"string"}},{"name":"since","in":"query","required":false,"description":"Collected at or after this time: an ISO date or date-time, or a span back from now (`24h`, `7d`, `2w`).","schema":{"type":"string"}},{"name":"until","in":"query","required":false,"description":"Collected before this time: an ISO date or date-time, or a span back from now.","schema":{"type":"string"}},{"name":"starred","in":"query","required":false,"description":"`true`: only items the owner starred.","schema":{"type":"boolean"}},{"name":"watched","in":"query","required":false,"description":"`true`: only items the owner watched (10+ minutes, or 80% of a shorter runtime).","schema":{"type":"boolean"}},{"name":"readLater","in":"query","required":false,"description":"`true`: only items marked 稍后读 / 稍后看 (read or watch later; articles and videos alike), ordered by when they were marked, newest first.","schema":{"type":"boolean"}},{"name":"state","in":"query","required":false,"description":"`processing`, `ready` or `error`.","schema":{"type":"string","enum":["processing","ready","error"]}},{"name":"include","in":"query","required":false,"description":"`content`: every item also carries its own text (`content`: body, note, how to read it), so one call reads a whole page of posts and articles. Transcripts are not included: `transcript` on each item says whether one exists and how long it is. At most 25 items a page with it.","schema":{"type":"string","enum":["content"]}},{"name":"cursor","in":"query","required":false,"description":"Opaque page cursor: `nextCursor` from the previous page.","schema":{"type":"string"}},{"name":"limit","in":"query","required":false,"description":"Page size, 1-100 (default 20).","schema":{"default":20,"type":"integer","minimum":1,"maximum":100}}],"responses":{"200":{"description":"One page of items.","content":{"application/json":{"schema":{"type":"object","properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/Item"}},"nextCursor":{"type":["string","null"],"description":"Pass as `cursor` for the next page; null on the last page."},"total":{"type":"integer","description":"Items matching the filters on every page together (`until` counts, the page `cursor` does not): paging to the end returns exactly this many."}},"required":["items","nextCursor","total"]}}}},"400":{"description":"A time parameter is malformed (`invalid_time`), or another parameter is (`invalid_query`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Missing, unknown, revoked or expired API key (`api_key_required`, `invalid_api_key`, `api_key_revoked`, `api_key_expired`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"The key lacks the scope this operation needs (`insufficient_scope`; `required` names it).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Over the key's per-minute limit (`rate_limited`). Wait for `Retry-After` seconds.","headers":{"Retry-After":{"description":"Seconds until the key may call again.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/shots":{"get":{"operationId":"listShots","tags":["items"],"summary":"Videos laid out as shots (切片)","description":"Each video cut into its shots (one cut to the next), each with a frame at the video's own resolution and a small copy, the words said over it, and what a recognizer saw (scene, action, setting, on-screen text, subjects, tags). `sentences` is the speech in order, each pointing at its shot: the same data read as an article. Two ways to show it: every shot side by side (平铺), or the sentences as paragraphs with their shots' frames beside them (文章). To play a shot, load `video.url` and seek to its `startS`. Shots are made on a local machine and uploaded; a video nobody has cut yet is simply absent. Without `items`: the 10 videos cut most recently.\n\nRequires scope `items:read`.","x-scope":"items:read","security":[{"apiKey":[]}],"parameters":[{"name":"items","in":"query","required":false,"description":"Comma-separated item ids or 短 IDs, at most 20. An item with several cut videos returns each.","schema":{"type":"string"}}],"responses":{"200":{"description":"The videos, in the order asked.","content":{"application/json":{"schema":{"type":"object","properties":{"videos":{"type":"array","items":{"$ref":"#/components/schemas/ShotVideo"}}},"required":["videos"]}}}},"400":{"description":"More than 20 items (`too_many_items`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Missing, unknown, revoked or expired API key (`api_key_required`, `invalid_api_key`, `api_key_revoked`, `api_key_expired`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"The key lacks the scope this operation needs (`insufficient_scope`; `required` names it).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Over the key's per-minute limit (`rate_limited`). Wait for `Retry-After` seconds.","headers":{"Retry-After":{"description":"Seconds until the key may call again.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/items/{id}":{"get":{"operationId":"getItem","tags":["items"],"summary":"Get one item","description":"The item's metadata, `transcript` (whether its transcript can be read, and why not) and `processing`: whether it is still being processed, ready or failed (the same verdict callback webhooks report). Body text, transcript text and media are in `/items/{id}/content`.\n\nRequires scope `items:read`.","x-scope":"items:read","security":[{"apiKey":[]}],"parameters":[{"name":"id","in":"path","required":true,"description":"Item id as returned by list, search or collect. An item's hubUrl or its path (`Ab3dE6gH/<slug>`), URL-encoded, also works: only the 8-character id in it counts, so an old slug still finds the item.","schema":{"type":"string"}}],"responses":{"200":{"description":"The item.","content":{"application/json":{"schema":{"type":"object","properties":{"item":{"$ref":"#/components/schemas/ItemDetail"}},"required":["item"]}}}},"401":{"description":"Missing, unknown, revoked or expired API key (`api_key_required`, `invalid_api_key`, `api_key_revoked`, `api_key_expired`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"The key lacks the scope this operation needs (`insufficient_scope`; `required` names it).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"No item with this id (`item_not_found`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Over the key's per-minute limit (`rate_limited`). Wait for `Retry-After` seconds.","headers":{"Retry-After":{"description":"Seconds until the key may call again.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/items/{id}/content":{"get":{"operationId":"getItemContent","tags":["items"],"summary":"Get an item's content","description":"Full body text, translation, summary, the owner's note, the transcript as timed sentences (`transcript.segments`, each with its start and end second and its `paragraph`, numbered from 0) and media as signed links valid for one hour (do not store those) plus each file's `stableUrl`, which never changes and may be stored (fetch it with a key holding `media:read`). `readAs` says how to take the item in: for a video or episode with real speech, the timeline reads it with the publisher's chapters.\n\nRequires scope `items:read`.","x-scope":"items:read","security":[{"apiKey":[]}],"parameters":[{"name":"id","in":"path","required":true,"description":"Item id as returned by list, search or collect. An item's hubUrl or its path (`Ab3dE6gH/<slug>`), URL-encoded, also works: only the 8-character id in it counts, so an old slug still finds the item.","schema":{"type":"string"}}],"responses":{"200":{"description":"The content.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ItemContent"}}}},"401":{"description":"Missing, unknown, revoked or expired API key (`api_key_required`, `invalid_api_key`, `api_key_revoked`, `api_key_expired`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"The key lacks the scope this operation needs (`insufficient_scope`; `required` names it).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"No item with this id (`item_not_found`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Over the key's per-minute limit (`rate_limited`). Wait for `Retry-After` seconds.","headers":{"Retry-After":{"description":"Seconds until the key may call again.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/items/{id}/media/{mediaId}":{"get":{"operationId":"getItemMedia","tags":["items"],"summary":"Download one media file by its stable address","description":"The file itself (video, image or audio), streamed with `Range` support. The address never changes and does not expire, so it may be stored: it is each media entry's `stableUrl` in `/items/{id}/content` and the timeline. It is not a credential: every fetch sends the key, and revoking the key or taking away `media:read` stops it. For a tool that cannot send the key, use the signed `url` instead (valid one hour).\n\nRequires scope `media:read`.","x-scope":"media:read","security":[{"apiKey":[]}],"parameters":[{"name":"id","in":"path","required":true,"description":"Item id as returned by list, search or collect. An item's hubUrl or its path (`Ab3dE6gH/<slug>`), URL-encoded, also works: only the 8-character id in it counts, so an old slug still finds the item.","schema":{"type":"string"}},{"name":"mediaId","in":"path","required":true,"description":"`media[].id` from `/items/{id}/content`.","schema":{"type":"string"}}],"responses":{"200":{"description":"The whole file.","content":{"application/octet-stream":{"schema":{"type":"string","format":"binary"}}}},"206":{"description":"The requested byte range (`Range: bytes=…`).","content":{"application/octet-stream":{"schema":{"type":"string","format":"binary"}}}},"401":{"description":"Missing, unknown, revoked or expired API key (`api_key_required`, `invalid_api_key`, `api_key_revoked`, `api_key_expired`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"The key lacks the scope this operation needs (`insufficient_scope`; `required` names it).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"No item with this id, or no stored file with this id on it (`media_not_found`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Over the key's per-minute limit (`rate_limited`). Wait for `Retry-After` seconds.","headers":{"Retry-After":{"description":"Seconds until the key may call again.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/items/{id}/timeline":{"get":{"operationId":"getItemTimeline","tags":["items"],"summary":"Read a video or episode as a timeline","description":"What is said, in time order: the publisher's chapters and the transcript's paragraphs, each with the second it starts at, plus the post's own text and signed media links. Markdown by default, written for a language model to read as is; `format=json` for the same data structured. Cite moments as the item's hubUrl plus the timestamp. When `speech` is `unusable` or `none`, the video has to be watched: use the media links.\n\nRequires scope `items:read`.","x-scope":"items:read","security":[{"apiKey":[]}],"parameters":[{"name":"id","in":"path","required":true,"description":"Item id as returned by list, search or collect. An item's hubUrl or its path (`Ab3dE6gH/<slug>`), URL-encoded, also works: only the 8-character id in it counts, so an old slug still finds the item.","schema":{"type":"string"}},{"name":"format","in":"query","required":false,"description":"`markdown` (default) or `json`.","schema":{"default":"markdown","type":"string","enum":["markdown","json"]}}],"responses":{"200":{"description":"The timeline.","content":{"text/markdown":{"schema":{"type":"string"},"example":"# Onion soup\n\nyoutube · 12:40 · en  \nOriginal: https://www.youtube.com/watch?v=…  \nHub: https://jiajun.fyi/i/Ab3dE6gH/onion-soup\n\n## Timeline\n\n[00:00] **Intro**\n\n[00:01] First we slice the onion.\n"},"application/json":{"schema":{"$ref":"#/components/schemas/Timeline"}}}},"400":{"description":"`format` is not markdown or json (`invalid_format`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Missing, unknown, revoked or expired API key (`api_key_required`, `invalid_api_key`, `api_key_revoked`, `api_key_expired`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"The key lacks the scope this operation needs (`insufficient_scope`; `required` names it).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"No item with this id (`item_not_found`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Over the key's per-minute limit (`rate_limited`). Wait for `Retry-After` seconds.","headers":{"Retry-After":{"description":"Seconds until the key may call again.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/items/{id}/note":{"put":{"operationId":"setItemNote","tags":["items"],"summary":"Set an item's note","description":"Replaces the owner's note on the item. `null` or an empty string clears it.\n\nRequires scope `notes:write`.","x-scope":"notes:write","security":[{"apiKey":[]}],"parameters":[{"name":"id","in":"path","required":true,"description":"Item id as returned by list, search or collect. An item's hubUrl or its path (`Ab3dE6gH/<slug>`), URL-encoded, also works: only the 8-character id in it counts, so an old slug still finds the item.","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"note":{"type":["string","null"],"maxLength":2000}},"required":["note"]}}}},"responses":{"200":{"description":"Saved.","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"note":{"type":["string","null"]}},"required":["id","note"]}}}},"400":{"description":"Invalid note (`invalid_note`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Missing, unknown, revoked or expired API key (`api_key_required`, `invalid_api_key`, `api_key_revoked`, `api_key_expired`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"The key lacks the scope this operation needs (`insufficient_scope`; `required` names it).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"No item with this id (`item_not_found`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Over the key's per-minute limit (`rate_limited`). Wait for `Retry-After` seconds.","headers":{"Retry-After":{"description":"Seconds until the key may call again.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/items/{id}/star":{"post":{"operationId":"starItem","tags":["items"],"summary":"Star or unstar an item","description":"Stars the item; send `{\"starred\": false}` to unstar.\n\nRequires scope `notes:write`.","x-scope":"notes:write","security":[{"apiKey":[]}],"parameters":[{"name":"id","in":"path","required":true,"description":"Item id as returned by list, search or collect. An item's hubUrl or its path (`Ab3dE6gH/<slug>`), URL-encoded, also works: only the 8-character id in it counts, so an old slug still finds the item.","schema":{"type":"string"}}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","properties":{"starred":{"default":true,"type":"boolean"}}}}}},"responses":{"200":{"description":"Saved.","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"starred":{"type":"boolean"}},"required":["id","starred"]}}}},"400":{"description":"`starred` is not a boolean (`invalid_star_input`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Missing, unknown, revoked or expired API key (`api_key_required`, `invalid_api_key`, `api_key_revoked`, `api_key_expired`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"The key lacks the scope this operation needs (`insufficient_scope`; `required` names it).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"No item with this id (`item_not_found`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Over the key's per-minute limit (`rate_limited`). Wait for `Retry-After` seconds.","headers":{"Retry-After":{"description":"Seconds until the key may call again.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/items/{id}/later":{"post":{"operationId":"setItemReadLater","tags":["items"],"summary":"Mark or clear read / watch later","description":"Marks the item 稍后读 / 稍后看 (read or watch later; the same mark for an article, a post or a video) so it shows in `GET /items?readLater=true`; send `{\"readLater\": false}` once it is read. Clearing only removes the mark: the item, its note and star stay. Marking an item that is already marked keeps its first mark time.\n\nRequires scope `notes:write`.","x-scope":"notes:write","security":[{"apiKey":[]}],"parameters":[{"name":"id","in":"path","required":true,"description":"Item id as returned by list, search or collect. An item's hubUrl or its path (`Ab3dE6gH/<slug>`), URL-encoded, also works: only the 8-character id in it counts, so an old slug still finds the item.","schema":{"type":"string"}}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","properties":{"readLater":{"default":true,"type":"boolean"}}}}}},"responses":{"200":{"description":"Saved.","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"readLater":{"type":"boolean"},"readLaterAt":{"type":["string","null"],"format":"date-time"}},"required":["id","readLater","readLaterAt"]}}}},"400":{"description":"`readLater` is not a boolean (`invalid_read_later_input`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Missing, unknown, revoked or expired API key (`api_key_required`, `invalid_api_key`, `api_key_revoked`, `api_key_expired`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"The key lacks the scope this operation needs (`insufficient_scope`; `required` names it).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"No item with this id (`item_not_found`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Over the key's per-minute limit (`rate_limited`). Wait for `Retry-After` seconds.","headers":{"Retry-After":{"description":"Seconds until the key may call again.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/collect":{"post":{"operationId":"collect","tags":["collect"],"summary":"Collect a link","description":"Adds a link to the library, the same way the owner's share sheet does; the hub then fetches its content in the background. Send `url`, or `text` containing a link (the first link is collected and the rest of the text becomes the note). Collecting a link that is already in the library returns `deduplicated: true` and when it was first collected. Text without any link is refused for now. Send `readLater: true` to also put it on the 稍后读 / 稍后看 (read or watch later) list, for a video as for an article; `collect:write` is enough for that.\n\nThe response names the item at once; processing (fetch, media rehost, transcription) continues in the background. A service caller should not poll in a loop: register a callback webhook for its key and wait for `item.ready` / `item.failed` (see `webhooks`), then read `timelineUrl` and `contentUrl`. Send `clientRef` to correlate. Without a webhook, poll `itemUrl` no more than once a minute until `processing.status` is `ready` or `failed`.\n\nRequires scope `collect:write`.","x-scope":"collect:write","security":[{"apiKey":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CollectInput"}}}},"responses":{"202":{"description":"Accepted; content is fetched in the background.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CollectResult"}}}},"400":{"description":"Invalid input (`invalid_collect_input`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Missing, unknown, revoked or expired API key (`api_key_required`, `invalid_api_key`, `api_key_revoked`, `api_key_expired`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"The key lacks the scope this operation needs (`insufficient_scope`; `required` names it).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"422":{"description":"`text` contains no link (`no_url_in_text`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Over the key's per-minute limit (`rate_limited`). Wait for `Retry-After` seconds.","headers":{"Retry-After":{"description":"Seconds until the key may call again.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/flomo/memos":{"post":{"operationId":"writeFlomoMemo","tags":["collect"],"summary":"Write a memo to Flomo","description":"Writes one memo to the owner's Flomo (flomoapp.com) through the hub, so a caller needs only its API key, never the Flomo webhook. `#tags` inside `content` become Flomo tags; `title`, `sourceUrl` and `tags` are optional and are written above / below the content. The memo is not stored in the library.\n\nRequires scope `notes:write`.","x-scope":"notes:write","security":[{"apiKey":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"content":{"type":"string","minLength":1,"maxLength":5000,"description":"Memo text; line breaks become paragraphs."},"title":{"type":"string","minLength":1,"maxLength":240},"sourceUrl":{"type":"string","format":"uri"},"tags":{"description":"Added as `#tag`; a leading `#` is optional.","maxItems":20,"type":"array","items":{"type":"string","minLength":1,"maxLength":64}}},"required":["content"]}}}},"responses":{"202":{"description":"Flomo accepted the memo.","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"string","const":"accepted"}},"required":["status"]}}}},"400":{"description":"Invalid input (`invalid_flomo_memo_input`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Missing, unknown, revoked or expired API key (`api_key_required`, `invalid_api_key`, `api_key_revoked`, `api_key_expired`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"The key lacks the scope this operation needs (`insufficient_scope`; `required` names it).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Over the key's per-minute limit (`rate_limited`). Wait for `Retry-After` seconds.","headers":{"Retry-After":{"description":"Seconds until the key may call again.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"502":{"description":"Flomo refused the memo (`flomo_provider_error`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"The owner has not set a Flomo webhook yet (`flomo_not_configured`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/english/words":{"get":{"operationId":"listEnglishWords","tags":["english"],"summary":"List English words","description":"Words from the owner's Eudic word book, newest first, with the owner's mastery (1-5 stars; 0 = removed).\n\nRequires scope `english:read`.","x-scope":"english:read","security":[{"apiKey":[]}],"parameters":[{"name":"cursor","in":"query","required":false,"description":"Opaque page cursor: `nextCursor` from the previous page.","schema":{"type":"string"}},{"name":"limit","in":"query","required":false,"description":"Page size, 1-100 (default 20).","schema":{"default":20,"type":"integer","minimum":1,"maximum":100}}],"responses":{"200":{"description":"One page of words.","content":{"application/json":{"schema":{"type":"object","properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/EnglishWord"}},"nextCursor":{"type":["string","null"],"description":"Pass as `cursor` for the next page; null on the last page."}},"required":["items","nextCursor"]}}}},"400":{"description":"`cursor` is malformed (`invalid_time`), or another parameter is (`invalid_query`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Missing, unknown, revoked or expired API key (`api_key_required`, `invalid_api_key`, `api_key_revoked`, `api_key_expired`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"The key lacks the scope this operation needs (`insufficient_scope`; `required` names it).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Over the key's per-minute limit (`rate_limited`). Wait for `Retry-After` seconds.","headers":{"Retry-After":{"description":"Seconds until the key may call again.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/english/practice":{"get":{"operationId":"listEnglishPractice","tags":["english"],"summary":"List English practice records","description":"Vocabulary, phrasing, grammar and framing notes drawn from the owner's own English, newest first.\n\nRequires scope `english:read`.","x-scope":"english:read","security":[{"apiKey":[]}],"parameters":[{"name":"dimension","in":"query","required":false,"description":"Only one kind of record.","schema":{"type":"string","enum":["vocabulary","natural_phrasing","grammar","high_level_framing"]}},{"name":"cursor","in":"query","required":false,"description":"Opaque page cursor: `nextCursor` from the previous page.","schema":{"type":"string"}},{"name":"limit","in":"query","required":false,"description":"Page size, 1-100 (default 20).","schema":{"default":20,"type":"integer","minimum":1,"maximum":100}}],"responses":{"200":{"description":"One page of records.","content":{"application/json":{"schema":{"type":"object","properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/PracticeRecord"}},"nextCursor":{"type":["string","null"],"description":"Pass as `cursor` for the next page; null on the last page."}},"required":["items","nextCursor"]}}}},"400":{"description":"Unknown `dimension` (`invalid_dimension`), malformed `cursor` (`invalid_time`), or another malformed parameter (`invalid_query`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Missing, unknown, revoked or expired API key (`api_key_required`, `invalid_api_key`, `api_key_revoked`, `api_key_expired`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"The key lacks the scope this operation needs (`insufficient_scope`; `required` names it).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Over the key's per-minute limit (`rate_limited`). Wait for `Retry-After` seconds.","headers":{"Retry-After":{"description":"Seconds until the key may call again.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/songbook/songs":{"get":{"operationId":"listSongbookSongs","tags":["songbook"],"summary":"List songbook songs with their links","description":"Every song from the 《基本无害》 music-masterclass episodes (about 240, one response, no paging), plus songs added outside the show (no `mentions`), with its Spotify / Apple Music / NetEase link and match status, and where it comes up in the show. `coverage` counts statuses per service.\n\nRequires scope `songbook:read`.","x-scope":"songbook:read","security":[{"apiKey":[]}],"parameters":[{"name":"status","in":"query","required":false,"description":"Only songs with a link in this status, e.g. `needs_review`.","schema":{"type":"string"}},{"name":"service","in":"query","required":false,"description":"With `status`: look at this service only.","schema":{"type":"string","enum":["spotify","apple","netease"]}}],"responses":{"200":{"description":"The songbook.","content":{"application/json":{"schema":{"type":"object","properties":{"total":{"type":"integer"},"coverage":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"integer"}},"description":"Per service: count per status, and `linked` (matched or manual with a url)."},"songs":{"type":"array","items":{"$ref":"#/components/schemas/Song"}}},"required":["total","coverage","songs"]}}}},"400":{"description":"Unknown `service` (`invalid_service`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Missing, unknown, revoked or expired API key (`api_key_required`, `invalid_api_key`, `api_key_revoked`, `api_key_expired`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"The key lacks the scope this operation needs (`insufficient_scope`; `required` names it).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Over the key's per-minute limit (`rate_limited`). Wait for `Retry-After` seconds.","headers":{"Retry-After":{"description":"Seconds until the key may call again.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"The songbook database is not bound (`songbook_not_configured`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"post":{"operationId":"addSongbookSong","tags":["songbook"],"summary":"Add a song to the songbook","description":"A song that is not from the show (for example one recorded for 录音生成): keyed 「歌名|歌手」, no `mentions`; the link matcher finds its streaming links like any other. A song already there is returned as it is (200). Answers with its sheet (缺谱 until a melody is set).\n\nRequires scope `songbook:write`.","x-scope":"songbook:write","security":[{"apiKey":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"title":{"type":"string","minLength":1,"maxLength":100},"artist":{"type":"string","minLength":1,"maxLength":100}},"required":["title","artist"],"additionalProperties":false}}}},"responses":{"200":{"description":"Already in the songbook.","content":{"application/json":{"schema":{"type":"object","properties":{"sheet":{"$ref":"#/components/schemas/SongSheet"}},"required":["sheet"]}}}},"201":{"description":"Added.","content":{"application/json":{"schema":{"type":"object","properties":{"sheet":{"$ref":"#/components/schemas/SongSheet"}},"required":["sheet"]}}}},"400":{"description":"Invalid input (`invalid_song_input`): title and artist are required and must not contain `|`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Missing, unknown, revoked or expired API key (`api_key_required`, `invalid_api_key`, `api_key_revoked`, `api_key_expired`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"The key lacks the scope this operation needs (`insufficient_scope`; `required` names it).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Over the key's per-minute limit (`rate_limited`). Wait for `Retry-After` seconds.","headers":{"Retry-After":{"description":"Seconds until the key may call again.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"The songbook database is not bound (`songbook_not_configured`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/songbook/score-takes":{"post":{"operationId":"createScoreTake","tags":["songbook"],"summary":"Save a recording and its detected notes","description":"Send multipart/form-data: `take` is JSON with title, songKey, durationSeconds and score (notes, tempo, meter, key); `audio` is an optional file up to 30 MB. Uses the same validation and Whisper / MusicXML pipeline as the shell recorder.\n\nRequires scope `songbook:write`.","x-scope":"songbook:write","security":[{"apiKey":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"take":{"type":"string"},"audio":{"type":"string"}},"required":["take"]}},"multipart/form-data":{"schema":{"type":"object","properties":{"take":{"type":"string"},"audio":{"type":"string","format":"binary","contentEncoding":"binary"}},"required":["take"]}}}},"responses":{"201":{"description":"Saved take, with initial lyrics status.","content":{"application/json":{"schema":{"type":"object","properties":{"take":{"$ref":"#/components/schemas/ScoreTake"}},"required":["take"]}}}},"400":{"description":"Invalid multipart or take JSON (`invalid_score_take_input`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Missing, unknown, revoked or expired API key (`api_key_required`, `invalid_api_key`, `api_key_revoked`, `api_key_expired`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"The key lacks the scope this operation needs (`insufficient_scope`; `required` names it).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"413":{"description":"Recording exceeds 30 MB (`audio_too_large`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Over the key's per-minute limit (`rate_limited`). Wait for `Retry-After` seconds.","headers":{"Retry-After":{"description":"Seconds until the key may call again.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"The songbook database is not bound (`songbook_not_configured`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"get":{"operationId":"listScoreTakes","tags":["songbook"],"summary":"List 扒谱结果 (takes saved from 实时扒谱)","description":"The 50 latest takes saved on jiajun.fyi/music-transcription, newest first, without their MusicXML. `songKey` keeps the ones recorded for that song. For importing into 图图乐谱: pick one, read it, and PATCH its `musicxml` and `lyrics.text` onto a sheet.\n\nRequires scope `songbook:read`.","x-scope":"songbook:read","security":[{"apiKey":[]}],"parameters":[{"name":"songKey","in":"query","required":false,"description":"Only takes recorded for this song, 「歌名|歌手」.","schema":{"type":"string"}}],"responses":{"200":{"description":"The latest takes.","content":{"application/json":{"schema":{"type":"object","properties":{"takes":{"type":"array","items":{"$ref":"#/components/schemas/ScoreTakeSummary"}}},"required":["takes"]}}}},"401":{"description":"Missing, unknown, revoked or expired API key (`api_key_required`, `invalid_api_key`, `api_key_revoked`, `api_key_expired`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"The key lacks the scope this operation needs (`insufficient_scope`; `required` names it).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Over the key's per-minute limit (`rate_limited`). Wait for `Retry-After` seconds.","headers":{"Retry-After":{"description":"Seconds until the key may call again.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"The songbook database is not bound (`songbook_not_configured`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/songbook/score-takes/{id}":{"get":{"operationId":"getScoreTake","tags":["songbook"],"summary":"Read one 扒谱结果 with its MusicXML and lyrics","description":"The take's score as MusicXML (lyrics under the notes once Whisper has them) plus key, BPM, meter and the lyrics text. While `lyrics.status` is `pending`, read again in a minute.\n\nRequires scope `songbook:read`.","x-scope":"songbook:read","security":[{"apiKey":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"The take.","content":{"application/json":{"schema":{"type":"object","properties":{"take":{"$ref":"#/components/schemas/ScoreTake"}},"required":["take"]}}}},"401":{"description":"Missing, unknown, revoked or expired API key (`api_key_required`, `invalid_api_key`, `api_key_revoked`, `api_key_expired`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"The key lacks the scope this operation needs (`insufficient_scope`; `required` names it).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"No such take (`score_take_not_found`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Over the key's per-minute limit (`rate_limited`). Wait for `Retry-After` seconds.","headers":{"Retry-After":{"description":"Seconds until the key may call again.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"The songbook database is not bound (`songbook_not_configured`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/songbook/links/match":{"post":{"operationId":"matchSongbookLinks","tags":["songbook"],"summary":"Match a batch of songbook links","description":"Searches Apple Music and NetEase for up to `limit` songs per service that are due: never matched, a failed search older than an hour, or not found more than 30 days ago. Idempotent: call it repeatedly until `done` is true. Apple searches are spaced 3 s apart, so a call can take a minute. Without `keys` it also fetches covers missing on existing links (`covers`: how many per platform). The hourly cron runs the same batch.\n\nRequires scope `songbook:write`.","x-scope":"songbook:write","security":[{"apiKey":[]}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","properties":{"service":{"type":"string","enum":["apple","netease"]},"limit":{"default":5,"type":"integer","minimum":1,"maximum":20},"keys":{"description":"Re-match these songs now even if matched before (manual links stay).","minItems":1,"maxItems":20,"type":"array","items":{"type":"string","minLength":1,"maxLength":200}}},"additionalProperties":false}}}},"responses":{"200":{"description":"What this batch decided, and what is still due.","content":{"application/json":{"schema":{"type":"object","properties":{"results":{"type":"array","items":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}}},"due":{"type":"object","properties":{"apple":{"type":"integer"},"netease":{"type":"integer"}}},"done":{"type":"boolean"},"covers":{"type":["object","null"],"properties":{"spotify":{"type":"integer"},"apple":{"type":"integer"},"netease":{"type":"integer"}},"description":"Covers filled in this call per platform; null when `keys` was given"}},"required":["results","due","done","covers"]}}}},"400":{"description":"Invalid input (`invalid_songbook_match_input`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Missing, unknown, revoked or expired API key (`api_key_required`, `invalid_api_key`, `api_key_revoked`, `api_key_expired`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"The key lacks the scope this operation needs (`insufficient_scope`; `required` names it).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Over the key's per-minute limit (`rate_limited`). Wait for `Retry-After` seconds.","headers":{"Retry-After":{"description":"Seconds until the key may call again.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"The songbook database is not bound (`songbook_not_configured`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/songbook/songs/{key}/links/{service}":{"patch":{"operationId":"setSongbookLink","tags":["songbook"],"summary":"Set a song's link by hand","description":"One of: `{\"confirm\": true, \"trackId\": \"...\"}` accepts the candidate (a `needs_review` pick) whose track you looked at, so a candidate the matcher swapped in since is not confirmed by mistake; `{\"trackId\": \"...\"}` sets this track (an id from `alternatives` carries its name and album along; otherwise pass `url`, `name`, `artist`, `album`, or the url is built from the id); `{\"trackId\": null}` records that the service does not have the song; `{\"rematch\": true}` hands it back to the matcher. Everything but `rematch` makes the link `manual`.\n\nRequires scope `songbook:write`.","x-scope":"songbook:write","security":[{"apiKey":[]}],"parameters":[{"name":"key","in":"path","required":true,"description":"The song key, URL-encoded.","schema":{"type":"string"}},{"name":"service","in":"path","required":true,"schema":{"type":"string","enum":["spotify","apple","netease"]}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"anyOf":[{"type":"object","properties":{"confirm":{"type":"boolean","const":true},"trackId":{"type":"string","minLength":1,"maxLength":64}},"required":["confirm","trackId"],"additionalProperties":false},{"type":"object","properties":{"rematch":{"type":"boolean","const":true}},"required":["rematch"],"additionalProperties":false},{"type":"object","properties":{"trackId":{"type":["string","null"],"minLength":1,"maxLength":64},"url":{"type":"string","maxLength":500,"format":"uri"},"name":{"type":"string","maxLength":300},"artist":{"type":"string","maxLength":300},"album":{"type":"string","maxLength":300}},"required":["trackId"],"additionalProperties":false}]}}}},"responses":{"200":{"description":"The link as stored now.","content":{"application/json":{"schema":{"type":"object","properties":{"key":{"type":"string"},"service":{"type":"string","enum":["spotify","apple","netease"]},"link":{"$ref":"#/components/schemas/SongLink"}},"required":["key","service","link"]}}}},"400":{"description":"Invalid input (`invalid_song_link_input`), a `trackId` that is not this service's id format (`invalid_track_id`: digits for Apple Music and NetEase, 22 letters and digits for Spotify), or `rematch` on Spotify (`rematch_unsupported`: Spotify links are set by hand only).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Missing, unknown, revoked or expired API key (`api_key_required`, `invalid_api_key`, `api_key_revoked`, `api_key_expired`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"The key lacks the scope this operation needs (`insufficient_scope`; `required` names it).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"No such song (`song_not_found`) or service (`invalid_service`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"`nothing_to_confirm`: no candidate on this link; `track_changed`: the candidate is no longer the `trackId` sent (reload and check again).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Over the key's per-minute limit (`rate_limited`). Wait for `Retry-After` seconds.","headers":{"Retry-After":{"description":"Seconds until the key may call again.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"The songbook database is not bound (`songbook_not_configured`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/songbook/tracklists":{"get":{"operationId":"listTracklists","tags":["songbook"],"summary":"List tracklists","description":"Every tracklist asked for (DJ sets on 1001Tracklists), newest request first, with its status and number of rows.\n\nRequires scope `songbook:read`.","x-scope":"songbook:read","security":[{"apiKey":[]}],"responses":{"200":{"description":"Tracklists.","content":{"application/json":{"schema":{"type":"object","properties":{"tracklists":{"type":"array","items":{"$ref":"#/components/schemas/TracklistSummary"}}},"required":["tracklists"]}}}},"401":{"description":"Missing, unknown, revoked or expired API key (`api_key_required`, `invalid_api_key`, `api_key_revoked`, `api_key_expired`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"The key lacks the scope this operation needs (`insufficient_scope`; `required` names it).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Over the key's per-minute limit (`rate_limited`). Wait for `Retry-After` seconds.","headers":{"Retry-After":{"description":"Seconds until the key may call again.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"The songbook database is not bound (`songbook_not_configured`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"post":{"operationId":"requestTracklist","tags":["songbook"],"summary":"Ask for a 1001Tracklists set","description":"Queues a set page. The logged-in Chrome on the owner's Mac opens it on the next PIH Bridge run (the site shows a human check to anything else); poll the tracklist until `status` is `parsed`. Asking again for a failed set queues it again; anything else is left as it is.\n\nRequires scope `songbook:write`.","x-scope":"songbook:write","security":[{"apiKey":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"url":{"type":"string","minLength":1,"maxLength":500,"description":"https://www.1001tracklists.com/tracklist/<id>/..."}},"required":["url"],"additionalProperties":false}}}},"responses":{"202":{"description":"Queued (or already known).","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"status":{"type":"string","enum":["queued","fetched","parsed","failed"]}},"required":["id","status"]}}}},"400":{"description":"Invalid input (`invalid_tracklist_input`), or not a 1001Tracklists set link (`unsupported_tracklist_url`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Missing, unknown, revoked or expired API key (`api_key_required`, `invalid_api_key`, `api_key_revoked`, `api_key_expired`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"The key lacks the scope this operation needs (`insufficient_scope`; `required` names it).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Over the key's per-minute limit (`rate_limited`). Wait for `Retry-After` seconds.","headers":{"Retry-After":{"description":"Seconds until the key may call again.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"The songbook database is not bound (`songbook_not_configured`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/songbook/tracklists/{id}":{"get":{"operationId":"getTracklist","tags":["songbook"],"summary":"Read one tracklist","description":"The set and its rows in page order.\n\nRequires scope `songbook:read`.","x-scope":"songbook:read","security":[{"apiKey":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"The tracklist.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Tracklist"}}}},"401":{"description":"Missing, unknown, revoked or expired API key (`api_key_required`, `invalid_api_key`, `api_key_revoked`, `api_key_expired`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"The key lacks the scope this operation needs (`insufficient_scope`; `required` names it).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"`tracklist_not_found`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Over the key's per-minute limit (`rate_limited`). Wait for `Retry-After` seconds.","headers":{"Retry-After":{"description":"Seconds until the key may call again.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"The songbook database is not bound (`songbook_not_configured`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/songbook/tracklists/{id}/reread":{"post":{"operationId":"rereadTracklist","tags":["songbook"],"summary":"Read a stored set page again","description":"Reads the stored copy of the page again, after the page reader changed. No new fetch.\n\nRequires scope `songbook:write`.","x-scope":"songbook:write","security":[{"apiKey":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"New status.","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"status":{"type":"string","enum":["queued","fetched","parsed","failed"]}},"required":["id","status"]}}}},"401":{"description":"Missing, unknown, revoked or expired API key (`api_key_required`, `invalid_api_key`, `api_key_revoked`, `api_key_expired`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"The key lacks the scope this operation needs (`insufficient_scope`; `required` names it).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"`tracklist_page_missing`: no stored page yet.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Over the key's per-minute limit (`rate_limited`). Wait for `Retry-After` seconds.","headers":{"Retry-After":{"description":"Seconds until the key may call again.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"The songbook database is not bound (`songbook_not_configured`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/songbook/sheets":{"get":{"operationId":"listSongSheets","tags":["songbook"],"summary":"List 图图乐谱 sheets","description":"Every songbook song's sheet, those with a melody first (then by title): melody text, lyrics and cover motif. Songs without a melody come with `hasScore: false` (缺谱). One response, no paging.\n\nRequires scope `songbook:read`.","x-scope":"songbook:read","security":[{"apiKey":[]}],"responses":{"200":{"description":"Every sheet.","content":{"application/json":{"schema":{"type":"object","properties":{"sheets":{"type":"array","items":{"$ref":"#/components/schemas/SongSheet"}}},"required":["sheets"]}}}},"401":{"description":"Missing, unknown, revoked or expired API key (`api_key_required`, `invalid_api_key`, `api_key_revoked`, `api_key_expired`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"The key lacks the scope this operation needs (`insufficient_scope`; `required` names it).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Over the key's per-minute limit (`rate_limited`). Wait for `Retry-After` seconds.","headers":{"Retry-After":{"description":"Seconds until the key may call again.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"The songbook database is not bound (`songbook_not_configured`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/songbook/sheets/{key}":{"patch":{"operationId":"setSongSheet","tags":["songbook"],"summary":"Change a song's 图图乐谱 sheet","description":"Any of: `score` with `format` (`abc` | `musicxml`) sets the melody, `score: null` clears it (缺谱); `lyrics`, `coverMotif`, `source`. Fields left out stay as they are.\n\nRequires scope `songbook:write`.","x-scope":"songbook:write","security":[{"apiKey":[]}],"parameters":[{"name":"key","in":"path","required":true,"description":"The song key, URL-encoded.","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"score":{"type":["string","null"],"maxLength":400000},"format":{"type":"string","enum":["abc","musicxml"]},"lyrics":{"type":["string","null"],"maxLength":20000},"coverMotif":{"type":["string","null"],"enum":["star","letters","tiger","cake","sheep","bridge","boat","gourd","horse","heart","notes",null]},"source":{"type":["string","null"],"maxLength":300}},"additionalProperties":false}}}},"responses":{"200":{"description":"The sheet as stored now.","content":{"application/json":{"schema":{"type":"object","properties":{"sheet":{"$ref":"#/components/schemas/SongSheet"}},"required":["sheet"]}}}},"400":{"description":"Invalid input (`invalid_song_sheet_input`), a score without `format` (`format_required`), or a score that is not the format named (`score_format_mismatch`: ABC needs a `K:` line, MusicXML a `<score-partwise>` or `<score-timewise>` root).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Missing, unknown, revoked or expired API key (`api_key_required`, `invalid_api_key`, `api_key_revoked`, `api_key_expired`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"The key lacks the scope this operation needs (`insufficient_scope`; `required` names it).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"No such songbook song (`song_not_found`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Over the key's per-minute limit (`rate_limited`). Wait for `Retry-After` seconds.","headers":{"Retry-After":{"description":"Seconds until the key may call again.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"The songbook database is not bound (`songbook_not_configured`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/things/wanted":{"post":{"operationId":"addWanted","tags":["things"],"summary":"Record something the owner wants to buy (记到想买)","description":"The owner says they want something: by words, a link or a photo. Finds the product if it is already in 物品 (same barcode / ASIN / brand + name, the same saved link, or the one the model matches from the words) and updates it instead of making a second one: status, note, the price said, the link. A new product is looked up in the background for its official picture and prices.\n\nStatus defaults to 想买 (`wanted`) with status source `user`: the owner said it, so a later model guess never changes it. Send `status` for 已买 / 不要了.\n\nBody: JSON (`WantedInput`), `multipart/form-data` with the same fields and `image` files, a bare `image/jpeg` / `image/png` body, or `text/plain` (taken as `text`). Show `message` to the person as is.\n\nRequires scope `things:write`.","x-scope":"things:write","security":[{"apiKey":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/WantedInput"}},"multipart/form-data":{"schema":{"$ref":"#/components/schemas/WantedInput"}},"text/plain":{"schema":{"type":"string"}}}},"responses":{"200":{"description":"An existing product was updated, or this repeats an earlier request (`repeated: true`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WantedResult"}}}},"201":{"description":"A new product was made.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WantedResult"}}}},"400":{"description":"Invalid input (`invalid_wanted_input`, `invalid_json`, `invalid_form`, `nothing_to_add`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Missing, unknown, revoked or expired API key (`api_key_required`, `invalid_api_key`, `api_key_revoked`, `api_key_expired`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"The key lacks the scope this operation needs (`insufficient_scope`; `required` names it).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"413":{"description":"Too many or too large images (`too_many_images`, `image_too_large`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"422":{"description":"Nothing recognisable: no name, and the photos show no product (`no_product`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Over the key's per-minute limit (`rate_limited`). Wait for `Retry-After` seconds.","headers":{"Retry-After":{"description":"Seconds until the key may call again.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/things/products":{"get":{"operationId":"listThingsProducts","tags":["things"],"summary":"Search 物品 (products)","description":"Products, most recently bought or changed first, with the latest and lowest price seen. Use it to check whether something is already on the 想买 list before adding, or to answer what is on it.\n\nRequires scope `things:read`.","x-scope":"things:read","security":[{"apiKey":[]}],"parameters":[{"name":"q","in":"query","required":false,"description":"Name, brand, model, barcode, or a receipt line as printed.","schema":{"type":"string"}},{"name":"status","in":"query","required":false,"schema":{"type":"string","enum":["wanted","owned","passed"]}},{"name":"kind","in":"query","required":false,"schema":{"type":"string","enum":["food","appliance","household","other"]}},{"name":"limit","in":"query","required":false,"schema":{"default":60,"type":"integer","minimum":1,"maximum":200}}],"responses":{"200":{"description":"Matching products and counts per status.","content":{"application/json":{"schema":{"type":"object","properties":{"counts":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"integer"},"description":"How many products in all and per status, and how many purchases, photos and documents."},"products":{"type":"array","items":{"$ref":"#/components/schemas/ThingsProduct"}}},"required":["counts","products"]}}}},"400":{"description":"A parameter is malformed (`invalid_query`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Missing, unknown, revoked or expired API key (`api_key_required`, `invalid_api_key`, `api_key_revoked`, `api_key_expired`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"The key lacks the scope this operation needs (`insufficient_scope`; `required` names it).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Over the key's per-minute limit (`rate_limited`). Wait for `Retry-After` seconds.","headers":{"Retry-After":{"description":"Seconds until the key may call again.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/things/wanted-webhook":{"get":{"operationId":"getWantedWebhook","tags":["things"],"summary":"Read the 想买 Webhook address","description":"The 想买 Webhook: one secret address that adds a 想买 thing (`GET ?url=…` or `POST {\"text\": …}` / any body `/things/wanted` takes), for the iPhone Shortcut 「记到想买」 and tools that cannot send a key. The secret in it is its only credential and can only add; treat the address like a key.\n\n`shortcut.ready` says whether the Shortcut with this address inside has been signed (a Mac signs it within a few minutes of a new address); then `shortcut.url` downloads it. `configured: false` until an address is made with POST.\n\nRequires scope `things:write`.","x-scope":"things:write","security":[{"apiKey":[]}],"responses":{"200":{"description":"The address and whether the Shortcut for it is signed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WantedWebhook"}}}},"401":{"description":"Missing, unknown, revoked or expired API key (`api_key_required`, `invalid_api_key`, `api_key_revoked`, `api_key_expired`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"The key lacks the scope this operation needs (`insufficient_scope`; `required` names it).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Over the key's per-minute limit (`rate_limited`). Wait for `Retry-After` seconds.","headers":{"Retry-After":{"description":"Seconds until the key may call again.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"post":{"operationId":"rotateWantedWebhook","tags":["things"],"summary":"Make a new 想买 Webhook address","description":"Makes the address (the first time) or replaces it: the old address stops at once, so a Shortcut installed with it answers 「地址已换新」 until the new one is installed. The Shortcut is signed again with the new address within a few minutes (`shortcut.ready`).\n\nRequires scope `things:write`.","x-scope":"things:write","security":[{"apiKey":[]}],"responses":{"200":{"description":"The address and whether the Shortcut for it is signed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WantedWebhook"}}}},"401":{"description":"Missing, unknown, revoked or expired API key (`api_key_required`, `invalid_api_key`, `api_key_revoked`, `api_key_expired`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"The key lacks the scope this operation needs (`insufficient_scope`; `required` names it).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Over the key's per-minute limit (`rate_limited`). Wait for `Retry-After` seconds.","headers":{"Retry-After":{"description":"Seconds until the key may call again.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/things/wanted.shortcut":{"get":{"operationId":"downloadWantedShortcut","tags":["things"],"summary":"Download the signed 记到想买 Shortcut","description":"The iPhone Shortcut 「记到想买」, signed, with the current 想买 Webhook address already inside: opened on an iPhone it goes straight to 「添加快捷指令」, nothing to paste. In the share sheet it sends the shared link or text; run from the home screen or Siri it asks what to buy. It holds the address, so keep it private.\n\nRequires scope `things:write`.","x-scope":"things:write","security":[{"apiKey":[]}],"responses":{"200":{"description":"The signed `.shortcut` file (Apple Encrypted Archive).","content":{"application/octet-stream":{"schema":{"type":"string","format":"binary"}}}},"401":{"description":"Missing, unknown, revoked or expired API key (`api_key_required`, `invalid_api_key`, `api_key_revoked`, `api_key_expired`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"The key lacks the scope this operation needs (`insufficient_scope`; `required` names it).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"No address yet, or the Shortcut for the current address is still being signed (`shortcut_not_ready`; `message` says which).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Over the key's per-minute limit (`rate_limited`). Wait for `Retry-After` seconds.","headers":{"Retry-After":{"description":"Seconds until the key may call again.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/restaurants":{"get":{"operationId":"listRestaurants","tags":["restaurants"],"summary":"Search 餐厅 (restaurants)","description":"Restaurants we want to try (想去) or have been to (去过), most recently changed first, with recommended dishes, tips and how often we went. Check here before adding, or to answer 「南湾有什么想去的韩餐」.\n\nRequires scope `restaurants:read`.","x-scope":"restaurants:read","security":[{"apiKey":[]}],"parameters":[{"name":"q","in":"query","required":false,"description":"Words in the name (either script, accents ignored), city, address, cuisine, dishes, tips or notes.","schema":{"type":"string"}},{"name":"status","in":"query","required":false,"description":"`want` (想去) or `been` (去过); the Chinese words work too.","schema":{"type":"string","enum":["want","been","想去","去过","吃过"]}},{"name":"area","in":"query","required":false,"description":"`sf` (三藩), `peninsula` (半岛), `south_bay` (南湾), `east_bay` (东湾), `north_bay` (北湾), `other` (周边), `travel` (外地: outside the Bay Area and 周边, the travel list; needs `country` and `city`); the Chinese words work too.","schema":{"type":"string","enum":["sf","peninsula","south_bay","east_bay","north_bay","other","travel","三藩","旧金山","三藩市","SF","半岛","南湾","东湾","北湾","周边","其他","外地","旅行"]}},{"name":"scope","in":"query","required":false,"description":"`local`: the Bay Area and 周边 (every area but `travel`); `travel`: the travel list (外地).","schema":{"type":"string","enum":["local","travel"]}},{"name":"country","in":"query","required":false,"description":"ISO 3166-1 alpha-2 code: `US`, `CN`, `JP`, `KR`; 美国 / 中国 / 日本 / 韩国 work too. Default `US` outside 外地.","schema":{"type":"string"}},{"name":"city","in":"query","required":false,"description":"Exactly this city (case ignored), as `places` lists it.","schema":{"type":"string"}},{"name":"cuisine","in":"query","required":false,"description":"Part of the cuisine label, e.g. `韩`.","schema":{"type":"string"}},{"name":"links","in":"query","required":false,"description":"For cleaning up: `unconfirmed` = stores with a link nobody confirmed (自动关联 or 待清洗); `review` = with a 待清洗 link.","schema":{"type":"string","enum":["unconfirmed","review"]}},{"name":"limit","in":"query","required":false,"schema":{"default":100,"type":"integer","minimum":1,"maximum":200}}],"responses":{"200":{"description":"Matching restaurants, how many there are in all and per status, and the index by place.","content":{"application/json":{"schema":{"type":"object","properties":{"counts":{"type":"object","properties":{"total":{"type":"integer"},"want":{"type":"integer"},"been":{"type":"integer"}},"required":["total","want","been"]},"places":{"type":"array","items":{"type":"object","properties":{"country":{"type":"string"},"countryName":{"type":"string"},"count":{"type":"integer"},"cities":{"type":"array","items":{"type":"object","properties":{"city":{"type":"string","description":"`''` for restaurants without a city."},"count":{"type":"integer"}},"required":["city","count"]}}},"required":["country","countryName","count","cities"]},"description":"Per country, then per city, how many restaurants `scope` and `status` select (before `country`, `city`, `q`, `area` and `cuisine` narrow the list): the travel list's index. Most first."},"restaurants":{"type":"array","items":{"$ref":"#/components/schemas/Restaurant"}}},"required":["counts","places","restaurants"]}}}},"400":{"description":"A parameter is malformed (`invalid_query`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Missing, unknown, revoked or expired API key (`api_key_required`, `invalid_api_key`, `api_key_revoked`, `api_key_expired`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"The key lacks the scope this operation needs (`insufficient_scope`; `required` names it).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Over the key's per-minute limit (`rate_limited`). Wait for `Retry-After` seconds.","headers":{"Retry-After":{"description":"Seconds until the key may call again.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"post":{"operationId":"addRestaurant","tags":["restaurants"],"summary":"Add a restaurant (or find it)","description":"Safe to call whenever a restaurant comes up: if it is already recorded (same name or local name, accents and case ignored, and the same street when both have an address) that one is returned and only what it lacked is filled in (empty fields; new dishes, tips, links and source posts are added; nothing is overwritten, and 去过 never goes back to 想去). Another branch is another restaurant: give its address; it is linked to the stores of the same name (in another country the link is 待清洗). Status defaults to 想去. Send `visit` to record a meal in the same call. To change what is there, use PATCH. Show `message` to the person as is.\n\nRequires scope `restaurants:write`.","x-scope":"restaurants:write","security":[{"apiKey":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RestaurantInput"}}}},"responses":{"200":{"description":"It was already recorded; `filled` says what this call added.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RestaurantAdded"}}}},"201":{"description":"A new restaurant was made.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RestaurantAdded"}}}},"400":{"description":"Invalid input (`invalid_restaurant_input`), a `sourceItemIds` entry is not in the library (`source_item_not_found`), or a new 外地 restaurant without `country` and `city` (`travel_place_required`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Missing, unknown, revoked or expired API key (`api_key_required`, `invalid_api_key`, `api_key_revoked`, `api_key_expired`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"The key lacks the scope this operation needs (`insufficient_scope`; `required` names it).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"Several restaurants by that name and no address to tell them apart (`ambiguous_restaurant`; `candidates` lists them).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Over the key's per-minute limit (`rate_limited`). Wait for `Retry-After` seconds.","headers":{"Retry-After":{"description":"Seconds until the key may call again.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/restaurants/{id}":{"get":{"operationId":"getRestaurant","tags":["restaurants"],"summary":"Read one restaurant","description":"The restaurant with its linked stores (`related`) and every visit.\n\nRequires scope `restaurants:read`.","x-scope":"restaurants:read","security":[{"apiKey":[]}],"parameters":[{"name":"id","in":"path","required":true,"description":"The restaurant's id from list or add.","schema":{"type":"string"}}],"responses":{"200":{"description":"The restaurant.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RestaurantDetail"}}}},"401":{"description":"Missing, unknown, revoked or expired API key (`api_key_required`, `invalid_api_key`, `api_key_revoked`, `api_key_expired`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"The key lacks the scope this operation needs (`insufficient_scope`; `required` names it).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"No such restaurant (`restaurant_not_found`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Over the key's per-minute limit (`rate_limited`). Wait for `Retry-After` seconds.","headers":{"Retry-After":{"description":"Seconds until the key may call again.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"patch":{"operationId":"updateRestaurant","tags":["restaurants"],"summary":"Change a restaurant","description":"Only the fields sent change. A list (`dishes`, `tips`, `links`, `sourceItemIds`) replaces the whole list, so read the restaurant first and send the full new list; `null` clears a text field.\n\nRequires scope `restaurants:write`.","x-scope":"restaurants:write","security":[{"apiKey":[]}],"parameters":[{"name":"id","in":"path","required":true,"description":"The restaurant's id from list or add.","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RestaurantPatch"}}}},"responses":{"200":{"description":"The restaurant as stored now.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RestaurantDetail"}}}},"400":{"description":"Invalid input (`invalid_restaurant_input`, `source_item_not_found`, `travel_place_required`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Missing, unknown, revoked or expired API key (`api_key_required`, `invalid_api_key`, `api_key_revoked`, `api_key_expired`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"The key lacks the scope this operation needs (`insufficient_scope`; `required` names it).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"No such restaurant (`restaurant_not_found`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Over the key's per-minute limit (`rate_limited`). Wait for `Retry-After` seconds.","headers":{"Retry-After":{"description":"Seconds until the key may call again.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/restaurants/{id}/visits":{"post":{"operationId":"addRestaurantVisit","tags":["restaurants"],"summary":"Record a visit (去吃了)","description":"We ate there: the restaurant becomes 去过. Every field is optional; an empty body records today (Pacific time). One visit per day: the same day again fills in that visit (a retry adds nothing).\n\nRequires scope `restaurants:write`.","x-scope":"restaurants:write","security":[{"apiKey":[]}],"parameters":[{"name":"id","in":"path","required":true,"description":"The restaurant's id from list or add.","schema":{"type":"string"}}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/VisitInput"}}}},"responses":{"200":{"description":"That day already had a visit; it was filled in.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/VisitAdded"}}}},"201":{"description":"The visit was recorded.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/VisitAdded"}}}},"400":{"description":"Invalid input (`invalid_visit_input`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Missing, unknown, revoked or expired API key (`api_key_required`, `invalid_api_key`, `api_key_revoked`, `api_key_expired`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"The key lacks the scope this operation needs (`insufficient_scope`; `required` names it).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"No such restaurant (`restaurant_not_found`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Over the key's per-minute limit (`rate_limited`). Wait for `Retry-After` seconds.","headers":{"Retry-After":{"description":"Seconds until the key may call again.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/restaurants/{id}/links":{"post":{"operationId":"addRestaurantLink","tags":["restaurants"],"summary":"Link two stores (关联的店)","description":"Says another store belongs with this one (the same restaurant elsewhere): the link is 已确认. Nothing is copied or moved; each store keeps its own dishes, visits and reviews. Linking stores already linked confirms that link.\n\nRequires scope `restaurants:write`.","x-scope":"restaurants:write","security":[{"apiKey":[]}],"parameters":[{"name":"id","in":"path","required":true,"description":"The restaurant's id from list or add.","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RestaurantLinkInput"}}}},"responses":{"200":{"description":"This restaurant as stored now, with `related`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RestaurantDetail"}}}},"400":{"description":"Invalid input (`invalid_link_input`, `same_restaurant`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Missing, unknown, revoked or expired API key (`api_key_required`, `invalid_api_key`, `api_key_revoked`, `api_key_expired`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"The key lacks the scope this operation needs (`insufficient_scope`; `required` names it).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"No such restaurant (`restaurant_not_found`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Over the key's per-minute limit (`rate_limited`). Wait for `Retry-After` seconds.","headers":{"Retry-After":{"description":"Seconds until the key may call again.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/restaurants/{id}/links/{linkId}":{"patch":{"operationId":"updateRestaurantLink","tags":["restaurants"],"summary":"Confirm or remove a link","description":"`confirmed`: they do belong together. `review`: 待清洗, to sort out later. `removed`: not related; the link goes.\n\nRequires scope `restaurants:write`.","x-scope":"restaurants:write","security":[{"apiKey":[]}],"parameters":[{"name":"id","in":"path","required":true,"description":"The restaurant's id.","schema":{"type":"string"}},{"name":"linkId","in":"path","required":true,"description":"`related[].link.id`.","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RestaurantLinkPatch"}}}},"responses":{"200":{"description":"This restaurant as stored now, with `related`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RestaurantDetail"}}}},"400":{"description":"Invalid input (`invalid_link_input`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Missing, unknown, revoked or expired API key (`api_key_required`, `invalid_api_key`, `api_key_revoked`, `api_key_expired`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"The key lacks the scope this operation needs (`insufficient_scope`; `required` names it).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"No such restaurant (`restaurant_not_found`) or link (`restaurant_link_not_found`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Over the key's per-minute limit (`rate_limited`). Wait for `Retry-After` seconds.","headers":{"Retry-After":{"description":"Seconds until the key may call again.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/restaurants/{id}/reviews":{"get":{"operationId":"getRestaurantReviews","tags":["restaurants"],"summary":"Read a restaurant's reviews (评价)","description":"What Google Maps, Yelp, OpenTable, The Infatuation, Hungry Onion and articles linked from the restaurant say about it, fetched in the background (hourly job) when it was added and refreshed weekly. A restaurant is one store: every listing is about this store (a review of another branch, or of another city, is never attached). Without `source`: per source, its status and listings (rating, count, a one-line summary, recommended dishes, link). With `source`: that source only, each listing with its review items (excerpts; never whole texts). A `pending` (待确认) listing may be another place of the same name: say so rather than state it as fact. Google and Yelp need API keys; until they are set their status is `no_key`.\n\nRequires scope `restaurants:read`.","x-scope":"restaurants:read","security":[{"apiKey":[]}],"parameters":[{"name":"id","in":"path","required":true,"description":"The restaurant's id from list or add.","schema":{"type":"string"}},{"name":"source","in":"query","required":false,"description":"Only this source, with its review items.","schema":{"type":"string","enum":["google","yelp","opentable","infatuation","hungry_onion","article"]}}],"responses":{"200":{"description":"The reviews: `RestaurantReviews`, or `RestaurantReviewsOfSource` when `source` is given.","content":{"application/json":{"schema":{"anyOf":[{"$ref":"#/components/schemas/RestaurantReviewsOfSource"},{"$ref":"#/components/schemas/RestaurantReviews"}]}}}},"400":{"description":"Unknown source (`invalid_query`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Missing, unknown, revoked or expired API key (`api_key_required`, `invalid_api_key`, `api_key_revoked`, `api_key_expired`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"The key lacks the scope this operation needs (`insufficient_scope`; `required` names it).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"No such restaurant (`restaurant_not_found`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Over the key's per-minute limit (`rate_limited`). Wait for `Retry-After` seconds.","headers":{"Retry-After":{"description":"Seconds until the key may call again.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"patch":{"operationId":"updateRestaurantReviews","tags":["restaurants"],"summary":"Pin or settle a review listing","description":"Two uses. Pin: `{source, url}` (The Infatuation review, Hungry Onion topic, OpenTable page, an article) or `{source, externalId}` (Google place id, Yelp business id), that listing is fetched at once and shown as 已确认, and earlier automatic finds for the source are set aside. Settle a 待确认 one: `{listingId, status: \"confirmed\" | \"rejected\"}`; a rejected listing never comes back on a refetch.\n\nRequires scope `restaurants:write`.","x-scope":"restaurants:write","security":[{"apiKey":[]}],"parameters":[{"name":"id","in":"path","required":true,"description":"The restaurant's id from list or add.","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ReviewPatch"}}}},"responses":{"200":{"description":"Done; `reviews` is that source as it stands now, after fetching the pinned page.","content":{"application/json":{"schema":{"type":"object","properties":{"message":{"type":"string","description":"One sentence to show as is."},"reviews":{"$ref":"#/components/schemas/RestaurantReviewsOfSource"}},"required":["message","reviews"]}}}},"400":{"description":"Invalid input (`invalid_review_patch`, `external_id_required`, `not_an_opentable_url`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Missing, unknown, revoked or expired API key (`api_key_required`, `invalid_api_key`, `api_key_revoked`, `api_key_expired`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"The key lacks the scope this operation needs (`insufficient_scope`; `required` names it).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"No such restaurant (`restaurant_not_found`) or listing (`review_listing_not_found`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Over the key's per-minute limit (`rate_limited`). Wait for `Retry-After` seconds.","headers":{"Retry-After":{"description":"Seconds until the key may call again.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/restaurants/{id}/reviews/refresh":{"post":{"operationId":"refreshRestaurantReviews","tags":["restaurants"],"summary":"Fetch a restaurant's reviews again","description":"With `source`: fetches that source again now (the call waits for it). Without: queues every source for the background job, within the hour. Read GET /restaurants/{id}/reviews afterwards.\n\nRequires scope `restaurants:write`.","x-scope":"restaurants:write","security":[{"apiKey":[]}],"parameters":[{"name":"id","in":"path","required":true,"description":"The restaurant's id from list or add.","schema":{"type":"string"}}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ReviewRefresh"}}}},"responses":{"202":{"description":"Queued (or, with `source`, fetched already; `queued` is then empty).","content":{"application/json":{"schema":{"type":"object","properties":{"queued":{"type":"array","items":{"type":"string","enum":["google","yelp","opentable","infatuation","hungry_onion","article"]}},"message":{"type":"string","description":"One sentence to show as is."}},"required":["queued","message"]}}}},"400":{"description":"Invalid input (`invalid_review_refresh`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Missing, unknown, revoked or expired API key (`api_key_required`, `invalid_api_key`, `api_key_revoked`, `api_key_expired`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"The key lacks the scope this operation needs (`insufficient_scope`; `required` names it).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"No such restaurant (`restaurant_not_found`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Over the key's per-minute limit (`rate_limited`). Wait for `Retry-After` seconds.","headers":{"Retry-After":{"description":"Seconds until the key may call again.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/books":{"get":{"operationId":"listBooks","tags":["books"],"summary":"List 读书 (WeRead books)","description":"Books on the owner's WeRead (微信读书) shelf, and books taken off it that still have notes, last read first, with progress and how many highlights and thoughts each has. Synced hourly, read-only. Books marked 私密阅读 are left out unless `includePrivate=true`. `counts` are per status for the same filters; `sync` says whether the sync works.\n\nRequires scope `books:read`.","x-scope":"books:read","security":[{"apiKey":[]}],"parameters":[{"name":"status","in":"query","required":false,"description":"`reading` 在读, `finished` 读完, `unread` 没打开过, `all`.","schema":{"default":"all","type":"string","enum":["reading","finished","unread","all"]}},{"name":"q","in":"query","required":false,"description":"Words in the title or author.","schema":{"type":"string","maxLength":100}},{"name":"includePrivate","in":"query","required":false,"description":"`true`: include books marked 私密阅读 on WeRead (left out by default). Only when the owner asks for them.","schema":{"default":false,"type":"boolean"}},{"name":"cursor","in":"query","required":false,"description":"`nextCursor` of the previous page.","schema":{"type":"string","maxLength":20}},{"name":"limit","in":"query","required":false,"schema":{"default":50,"type":"integer","minimum":1,"maximum":200}}],"responses":{"200":{"description":"One page of books.","content":{"application/json":{"schema":{"type":"object","properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/Book"}},"nextCursor":{"type":["string","null"],"description":"Pass as `cursor` for the next page; null on the last page."},"counts":{"type":"object","properties":{"all":{"type":"integer"},"reading":{"type":"integer"},"finished":{"type":"integer"},"unread":{"type":"integer"}},"required":["all","reading","finished","unread"]},"sync":{"$ref":"#/components/schemas/BookSync"}},"required":["items","nextCursor","counts","sync"]}}}},"400":{"description":"A parameter is malformed (`invalid_query`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Missing, unknown, revoked or expired API key (`api_key_required`, `invalid_api_key`, `api_key_revoked`, `api_key_expired`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"The key lacks the scope this operation needs (`insufficient_scope`; `required` names it).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Over the key's per-minute limit (`rate_limited`). Wait for `Retry-After` seconds.","headers":{"Retry-After":{"description":"Seconds until the key may call again.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/books/highlights":{"get":{"operationId":"searchBookNotes","tags":["books"],"summary":"Search highlights and thoughts (划线、想法)","description":"Full-text search over the owner's WeRead highlights and thoughts, best match first; without `q`, the newest highlights. Each hit is a highlight with the thoughts on it, or a thought on a passage that is not highlighted. Answer 「我在哪本书里划过关于 X 的话」 with this. Words of 3+ characters match as substrings through the index; shorter ones scan. Private books are left out unless `includePrivate=true`.\n\nRequires scope `books:read`.","x-scope":"books:read","security":[{"apiKey":[]}],"parameters":[{"name":"q","in":"query","required":false,"description":"Words that must all appear (Chinese or other).","schema":{"type":"string","maxLength":100}},{"name":"bookId","in":"query","required":false,"description":"Only this book.","schema":{"type":"string","maxLength":64}},{"name":"includePrivate","in":"query","required":false,"description":"`true`: include books marked 私密阅读 on WeRead (left out by default). Only when the owner asks for them.","schema":{"default":false,"type":"boolean"}},{"name":"cursor","in":"query","required":false,"description":"`nextCursor` of the previous page.","schema":{"type":"string","maxLength":20}},{"name":"limit","in":"query","required":false,"schema":{"default":30,"type":"integer","minimum":1,"maximum":100}}],"responses":{"200":{"description":"One page of hits.","content":{"application/json":{"schema":{"type":"object","properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/BookNote"}},"nextCursor":{"type":["string","null"],"description":"Pass as `cursor` for the next page; null on the last page."}},"required":["items","nextCursor"]}}}},"400":{"description":"A parameter is malformed (`invalid_query`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Missing, unknown, revoked or expired API key (`api_key_required`, `invalid_api_key`, `api_key_revoked`, `api_key_expired`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"The key lacks the scope this operation needs (`insufficient_scope`; `required` names it).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Over the key's per-minute limit (`rate_limited`). Wait for `Retry-After` seconds.","headers":{"Retry-After":{"description":"Seconds until the key may call again.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/books/stats":{"get":{"operationId":"getReadingStats","tags":["books"],"summary":"Read 阅读时长 (reading time)","description":"Reading time from WeRead for this week, month, year or in total: seconds, reading days, the day average and its change, books read longest, and seconds per day. Days are Beijing (UTC+8) calendar days, as WeRead counts them; say so when it matters (the owner lives in Pacific time).\n\nRequires scope `books:read`.","x-scope":"books:read","security":[{"apiKey":[]}],"parameters":[{"name":"range","in":"query","required":false,"description":"`week`, `month` (default), `year`, `total`.","schema":{"default":"month","type":"string","enum":["week","month","year","total"]}}],"responses":{"200":{"description":"The numbers; zeros and empty lists before the first sync.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ReadingStats"}}}},"400":{"description":"Unknown range (`invalid_query`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Missing, unknown, revoked or expired API key (`api_key_required`, `invalid_api_key`, `api_key_revoked`, `api_key_expired`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"The key lacks the scope this operation needs (`insufficient_scope`; `required` names it).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Over the key's per-minute limit (`rate_limited`). Wait for `Retry-After` seconds.","headers":{"Retry-After":{"description":"Seconds until the key may call again.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/books/{id}":{"get":{"operationId":"getBook","tags":["books"],"summary":"Read one book's notes","description":"The book with every highlight by chapter (book order) and each thought under the passage it is about; chapter notes and thoughts on passages that are not highlighted sit in their chapter's `thoughts`, reviews of the whole book in `reviews`. Link to it with `readerLink`.\n\nRequires scope `books:read`.","x-scope":"books:read","security":[{"apiKey":[]}],"parameters":[{"name":"id","in":"path","required":true,"description":"The book's id from GET /books.","schema":{"type":"string"}},{"name":"includePrivate","in":"query","required":false,"description":"`true`: include books marked 私密阅读 on WeRead (left out by default). Only when the owner asks for them.","schema":{"default":false,"type":"boolean"}}],"responses":{"200":{"description":"The book.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BookDetail"}}}},"401":{"description":"Missing, unknown, revoked or expired API key (`api_key_required`, `invalid_api_key`, `api_key_revoked`, `api_key_expired`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"The key lacks the scope this operation needs (`insufficient_scope`; `required` names it).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"No such book, or a private one without `includePrivate=true` (`book_not_found`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Over the key's per-minute limit (`rate_limited`). Wait for `Retry-After` seconds.","headers":{"Retry-After":{"description":"Seconds until the key may call again.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/books/sync":{"post":{"operationId":"syncBooks","tags":["books"],"summary":"Sync WeRead now (立即同步)","description":"Asks for a sync now instead of on the hour: the shelf and notebooks are read within a minute, changed books after that. Nothing on WeRead changes (the sync only reads).\n\nRequires scope `books:write`.","x-scope":"books:write","security":[{"apiKey":[]}],"responses":{"202":{"description":"Queued.","content":{"application/json":{"schema":{"type":"object","properties":{"queued":{"type":"boolean","const":true}},"required":["queued"]}}}},"401":{"description":"Missing, unknown, revoked or expired API key (`api_key_required`, `invalid_api_key`, `api_key_revoked`, `api_key_expired`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"The key lacks the scope this operation needs (`insufficient_scope`; `required` names it).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"No WeRead key is set (`weread_not_configured`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Over the key's per-minute limit (`rate_limited`). Wait for `Retry-After` seconds.","headers":{"Retry-After":{"description":"Seconds until the key may call again.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/people":{"get":{"operationId":"listPeople","tags":["people"],"summary":"List 人物 (people)","description":"People found in the library: guests of interviews and podcasts, hosts, the person an article or video is about, and authors of their own essays, posts and talks. Most items first. New items are read by 人物识别 after their summary; older ones were scanned once. Answer 「库里有哪些访谈过 X 的内容」 with `q` here, then GET /people/{id}.\n\nRequires scope `people:read`.","x-scope":"people:read","security":[{"apiKey":[]}],"parameters":[{"name":"q","in":"query","required":false,"description":"Words in the name, an alias or the description.","schema":{"type":"string","maxLength":80}},{"name":"role","in":"query","required":false,"description":"Only people with an item in this role.","schema":{"type":"string","enum":["interviewee","host","author","subject","受访","嘉宾","主持","作者","主题"]}},{"name":"cursor","in":"query","required":false,"description":"`nextCursor` of the previous page.","schema":{"type":"string","maxLength":20}},{"name":"limit","in":"query","required":false,"schema":{"default":50,"type":"integer","minimum":1,"maximum":200}}],"responses":{"200":{"description":"One page of people.","content":{"application/json":{"schema":{"type":"object","properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/Person"}},"nextCursor":{"type":["string","null"],"description":"Pass as `cursor` for the next page; null on the last page."},"total":{"type":"integer","description":"People matching `q` and `role`."}},"required":["items","nextCursor","total"]}}}},"400":{"description":"A parameter is malformed (`invalid_query`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Missing, unknown, revoked or expired API key (`api_key_required`, `invalid_api_key`, `api_key_revoked`, `api_key_expired`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"The key lacks the scope this operation needs (`insufficient_scope`; `required` names it).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Over the key's per-minute limit (`rate_limited`). Wait for `Retry-After` seconds.","headers":{"Retry-After":{"description":"Seconds until the key may call again.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"post":{"operationId":"addPerson","tags":["people"],"summary":"Add a person (or find them)","description":"Safe to call whenever someone comes up: a person whose name or alias matches (case, spaces, punctuation and 间隔号 ignored) is returned as is, gaining new aliases and a description it lacked. Send `items` to link items in the same call.\n\nRequires scope `people:write`.","x-scope":"people:write","security":[{"apiKey":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PersonInput"}}}},"responses":{"200":{"description":"Already there.","content":{"application/json":{"schema":{"type":"object","properties":{"created":{"type":"boolean","const":false},"person":{"$ref":"#/components/schemas/PersonDetail"}},"required":["created","person"]}}}},"201":{"description":"A new person.","content":{"application/json":{"schema":{"type":"object","properties":{"created":{"type":"boolean","const":true},"person":{"$ref":"#/components/schemas/PersonDetail"}},"required":["created","person"]}}}},"400":{"description":"Invalid input (`invalid_person_input`, `invalid_name`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Missing, unknown, revoked or expired API key (`api_key_required`, `invalid_api_key`, `api_key_revoked`, `api_key_expired`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"The key lacks the scope this operation needs (`insufficient_scope`; `required` names it).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"An `items` entry is not in the library (`item_not_found`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Over the key's per-minute limit (`rate_limited`). Wait for `Retry-After` seconds.","headers":{"Retry-After":{"description":"Seconds until the key may call again.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/people/{id}":{"get":{"operationId":"getPerson","tags":["people"],"summary":"Read one person and their items","description":"The person with every item about them, newest first: title, source, the hub link and the original, their role and the line that shows it. Read an item's text with `/items/{id}`.\n\nRequires scope `people:read`.","x-scope":"people:read","security":[{"apiKey":[]}],"parameters":[{"name":"id","in":"path","required":true,"description":"The person's id from GET /people.","schema":{"type":"string"}}],"responses":{"200":{"description":"The person.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PersonDetail"}}}},"401":{"description":"Missing, unknown, revoked or expired API key (`api_key_required`, `invalid_api_key`, `api_key_revoked`, `api_key_expired`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"The key lacks the scope this operation needs (`insufficient_scope`; `required` names it).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"No such person (`person_not_found`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Over the key's per-minute limit (`rate_limited`). Wait for `Retry-After` seconds.","headers":{"Retry-After":{"description":"Seconds until the key may call again.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"patch":{"operationId":"updatePerson","tags":["people"],"summary":"Rename a person, or change aliases or description","description":"Only the fields sent change. A new `name` keeps the old one as an alias; `aliases` replaces the whole list. A name or alias that is already another person's is refused: merge the two instead.\n\nRequires scope `people:write`.","x-scope":"people:write","security":[{"apiKey":[]}],"parameters":[{"name":"id","in":"path","required":true,"description":"The person's id from GET /people.","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PersonPatch"}}}},"responses":{"200":{"description":"The person as stored now.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PersonDetail"}}}},"400":{"description":"Invalid input (`invalid_person_input`, `invalid_name`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Missing, unknown, revoked or expired API key (`api_key_required`, `invalid_api_key`, `api_key_revoked`, `api_key_expired`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"The key lacks the scope this operation needs (`insufficient_scope`; `required` names it).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"No such person (`person_not_found`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"That name is another person's (`name_taken`; `personId` is theirs).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Over the key's per-minute limit (`rate_limited`). Wait for `Retry-After` seconds.","headers":{"Retry-After":{"description":"Seconds until the key may call again.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/people/{id}/merge":{"post":{"operationId":"mergePeople","tags":["people"],"summary":"Merge two records of the same person","description":"`personId`'s items, names and aliases move to this person and that record is removed (an item both had keeps this person's link). Use it when 人物识别 kept one human as two, e.g. under a Chinese and an English name.\n\nRequires scope `people:write`.","x-scope":"people:write","security":[{"apiKey":[]}],"parameters":[{"name":"id","in":"path","required":true,"description":"The person's id from GET /people.","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PersonMerge"}}}},"responses":{"200":{"description":"This person as stored now.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PersonDetail"}}}},"400":{"description":"Invalid input (`invalid_merge_input`, `same_person`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Missing, unknown, revoked or expired API key (`api_key_required`, `invalid_api_key`, `api_key_revoked`, `api_key_expired`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"The key lacks the scope this operation needs (`insufficient_scope`; `required` names it).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"No such person, either one (`person_not_found`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Over the key's per-minute limit (`rate_limited`). Wait for `Retry-After` seconds.","headers":{"Retry-After":{"description":"Seconds until the key may call again.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/people/{id}/items":{"post":{"operationId":"addPersonItem","tags":["people"],"summary":"Link an item to a person","description":"Says the item is about this person, in this role (手动). A link removed earlier comes back; an automatic one becomes manual.\n\nRequires scope `people:write`.","x-scope":"people:write","security":[{"apiKey":[]}],"parameters":[{"name":"id","in":"path","required":true,"description":"The person's id from GET /people.","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PersonItemInput"}}}},"responses":{"200":{"description":"The person as stored now.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PersonDetail"}}}},"400":{"description":"Invalid input (`invalid_person_item_input`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Missing, unknown, revoked or expired API key (`api_key_required`, `invalid_api_key`, `api_key_revoked`, `api_key_expired`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"The key lacks the scope this operation needs (`insufficient_scope`; `required` names it).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"No such person (`person_not_found`) or item (`item_not_found`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Over the key's per-minute limit (`rate_limited`). Wait for `Retry-After` seconds.","headers":{"Retry-After":{"description":"Seconds until the key may call again.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/people/{id}/items/{itemId}":{"patch":{"operationId":"updatePersonItem","tags":["people"],"summary":"Change or remove a person's item","description":"`role` changes their role in it; `removed: true` takes the item off this person (人物识别 will not link it again), `removed: false` puts it back.\n\nRequires scope `people:write`.","x-scope":"people:write","security":[{"apiKey":[]}],"parameters":[{"name":"id","in":"path","required":true,"description":"The person's id.","schema":{"type":"string"}},{"name":"itemId","in":"path","required":true,"description":"The item's id.","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PersonItemPatch"}}}},"responses":{"200":{"description":"The person as stored now.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PersonDetail"}}}},"400":{"description":"Invalid input (`invalid_person_item_input`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Missing, unknown, revoked or expired API key (`api_key_required`, `invalid_api_key`, `api_key_revoked`, `api_key_expired`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"The key lacks the scope this operation needs (`insufficient_scope`; `required` names it).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"No such person (`person_not_found`), or the item is not theirs (`person_item_not_found`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Over the key's per-minute limit (`rate_limited`). Wait for `Retry-After` seconds.","headers":{"Retry-After":{"description":"Seconds until the key may call again.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/recipes":{"get":{"operationId":"listRecipes","tags":["recipes"],"summary":"List 菜谱 (recipes)","description":"Dishes worked out from collected posts and videos (one per dish: a video that cooks five has five), newest first, with the owner's 想做 / 做过 marks. Answer 「我收藏过的 X 怎么做」「用鸡翅能做什么」 with `q` here, then GET /recipes/{id} for ingredients and steps. Use this, not `/search`, for recipes.\n\nA dish can have several versions (PIH's own and agents' from POST /recipes); each dish is listed once, as the version it shows, with `versions` saying how many it has.\n\nRequires scope `recipes:read`.","x-scope":"recipes:read","security":[{"apiKey":[]}],"parameters":[{"name":"q","in":"query","required":false,"description":"Words in the dish name, an ingredient, or the source post's title.","schema":{"type":"string","maxLength":100}},{"name":"item","in":"query","required":false,"description":"Only the dishes worked out from this item (its id); after `POST /recipes/import`, poll this.","schema":{"type":"string","minLength":1,"maxLength":64}},{"name":"want","in":"query","required":false,"description":"`true`: only dishes marked 想做.","schema":{"type":"boolean"}},{"name":"cooked","in":"query","required":false,"description":"`true`: only dishes cooked at least once.","schema":{"type":"boolean"}},{"name":"since","in":"query","required":false,"description":"Worked out at or after this time (ISO date or date-time).","schema":{"type":"string"}},{"name":"cursor","in":"query","required":false,"description":"`nextCursor` of the previous page.","schema":{"type":"string","maxLength":20}},{"name":"limit","in":"query","required":false,"description":"Page size, 1-100 (default 20).","schema":{"default":20,"type":"integer","minimum":1,"maximum":100}}],"responses":{"200":{"description":"One page of recipes.","content":{"application/json":{"schema":{"type":"object","properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/Recipe"}},"nextCursor":{"type":["string","null"],"description":"Pass as `cursor` for the next page; null on the last page."}},"required":["items","nextCursor"]}}}},"400":{"description":"`since` is malformed (`invalid_time`), or another parameter is (`invalid_query`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Missing, unknown, revoked or expired API key (`api_key_required`, `invalid_api_key`, `api_key_revoked`, `api_key_expired`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"The key lacks the scope this operation needs (`insufficient_scope`; `required` names it).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Over the key's per-minute limit (`rate_limited`). Wait for `Retry-After` seconds.","headers":{"Retry-After":{"description":"Seconds until the key may call again.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"post":{"operationId":"addRecipeVersion","tags":["recipes"],"summary":"Write your own recipe version for a saved post","description":"When you have read a saved post or video yourself (for example while talking with the owner), write the recipe you worked out into the library as your own version of that dish: the same shape PIH's own extraction stores (title, ingredients with amounts and groups, steps with the second each is shown, servings, time, tips). It sits next to PIH's version instead of replacing it; sending the same dish again with the same key replaces only your earlier version and keeps its id. The dish keeps showing PIH's version when there is one, unless the owner picks yours on recipes.jiajun.fyi; it shows yours when PIH has none.\n\nAn item cut into several dishes needs `part` (from GET /recipes?item=) or `startS` + `endS`; step times must fall inside that stretch. Validation is strict: unknown fields, tags outside the vocabulary and steps outside the dish are rejected with `details` per field. To have PIH work it out instead, use POST /recipes/import.\n\nRequires scope `recipes:write`.","x-scope":"recipes:write","security":[{"apiKey":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RecipeVersionInput"}}}},"responses":{"201":{"description":"Written (or your identical earlier version kept).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RecipeVersionWritten"}}}},"400":{"description":"Invalid input (`invalid_recipe_input`; `details` lists each field), or the item has several dishes and no `part` was sent (`part_required`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Missing, unknown, revoked or expired API key (`api_key_required`, `invalid_api_key`, `api_key_revoked`, `api_key_expired`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"The key lacks the scope this operation needs (`insufficient_scope`; `required` names it).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"No such item (`item_not_found`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Over the key's per-minute limit (`rate_limited`). Wait for `Retry-After` seconds.","headers":{"Retry-After":{"description":"Seconds until the key may call again.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/recipes/{id}":{"get":{"operationId":"getRecipe","tags":["recipes"],"summary":"Read one recipe","description":"The dish with every ingredient (amount and group), the steps (with the second in the video where each is shown), the owner's marks and cook log, and the item it came from. Read that item's text or transcript with `/items/{id}/content`. Any version's id reads that version; `shown` says whether it is the one the dish shows.\n\nRequires scope `recipes:read`.","x-scope":"recipes:read","security":[{"apiKey":[]}],"parameters":[{"name":"id","in":"path","required":true,"description":"The 菜谱短 ID from GET /recipes.","schema":{"type":"string"}}],"responses":{"200":{"description":"The recipe.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RecipeDetail"}}}},"401":{"description":"Missing, unknown, revoked or expired API key (`api_key_required`, `invalid_api_key`, `api_key_revoked`, `api_key_expired`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"The key lacks the scope this operation needs (`insufficient_scope`; `required` names it).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"No such recipe (`recipe_not_found`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Over the key's per-minute limit (`rate_limited`). Wait for `Retry-After` seconds.","headers":{"Retry-After":{"description":"Seconds until the key may call again.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/recipes/{id}/versions":{"get":{"operationId":"listRecipeVersions","tags":["recipes"],"summary":"List every version of a dish","description":"All current versions of the dish that this id belongs to, the shown one first, each with who made it (`producer`), its ingredients and steps. Versions sit side by side: PIH redoing its own never touches an agent's, and an agent's never replaces PIH's. The dish shows the owner's pick, else PIH's version, else the newest agent's. Marks (想做 / 做过 / 排餐) belong to the dish, so every version carries the same ones.\n\nRequires scope `recipes:read`.","x-scope":"recipes:read","security":[{"apiKey":[]}],"parameters":[{"name":"id","in":"path","required":true,"description":"The 菜谱短 ID from GET /recipes.","schema":{"type":"string"}}],"responses":{"200":{"description":"The versions.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RecipeVersionList"}}}},"401":{"description":"Missing, unknown, revoked or expired API key (`api_key_required`, `invalid_api_key`, `api_key_revoked`, `api_key_expired`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"The key lacks the scope this operation needs (`insufficient_scope`; `required` names it).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"No such recipe (`recipe_not_found`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Over the key's per-minute limit (`rate_limited`). Wait for `Retry-After` seconds.","headers":{"Retry-After":{"description":"Seconds until the key may call again.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/recipes/{id}/want":{"put":{"operationId":"setRecipeWant","tags":["recipes"],"summary":"Mark 想做 (want to cook), or take it back","description":"The same mark as the heart on recipes.jiajun.fyi. Setting it twice changes nothing.\n\nRequires scope `recipes:write`.","x-scope":"recipes:write","security":[{"apiKey":[]}],"parameters":[{"name":"id","in":"path","required":true,"description":"The 菜谱短 ID from GET /recipes.","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RecipeWant"}}}},"responses":{"200":{"description":"The recipe as it is now.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RecipeDetail"}}}},"400":{"description":"Invalid input (`invalid_recipe_input`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Missing, unknown, revoked or expired API key (`api_key_required`, `invalid_api_key`, `api_key_revoked`, `api_key_expired`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"The key lacks the scope this operation needs (`insufficient_scope`; `required` names it).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"No such recipe (`recipe_not_found`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Over the key's per-minute limit (`rate_limited`). Wait for `Retry-After` seconds.","headers":{"Retry-After":{"description":"Seconds until the key may call again.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/recipes/{id}/cooked":{"post":{"operationId":"addRecipeCooked","tags":["recipes"],"summary":"Log a 做过 (cooked it)","description":"One more entry in the dish's cook log, today (Pacific) unless `date` says an earlier day, with an optional note. Each call adds one: do not retry a call that answered.\n\nRequires scope `recipes:write`.","x-scope":"recipes:write","security":[{"apiKey":[]}],"parameters":[{"name":"id","in":"path","required":true,"description":"The 菜谱短 ID from GET /recipes.","schema":{"type":"string"}}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RecipeCooked"}}}},"responses":{"201":{"description":"Logged; the recipe with its cook log.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RecipeDetail"}}}},"400":{"description":"Invalid input, or a date in the future (`invalid_recipe_input`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Missing, unknown, revoked or expired API key (`api_key_required`, `invalid_api_key`, `api_key_revoked`, `api_key_expired`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"The key lacks the scope this operation needs (`insufficient_scope`; `required` names it).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"No such recipe (`recipe_not_found`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Over the key's per-minute limit (`rate_limited`). Wait for `Retry-After` seconds.","headers":{"Retry-After":{"description":"Seconds until the key may call again.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/recipes/{id}/plan":{"post":{"operationId":"planRecipe","tags":["recipes"],"summary":"Plan the dish for a day (排餐), or take it off","description":"Puts the dish on a day's breakfast, lunch or dinner on the recipes site's week menu; its ingredients join that week's grocery list. `remove: true` takes it off that day again.\n\nRequires scope `recipes:write`.","x-scope":"recipes:write","security":[{"apiKey":[]}],"parameters":[{"name":"id","in":"path","required":true,"description":"The 菜谱短 ID from GET /recipes.","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RecipePlan"}}}},"responses":{"200":{"description":"The recipe with the days it is planned for (`plans`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RecipeDetail"}}}},"400":{"description":"Invalid input (`invalid_recipe_input`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Missing, unknown, revoked or expired API key (`api_key_required`, `invalid_api_key`, `api_key_revoked`, `api_key_expired`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"The key lacks the scope this operation needs (`insufficient_scope`; `required` names it).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"No such recipe (`recipe_not_found`), or `remove` and it is not planned that day (`plan_not_found`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Over the key's per-minute limit (`rate_limited`). Wait for `Retry-After` seconds.","headers":{"Retry-After":{"description":"Seconds until the key may call again.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/recipes/grocery":{"post":{"operationId":"addGroceryItem","tags":["recipes"],"summary":"Add a line to the grocery list (采购清单)","description":"A hand-added line on a week's grocery list on recipes.jiajun.fyi, next to the ingredients of the planned dishes (those are added by planning the dish).\n\nRequires scope `recipes:write`.","x-scope":"recipes:write","security":[{"apiKey":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/GroceryInput"}}}},"responses":{"201":{"description":"Added.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/GroceryItem"}}}},"400":{"description":"Invalid input (`invalid_recipe_input`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Missing, unknown, revoked or expired API key (`api_key_required`, `invalid_api_key`, `api_key_revoked`, `api_key_expired`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"The key lacks the scope this operation needs (`insufficient_scope`; `required` names it).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Over the key's per-minute limit (`rate_limited`). Wait for `Retry-After` seconds.","headers":{"Retry-After":{"description":"Seconds until the key may call again.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/recipes/import":{"post":{"operationId":"importRecipes","tags":["recipes"],"summary":"Turn saved posts or videos into recipes","description":"For library items that are cooking content but have no recipe yet. Each one is read the same way the library reads new posts (the transcript with its times, or the on-screen text or pictures when nobody speaks): a video that cooks several dishes becomes one recipe per dish, each step with the second it is shown, linked to the item. Asking for an item is taken as saying it is a recipe. Then poll `GET /recipes?item={id}`; when nothing shows after a few minutes the model found no dish in it.\n\nRequires scope `recipes:write`.","x-scope":"recipes:write","security":[{"apiKey":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RecipeImport"}}}},"responses":{"202":{"description":"Accepted; one outcome per item, in the order sent.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RecipeImportResult"}}}},"400":{"description":"Invalid input (`invalid_recipe_input`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Missing, unknown, revoked or expired API key (`api_key_required`, `invalid_api_key`, `api_key_revoked`, `api_key_expired`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"The key lacks the scope this operation needs (`insufficient_scope`; `required` names it).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Over the key's per-minute limit (`rate_limited`). Wait for `Retry-After` seconds.","headers":{"Retry-After":{"description":"Seconds until the key may call again.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/feedback":{"post":{"operationId":"addFeedback","tags":["feedback"],"summary":"Report a problem, a missing capability or an idea","description":"Any valid key may call this, whatever its scopes: when a call fails, answers something wrong, or the API cannot do what the user asked (including a missing scope), send what you tried, the error and your logs. The server adds which key sent it, your User-Agent and the key's last 30 calls, so you need not copy them. The owner is told and may answer through `resolution`.\n\nSending the same `kind` and `title` again while the first is open (within 24 hours) updates that report (one more occurrence, your latest logs) and answers 200 with `deduplicated: true`. Limits on top of the key's per-minute limit: 10 reports an hour and 30 a Pacific day per key, 200 a day for the whole library; over them, 429 `feedback_rate_limited` with `Retry-After`.\n\nAny valid key; no scope needed.","x-scope":"any key","security":[{"apiKey":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/FeedbackInput"}}}},"responses":{"200":{"description":"A repeat of an open report; that one was updated.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FeedbackFiled"}}}},"201":{"description":"Filed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FeedbackFiled"}}}},"400":{"description":"Invalid input (`invalid_feedback_input`; `details` lists each field, e.g. logs over 32 KB).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Missing, unknown, revoked or expired API key (`api_key_required`, `invalid_api_key`, `api_key_revoked`, `api_key_expired`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Too many reports (`feedback_rate_limited`; wait `Retry-After` seconds) or over the key's per-minute limit (`rate_limited`).","headers":{"Retry-After":{"description":"Seconds until the key may call again.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"get":{"operationId":"listFeedback","tags":["feedback"],"summary":"List agent feedback","description":"Reports agents sent, newest first, without logs and calls (read one for those).\n\nRequires scope `feedback:read`.","x-scope":"feedback:read","security":[{"apiKey":[]}],"parameters":[{"name":"status","in":"query","required":false,"description":"Only reports in this status.","schema":{"type":"string","enum":["open","resolved","wont_fix","duplicate"]}},{"name":"kind","in":"query","required":false,"description":"Only reports of this kind.","schema":{"type":"string","enum":["bug","missing","idea","note"]}},{"name":"priority","in":"query","required":false,"description":"Only reports with this priority.","schema":{"type":"string","enum":["p0","p1","p2"]}},{"name":"tag","in":"query","required":false,"description":"Only reports carrying this tag.","schema":{"type":"string","minLength":1,"maxLength":40}},{"name":"since","in":"query","required":false,"description":"Only reports seen (sent or repeated) at or after this ISO 8601 date or date-time.","schema":{"type":"string"}},{"name":"cursor","in":"query","required":false,"description":"`nextCursor` of the previous page.","schema":{"type":"string","maxLength":20}},{"name":"limit","in":"query","required":false,"description":"Page size, 1-100 (default 20).","schema":{"default":20,"type":"integer","minimum":1,"maximum":100}}],"responses":{"200":{"description":"One page of reports.","content":{"application/json":{"schema":{"type":"object","properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/Feedback"}},"nextCursor":{"type":["string","null"],"description":"Pass as `cursor` for the next page; null on the last page."}},"required":["items","nextCursor"]}}}},"400":{"description":"A parameter is malformed (`invalid_query`, `invalid_time`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Missing, unknown, revoked or expired API key (`api_key_required`, `invalid_api_key`, `api_key_revoked`, `api_key_expired`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"The key lacks the scope this operation needs (`insufficient_scope`; `required` names it).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Over the key's per-minute limit (`rate_limited`). Wait for `Retry-After` seconds.","headers":{"Retry-After":{"description":"Seconds until the key may call again.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/feedback/{id}":{"get":{"operationId":"getFeedback","tags":["feedback"],"summary":"Read one report (by id or FB-12) with its logs and calls","description":"The whole report: message, the failing request, context, the agent's logs and the key's last 30 calls before it was sent.\n\nRequires scope `feedback:read`.","x-scope":"feedback:read","security":[{"apiKey":[]}],"parameters":[{"name":"id","in":"path","required":true,"description":"The report's id, or its `ref` (`FB-12`; `FB-12-<slug>` also works).","schema":{"type":"string"}}],"responses":{"200":{"description":"The report.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FeedbackDetail"}}}},"401":{"description":"Missing, unknown, revoked or expired API key (`api_key_required`, `invalid_api_key`, `api_key_revoked`, `api_key_expired`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"The key lacks the scope this operation needs (`insufficient_scope`; `required` names it).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"No such report (`feedback_not_found`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Over the key's per-minute limit (`rate_limited`). Wait for `Retry-After` seconds.","headers":{"Retry-After":{"description":"Seconds until the key may call again.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"patch":{"operationId":"updateFeedback","tags":["feedback"],"summary":"Sort, resolve or merge a report (by id or FB-12)","description":"Only the fields sent change. Set `status` to `resolved` or `wont_fix` with a `resolution` saying what was done or why not (`open` reopens it); re-file with `kind` and `title`; sort with `priority` and `tags` (the whole list). `duplicateOf` folds this report into another one: its status becomes `duplicate`, its occurrences are added to that report's, and reports already folded into this one move along. Setting a `status` on a duplicate undoes it.\n\nRequires scope `feedback:read`.","x-scope":"feedback:read","security":[{"apiKey":[]}],"parameters":[{"name":"id","in":"path","required":true,"description":"The report's id, or its `ref` (`FB-12`; `FB-12-<slug>` also works).","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/FeedbackPatch"}}}},"responses":{"200":{"description":"The report as stored now.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FeedbackDetail"}}}},"400":{"description":"Invalid input (`invalid_feedback_input`), or a report named as its own duplicate (`invalid_duplicate`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Missing, unknown, revoked or expired API key (`api_key_required`, `invalid_api_key`, `api_key_revoked`, `api_key_expired`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"The key lacks the scope this operation needs (`insufficient_scope`; `required` names it).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"No such report, this one or `duplicateOf` (`feedback_not_found`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Over the key's per-minute limit (`rate_limited`). Wait for `Retry-After` seconds.","headers":{"Retry-After":{"description":"Seconds until the key may call again.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/todos":{"get":{"operationId":"listTodos","tags":["todos"],"summary":"List 待办 (todos)","description":"The owner's todo list. Open ones come first, most urgent (`p0`) first, then by due date (none last), then the owner's hand order; done and dropped ones follow, most recently closed first. 「我还有什么没做」 is `status=open`.\n\nRequires scope `todos:read`.","x-scope":"todos:read","security":[{"apiKey":[]}],"parameters":[{"name":"status","in":"query","required":false,"description":"Only `open`, `done` or `dropped`.","schema":{"type":"string","enum":["open","done","dropped"]}},{"name":"priority","in":"query","required":false,"description":"Only this priority; `none` for todos without one.","schema":{"type":"string","enum":["p0","p1","p2","none"]}},{"name":"area","in":"query","required":false,"description":"Only `work` or only `life` todos.","schema":{"type":"string","enum":["work","life"]}},{"name":"project","in":"query","required":false,"description":"Only todos of this project (exact name); `none` for todos without one.","schema":{"type":"string","minLength":1,"maxLength":60}},{"name":"tag","in":"query","required":false,"description":"Only todos with this tag.","schema":{"type":"string","minLength":1,"maxLength":40}},{"name":"q","in":"query","required":false,"description":"Words in the title or note.","schema":{"type":"string","maxLength":100}},{"name":"item","in":"query","required":false,"description":"Only todos about this library item (its id from /items or /search).","schema":{"type":"string","minLength":1,"maxLength":64}},{"name":"since","in":"query","required":false,"description":"Changed at or after this time (ISO date or date-time, or `24h`, `7d`, `2w`).","schema":{"type":"string"}},{"name":"cursor","in":"query","required":false,"description":"`nextCursor` of the previous page.","schema":{"type":"string","maxLength":20}},{"name":"limit","in":"query","required":false,"description":"Page size, 1-100 (default 20).","schema":{"default":20,"type":"integer","minimum":1,"maximum":100}}],"responses":{"200":{"description":"One page of todos.","content":{"application/json":{"schema":{"type":"object","properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/Todo"}},"nextCursor":{"type":["string","null"],"description":"Pass as `cursor` for the next page; null on the last page."}},"required":["items","nextCursor"]}}}},"400":{"description":"`since` is malformed (`invalid_time`), or another parameter is (`invalid_query`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Missing, unknown, revoked or expired API key (`api_key_required`, `invalid_api_key`, `api_key_revoked`, `api_key_expired`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"The key lacks the scope this operation needs (`insufficient_scope`; `required` names it).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Over the key's per-minute limit (`rate_limited`). Wait for `Retry-After` seconds.","headers":{"Retry-After":{"description":"Seconds until the key may call again.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"post":{"operationId":"addTodo","tags":["todos"],"summary":"Add a todo","description":"A new open todo, last in its priority group. It is marked as added by this key's name, which the owner sees on the hub. A todo about something in the library (watch this video, cook this recipe) names it with `itemId` instead of pasting its link into `note`: `GET /todos?item=` then finds it from the item.\n\nRequires scope `todos:write`.","x-scope":"todos:write","security":[{"apiKey":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TodoInput"}}}},"responses":{"201":{"description":"The new todo.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Todo"}}}},"400":{"description":"Invalid input (`invalid_todo_input`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Missing, unknown, revoked or expired API key (`api_key_required`, `invalid_api_key`, `api_key_revoked`, `api_key_expired`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"The key lacks the scope this operation needs (`insufficient_scope`; `required` names it).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"`itemId` names no library item (`item_not_found`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Over the key's per-minute limit (`rate_limited`). Wait for `Retry-After` seconds.","headers":{"Retry-After":{"description":"Seconds until the key may call again.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/todos/{id}":{"patch":{"operationId":"updateTodo","tags":["todos"],"summary":"Change a todo, complete, drop or reopen it","description":"Only the fields sent change; `null` clears `note`, `priority` or `dueDate`. `status: done` completes it, `dropped` gives it up, `open` brings it back. Nothing deletes a todo.\n\nRequires scope `todos:write`.","x-scope":"todos:write","security":[{"apiKey":[]}],"parameters":[{"name":"id","in":"path","required":true,"description":"The todo's id from GET /todos or POST /todos.","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TodoPatch"}}}},"responses":{"200":{"description":"The todo as stored now.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Todo"}}}},"400":{"description":"Invalid input (`invalid_todo_input`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Missing, unknown, revoked or expired API key (`api_key_required`, `invalid_api_key`, `api_key_revoked`, `api_key_expired`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"The key lacks the scope this operation needs (`insufficient_scope`; `required` names it).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"No such todo (`todo_not_found`), or `itemId` names no library item (`item_not_found`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Over the key's per-minute limit (`rate_limited`). Wait for `Retry-After` seconds.","headers":{"Retry-After":{"description":"Seconds until the key may call again.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/card-notes":{"get":{"operationId":"listCardNotes","tags":["card_notes"],"summary":"List 卡片笔记 (card notes), newest first","description":"The owner's 卡片盒 (card box), newest card first; cards taken back are left out. Filter by tag with `tag`; repeat it (`?tag=读书&tag=产品`) for cards that carry every one. GET /card-notes/tags lists the tags in use.\n\nRequires scope `card_notes:read`.","x-scope":"card_notes:read","security":[{"apiKey":[]}],"parameters":[{"name":"tag","in":"query","required":false,"description":"Only cards with this tag (case-insensitive, a leading `#` ignored). Repeat for cards with all of them, up to 10.","schema":{"maxItems":10,"type":"array","items":{"type":"string"}}},{"name":"cursor","in":"query","required":false,"description":"`nextCursor` of the previous page.","schema":{"type":"string","maxLength":20}},{"name":"limit","in":"query","required":false,"description":"Page size, 1-100 (default 20).","schema":{"default":20,"type":"integer","minimum":1,"maximum":100}}],"responses":{"200":{"description":"One page of cards.","content":{"application/json":{"schema":{"type":"object","properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/CardNote"}},"nextCursor":{"type":["string","null"],"description":"Pass as `cursor` for the next page; null on the last page."}},"required":["items","nextCursor"]}}}},"400":{"description":"A parameter is malformed (`invalid_query`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Missing, unknown, revoked or expired API key (`api_key_required`, `invalid_api_key`, `api_key_revoked`, `api_key_expired`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"The key lacks the scope this operation needs (`insufficient_scope`; `required` names it).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Over the key's per-minute limit (`rate_limited`). Wait for `Retry-After` seconds.","headers":{"Retry-After":{"description":"Seconds until the key may call again.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"post":{"operationId":"addCardNote","tags":["card_notes"],"summary":"Add a card note","description":"A new card in the box: `title` required, `body` (Markdown) and `tags` optional. Tags are trimmed, a leading `#` is dropped and repeats are kept once; check GET /card-notes/tags and reuse a spelling already there. The card is marked as written by this key's name.\n\nRequires scope `card_notes:write`.","x-scope":"card_notes:write","security":[{"apiKey":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CardNoteInput"}}}},"responses":{"201":{"description":"The new card.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CardNote"}}}},"400":{"description":"Invalid input (`invalid_card_note_input`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Missing, unknown, revoked or expired API key (`api_key_required`, `invalid_api_key`, `api_key_revoked`, `api_key_expired`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"The key lacks the scope this operation needs (`insufficient_scope`; `required` names it).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Over the key's per-minute limit (`rate_limited`). Wait for `Retry-After` seconds.","headers":{"Retry-After":{"description":"Seconds until the key may call again.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/card-notes/tags":{"get":{"operationId":"listCardNoteTags","tags":["card_notes"],"summary":"List the tags in the card box, with counts","description":"Every tag on a card still in the box, most used first. Reuse these spellings when tagging a new card.\n\nRequires scope `card_notes:read`.","x-scope":"card_notes:read","security":[{"apiKey":[]}],"responses":{"200":{"description":"Tags, most used first.","content":{"application/json":{"schema":{"type":"object","properties":{"tags":{"type":"array","items":{"type":"object","properties":{"tag":{"type":"string"},"count":{"type":"integer"}},"required":["tag","count"]}}},"required":["tags"]}}}},"401":{"description":"Missing, unknown, revoked or expired API key (`api_key_required`, `invalid_api_key`, `api_key_revoked`, `api_key_expired`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"The key lacks the scope this operation needs (`insufficient_scope`; `required` names it).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Over the key's per-minute limit (`rate_limited`). Wait for `Retry-After` seconds.","headers":{"Retry-After":{"description":"Seconds until the key may call again.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/card-notes/{id}":{"get":{"operationId":"getCardNote","tags":["card_notes"],"summary":"Read one card note","description":"One card, also after it was taken back (`removedAt` says so).\n\nRequires scope `card_notes:read`.","x-scope":"card_notes:read","security":[{"apiKey":[]}],"parameters":[{"name":"id","in":"path","required":true,"description":"The card's id from GET /card-notes or POST /card-notes.","schema":{"type":"string"}}],"responses":{"200":{"description":"The card.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CardNote"}}}},"401":{"description":"Missing, unknown, revoked or expired API key (`api_key_required`, `invalid_api_key`, `api_key_revoked`, `api_key_expired`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"The key lacks the scope this operation needs (`insufficient_scope`; `required` names it).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"No such card (`card_note_not_found`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Over the key's per-minute limit (`rate_limited`). Wait for `Retry-After` seconds.","headers":{"Retry-After":{"description":"Seconds until the key may call again.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"patch":{"operationId":"updateCardNote","tags":["card_notes"],"summary":"Change a card note, or take it back (撤销)","description":"Only the fields sent change; `tags` replaces the whole list. `removed: true` takes the card back: it leaves the list and the tag counts, and is still readable by id; `removed: false` brings it back. Nothing in this API deletes a card.\n\nRequires scope `card_notes:write`.","x-scope":"card_notes:write","security":[{"apiKey":[]}],"parameters":[{"name":"id","in":"path","required":true,"description":"The card's id from GET /card-notes or POST /card-notes.","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CardNotePatch"}}}},"responses":{"200":{"description":"The card as stored now.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CardNote"}}}},"400":{"description":"Invalid input (`invalid_card_note_input`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Missing, unknown, revoked or expired API key (`api_key_required`, `invalid_api_key`, `api_key_revoked`, `api_key_expired`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"The key lacks the scope this operation needs (`insufficient_scope`; `required` names it).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"No such card (`card_note_not_found`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Over the key's per-minute limit (`rate_limited`). Wait for `Retry-After` seconds.","headers":{"Retry-After":{"description":"Seconds until the key may call again.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/tutu/media":{"get":{"operationId":"listTutuMedia","tags":["tutu"],"summary":"List 图图's photos and videos","description":"Every photo and video on 图图's growth timeline (成长时间线, tutu.jiajun.fyi), newest first: his Brightwheel media and the family photos of him, flat (similar shots are not folded). Page with `cursor` until `nextCursor` is null. Files are signed links (`url`, `thumbUrl`) a player or TV can fetch without the key; they do not count against the key's limit.\n\nRequires scope `tutu:read`.","x-scope":"tutu:read","security":[{"apiKey":[]}],"parameters":[{"name":"kind","in":"query","required":false,"description":"Only photos (`image`) or only videos (`video`).","schema":{"type":"string","enum":["image","video"]}},{"name":"cursor","in":"query","required":false,"description":"`nextCursor` of the previous page.","schema":{"type":"string","maxLength":100}},{"name":"limit","in":"query","required":false,"description":"Page size, 1-500 (default 200).","schema":{"default":200,"type":"integer","minimum":1,"maximum":500}}],"responses":{"200":{"description":"One page, newest first.","content":{"application/json":{"schema":{"type":"object","properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/TutuMedium"}},"nextCursor":{"type":["string","null"],"description":"Pass as `cursor` for the next page; null on the last page."},"linksExpireAt":{"type":"string","format":"date-time","description":"When `url` and `thumbUrl` stop working (6 to 12 hours from now). Links stay the same within a 6-hour window, so they can be cached by URL; afterwards list again or read the one medium."}},"required":["items","nextCursor","linksExpireAt"]}}}},"400":{"description":"`cursor` is not one this route handed out (`invalid_cursor`), or another parameter is malformed (`invalid_query`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Missing, unknown, revoked or expired API key (`api_key_required`, `invalid_api_key`, `api_key_revoked`, `api_key_expired`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"The key lacks the scope this operation needs (`insufficient_scope`; `required` names it).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Over the key's per-minute limit (`rate_limited`). Wait for `Retry-After` seconds.","headers":{"Retry-After":{"description":"Seconds until the key may call again.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/tutu/media/{id}":{"get":{"operationId":"getTutuMedium","tags":["tutu"],"summary":"Get one of 图图's photos or videos","description":"One photo or video by its 照片短 ID, with fresh signed links: read it again when a link from the list has expired.\n\nRequires scope `tutu:read`.","x-scope":"tutu:read","security":[{"apiKey":[]}],"parameters":[{"name":"id","in":"path","required":true,"description":"The 照片短 ID from the list.","schema":{"type":"string","pattern":"^[0-9A-Za-z]{8}$"}}],"responses":{"200":{"description":"The photo or video.","content":{"application/json":{"schema":{"type":"object","properties":{"medium":{"$ref":"#/components/schemas/TutuMedium"},"linksExpireAt":{"type":"string","format":"date-time","description":"When `url` and `thumbUrl` stop working (6 to 12 hours from now). Links stay the same within a 6-hour window, so they can be cached by URL; afterwards list again or read the one medium."}},"required":["medium","linksExpireAt"]}}}},"401":{"description":"Missing, unknown, revoked or expired API key (`api_key_required`, `invalid_api_key`, `api_key_revoked`, `api_key_expired`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"The key lacks the scope this operation needs (`insufficient_scope`; `required` names it).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"No photo or video with this id on the timeline (`medium_not_found`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Over the key's per-minute limit (`rate_limited`). Wait for `Retry-After` seconds.","headers":{"Retry-After":{"description":"Seconds until the key may call again.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/tutu/days":{"get":{"operationId":"listTutuDays","tags":["tutu"],"summary":"List 图图's daycare days: arrival, pick-up and naps","description":"The last `days` Pacific days (today included) that have a Brightwheel check-in, pick-up or nap, newest first. A nap still going on has no `endAt`. Days at home have nothing logged and are left out.\n\nRequires scope `tutu:read`.","x-scope":"tutu:read","security":[{"apiKey":[]}],"parameters":[{"name":"days","in":"query","required":false,"description":"How many days back, 1-62 (default 7: this week).","schema":{"default":7,"type":"integer","minimum":1,"maximum":62}}],"responses":{"200":{"description":"Days, newest first.","content":{"application/json":{"schema":{"type":"object","properties":{"days":{"type":"array","items":{"$ref":"#/components/schemas/TutuDay"}}},"required":["days"]}}}},"400":{"description":"`days` is not a whole number from 1 to 62 (`invalid_query`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Missing, unknown, revoked or expired API key (`api_key_required`, `invalid_api_key`, `api_key_revoked`, `api_key_expired`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"The key lacks the scope this operation needs (`insufficient_scope`; `required` names it).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Over the key's per-minute limit (`rate_limited`). Wait for `Retry-After` seconds.","headers":{"Retry-After":{"description":"Seconds until the key may call again.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/tutu/push-device":{"put":{"operationId":"setTutuPushDevice","tags":["tutu"],"summary":"Turn new-photo notifications on or off for one device","description":"The 图图 App sends its APNs device token here after the user allows notifications. While it is on, a notification arrives within a minute or two of new photos or videos of 图图 (taken in the last 3 days) reaching the timeline: one per batch, never for photos that were already there. `enabled: false` turns it off; a token APNs rejects as gone is dropped on its own.\n\nRequires scope `tutu:read`.","x-scope":"tutu:read","security":[{"apiKey":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"token":{"type":"string","pattern":"^[0-9a-fA-F]{32,200}$","description":"The APNs device token, hex."},"environment":{"type":"string","enum":["production","development"],"description":"`production` for TestFlight and App Store builds, `development` for builds run from Xcode."},"enabled":{"type":"boolean","description":"`true` to get notifications on this device, `false` to stop."}},"required":["token","environment","enabled"]}}}},"responses":{"200":{"description":"Whether this device now gets notifications.","content":{"application/json":{"schema":{"type":"object","properties":{"enabled":{"type":"boolean"}},"required":["enabled"]}}}},"400":{"description":"Invalid input (`invalid_push_device`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Missing, unknown, revoked or expired API key (`api_key_required`, `invalid_api_key`, `api_key_revoked`, `api_key_expired`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"The key lacks the scope this operation needs (`insufficient_scope`; `required` names it).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Over the key's per-minute limit (`rate_limited`). Wait for `Retry-After` seconds.","headers":{"Retry-After":{"description":"Seconds until the key may call again.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/tutu/home-logs":{"get":{"operationId":"listTutuHomeLogs","tags":["tutu"],"summary":"List 图图's home logs: sleep, wake-up, meals, poops, photos","description":"The 家里记录 of the last `days` Pacific days (today included), newest first: what the family logged at home with the 图图 App. Daycare's arrival, pick-up and naps are GET /tutu/days. Records taken back (撤销) are left out.\n\nRequires scope `tutu:read`.","x-scope":"tutu:read","security":[{"apiKey":[]}],"parameters":[{"name":"days","in":"query","required":false,"description":"How many days back, 1-62 (default 7: this week).","schema":{"default":7,"type":"integer","minimum":1,"maximum":62}},{"name":"kind","in":"query","required":false,"description":"Only this kind.","schema":{"type":"string","enum":["sleep","wake","meal","poop","moment"]}}],"responses":{"200":{"description":"Records, newest first.","content":{"application/json":{"schema":{"type":"object","properties":{"logs":{"type":"array","items":{"$ref":"#/components/schemas/HomeLog"}},"linksExpireAt":{"type":"string","format":"date-time","description":"When the photos' `url`s stop working (6 to 12 hours from now); afterwards list again."}},"required":["logs","linksExpireAt"]}}}},"400":{"description":"`days` is not a whole number from 1 to 62, or `kind` is not one of the kinds (`invalid_query`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Missing, unknown, revoked or expired API key (`api_key_required`, `invalid_api_key`, `api_key_revoked`, `api_key_expired`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"The key lacks the scope this operation needs (`insufficient_scope`; `required` names it).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Over the key's per-minute limit (`rate_limited`). Wait for `Retry-After` seconds.","headers":{"Retry-After":{"description":"Seconds until the key may call again.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"post":{"operationId":"addTutuHomeLog","tags":["tutu"],"summary":"Log something at home: fell asleep, woke up, a meal, a poop, a photo","description":"One tap in the 图图 App. `at` defaults to now. Send `clientId` (the app's own id for the tap) so a retried upload answers the first record (200) instead of logging it twice (201). Add photos with POST /tutu/home-logs/{id}/photos.\n\nRequires scope `tutu:write`.","x-scope":"tutu:write","security":[{"apiKey":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/HomeLogInput"}}}},"responses":{"200":{"description":"Already logged with this `clientId`: the record as it is.","content":{"application/json":{"schema":{"type":"object","properties":{"log":{"$ref":"#/components/schemas/HomeLog"},"linksExpireAt":{"type":"string","format":"date-time","description":"When the photos' `url`s stop working (6 to 12 hours from now); afterwards list again."}},"required":["log","linksExpireAt"]}}}},"201":{"description":"Logged.","content":{"application/json":{"schema":{"type":"object","properties":{"log":{"$ref":"#/components/schemas/HomeLog"},"linksExpireAt":{"type":"string","format":"date-time","description":"When the photos' `url`s stop working (6 to 12 hours from now); afterwards list again."}},"required":["log","linksExpireAt"]}}}},"400":{"description":"Invalid input (`invalid_home_log_input`): an unknown kind, a time in the future, meal fields on a record that is not a meal, poop fields on one that is not a poop.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Missing, unknown, revoked or expired API key (`api_key_required`, `invalid_api_key`, `api_key_revoked`, `api_key_expired`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"The key lacks the scope this operation needs (`insufficient_scope`; `required` names it).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Over the key's per-minute limit (`rate_limited`). Wait for `Retry-After` seconds.","headers":{"Retry-After":{"description":"Seconds until the key may call again.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/tutu/home-logs/{id}":{"get":{"operationId":"getTutuHomeLog","tags":["tutu"],"summary":"Get one home log","description":"One 家里记录 with fresh photo links, also after it was taken back (`removedAt` says so).\n\nRequires scope `tutu:read`.","x-scope":"tutu:read","security":[{"apiKey":[]}],"parameters":[{"name":"id","in":"path","required":true,"description":"The record's id.","schema":{"type":"string","pattern":"^[0-9A-Za-z]{8}$"}}],"responses":{"200":{"description":"The record.","content":{"application/json":{"schema":{"type":"object","properties":{"log":{"$ref":"#/components/schemas/HomeLog"},"linksExpireAt":{"type":"string","format":"date-time","description":"When the photos' `url`s stop working (6 to 12 hours from now); afterwards list again."}},"required":["log","linksExpireAt"]}}}},"401":{"description":"Missing, unknown, revoked or expired API key (`api_key_required`, `invalid_api_key`, `api_key_revoked`, `api_key_expired`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"The key lacks the scope this operation needs (`insufficient_scope`; `required` names it).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"No such record (`home_log_not_found`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Over the key's per-minute limit (`rate_limited`). Wait for `Retry-After` seconds.","headers":{"Retry-After":{"description":"Seconds until the key may call again.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"patch":{"operationId":"updateTutuHomeLog","tags":["tutu"],"summary":"Change a home log, or take it back (撤销)","description":"Only the fields sent change. `kind` re-types it (the camera's mode strip): meal fields go when it stops being a meal, poop fields when it stops being a poop. `removed: true` takes it back: it leaves every list, its photos stay stored; nothing in this API deletes a record. `removePhotos` takes photos off it (换照片).\n\nRequires scope `tutu:write`.","x-scope":"tutu:write","security":[{"apiKey":[]}],"parameters":[{"name":"id","in":"path","required":true,"description":"The record's id.","schema":{"type":"string","pattern":"^[0-9A-Za-z]{8}$"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/HomeLogPatch"}}}},"responses":{"200":{"description":"The record as stored now.","content":{"application/json":{"schema":{"type":"object","properties":{"log":{"$ref":"#/components/schemas/HomeLog"},"linksExpireAt":{"type":"string","format":"date-time","description":"When the photos' `url`s stop working (6 to 12 hours from now); afterwards list again."}},"required":["log","linksExpireAt"]}}}},"400":{"description":"Invalid input (`invalid_home_log_input`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Missing, unknown, revoked or expired API key (`api_key_required`, `invalid_api_key`, `api_key_revoked`, `api_key_expired`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"The key lacks the scope this operation needs (`insufficient_scope`; `required` names it).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"No such record (`home_log_not_found`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Over the key's per-minute limit (`rate_limited`). Wait for `Retry-After` seconds.","headers":{"Retry-After":{"description":"Seconds until the key may call again.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/tutu/home-logs/{id}/photos":{"post":{"operationId":"addTutuHomeLogPhoto","tags":["tutu"],"summary":"Add a photo to a home log","description":"The body is the photo itself (`Content-Type: image/jpeg`, `image/png`, `image/webp` or `image/heic`, at most 15 MB). Send it downscaled (about 2048 px on the long side is plenty). `width` and `height` let apps lay it out before the file arrives. Stored privately; handed out only as signed links.\n\nRequires scope `tutu:write`.","x-scope":"tutu:write","security":[{"apiKey":[]}],"parameters":[{"name":"id","in":"path","required":true,"description":"The record's id.","schema":{"type":"string","pattern":"^[0-9A-Za-z]{8}$"}},{"name":"width","in":"query","required":false,"description":"The photo's width in pixels.","schema":{"type":"integer","minimum":1,"maximum":20000}},{"name":"height","in":"query","required":false,"description":"The photo's height in pixels.","schema":{"type":"integer","minimum":1,"maximum":20000}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{}},"image/jpeg":{"schema":{"type":"string","format":"binary"}},"image/png":{"schema":{"type":"string","format":"binary"}},"image/webp":{"schema":{"type":"string","format":"binary"}},"image/heic":{"schema":{"type":"string","format":"binary"}}}},"responses":{"201":{"description":"The record with the photo.","content":{"application/json":{"schema":{"type":"object","properties":{"log":{"$ref":"#/components/schemas/HomeLog"},"linksExpireAt":{"type":"string","format":"date-time","description":"When the photos' `url`s stop working (6 to 12 hours from now); afterwards list again."}},"required":["log","linksExpireAt"]}}}},"400":{"description":"No body (`photo_required`), or `width` / `height` malformed (`invalid_query`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Missing, unknown, revoked or expired API key (`api_key_required`, `invalid_api_key`, `api_key_revoked`, `api_key_expired`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"The key lacks the scope this operation needs (`insufficient_scope`; `required` names it).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"No such record, or it was taken back (`home_log_not_found`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"413":{"description":"Over 15 MB (`photo_too_large`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"415":{"description":"Not a JPEG, PNG, WebP or HEIC photo (`unsupported_photo`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Over the key's per-minute limit (`rate_limited`). Wait for `Retry-After` seconds.","headers":{"Retry-After":{"description":"Seconds until the key may call again.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/tutu/growth":{"get":{"operationId":"listTutuGrowth","tags":["tutu"],"summary":"List 图图's growth measurements","description":"His 生长测量 (weight, height, head circumference), oldest first, each with its WHO percentile for his age. With scope `tutu:read` (the 图图 App's key) only the numbers; `note` and the record a measurement came from need `health:read`.\n\nRequires scope `tutu:read`.","x-scope":"tutu:read","security":[{"apiKey":[]}],"parameters":[{"name":"cursor","in":"query","required":false,"description":"`nextCursor` of the previous page.","schema":{"type":"string","maxLength":20}},{"name":"limit","in":"query","required":false,"description":"Page size, 1-500 (default 200).","schema":{"default":200,"type":"integer","minimum":1,"maximum":500}}],"responses":{"200":{"description":"One page, oldest first.","content":{"application/json":{"schema":{"type":"object","properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/GrowthMeasurement"}},"nextCursor":{"type":["string","null"],"description":"Pass as `cursor` for the next page; null on the last page."}},"required":["items","nextCursor"]}}}},"401":{"description":"Missing, unknown, revoked or expired API key (`api_key_required`, `invalid_api_key`, `api_key_revoked`, `api_key_expired`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"The key lacks the scope this operation needs (`insufficient_scope`; `required` names it).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Over the key's per-minute limit (`rate_limited`). Wait for `Retry-After` seconds.","headers":{"Retry-After":{"description":"Seconds until the key may call again.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"post":{"operationId":"addTutuGrowth","tags":["tutu"],"summary":"Add a growth measurement","description":"Any of weight, height and head circumference measured on one day. `recordId` ties it to the 医疗记录 (visit) it was measured at.\n\nRequires scope `health:write`.","x-scope":"health:write","security":[{"apiKey":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/GrowthInput"}}}},"responses":{"201":{"description":"The measurement.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/GrowthMeasurement"}}}},"400":{"description":"Invalid input (`invalid_growth_input`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Missing, unknown, revoked or expired API key (`api_key_required`, `invalid_api_key`, `api_key_revoked`, `api_key_expired`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"The key lacks the scope this operation needs (`insufficient_scope`; `required` names it).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"`recordId` names no record (`health_record_not_found`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Over the key's per-minute limit (`rate_limited`). Wait for `Retry-After` seconds.","headers":{"Retry-After":{"description":"Seconds until the key may call again.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/tutu/growth/chart":{"get":{"operationId":"getTutuGrowthChart","tags":["tutu"],"summary":"图图's growth chart with WHO bands","description":"One metric (`weight`, `height` or `head`): his measurements with their age in months and percentile, and the WHO 3rd, 15th, 50th, 85th and 97th percentile curves to draw behind them. Computed on the server so every client shows the same.\n\nRequires scope `tutu:read`.","x-scope":"tutu:read","security":[{"apiKey":[]}],"parameters":[{"name":"metric","in":"query","required":false,"description":"`weight` (default), `height` or `head`.","schema":{"default":"weight","type":"string","enum":["weight","height","head"]}}],"responses":{"200":{"description":"The chart.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/GrowthChart"}}}},"401":{"description":"Missing, unknown, revoked or expired API key (`api_key_required`, `invalid_api_key`, `api_key_revoked`, `api_key_expired`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"The key lacks the scope this operation needs (`insufficient_scope`; `required` names it).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Over the key's per-minute limit (`rate_limited`). Wait for `Retry-After` seconds.","headers":{"Retry-After":{"description":"Seconds until the key may call again.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/tutu/health/records":{"get":{"operationId":"listHealthRecords","tags":["tutu"],"summary":"List 图图's health records","description":"His 医疗记录, newest first, each with what was measured and its attachments (signed links, one hour).\n\nRequires scope `health:read`.","x-scope":"health:read","security":[{"apiKey":[]}],"parameters":[{"name":"kind","in":"query","required":false,"description":"Only this kind.","schema":{"type":"string","enum":["checkup","vaccine","sick_visit","medication","allergy","lab","other"]}},{"name":"cursor","in":"query","required":false,"description":"`nextCursor` of the previous page.","schema":{"type":"string","maxLength":20}},{"name":"limit","in":"query","required":false,"description":"Page size, 1-500 (default 200).","schema":{"default":200,"type":"integer","minimum":1,"maximum":500}}],"responses":{"200":{"description":"One page, newest first.","content":{"application/json":{"schema":{"type":"object","properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/HealthRecord"}},"nextCursor":{"type":["string","null"],"description":"Pass as `cursor` for the next page; null on the last page."}},"required":["items","nextCursor"]}}}},"401":{"description":"Missing, unknown, revoked or expired API key (`api_key_required`, `invalid_api_key`, `api_key_revoked`, `api_key_expired`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"The key lacks the scope this operation needs (`insufficient_scope`; `required` names it).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Over the key's per-minute limit (`rate_limited`). Wait for `Retry-After` seconds.","headers":{"Retry-After":{"description":"Seconds until the key may call again.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"post":{"operationId":"addHealthRecord","tags":["tutu"],"summary":"Add a health record","description":"A visit, shot, illness, medicine, allergy or test. Shots given go in `vaccines`; what was measured at the visit in `measurement` (written as a growth measurement at the clinic).\n\nRequires scope `health:write`.","x-scope":"health:write","security":[{"apiKey":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/HealthRecordInput"}}}},"responses":{"201":{"description":"The record.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HealthRecord"}}}},"400":{"description":"Invalid input (`invalid_health_record_input`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Missing, unknown, revoked or expired API key (`api_key_required`, `invalid_api_key`, `api_key_revoked`, `api_key_expired`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"The key lacks the scope this operation needs (`insufficient_scope`; `required` names it).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Over the key's per-minute limit (`rate_limited`). Wait for `Retry-After` seconds.","headers":{"Retry-After":{"description":"Seconds until the key may call again.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/tutu/health/records/{id}":{"get":{"operationId":"getHealthRecord","tags":["tutu"],"summary":"Get one health record","description":"One 医疗记录 with fresh attachment links.\n\nRequires scope `health:read`.","x-scope":"health:read","security":[{"apiKey":[]}],"parameters":[{"name":"id","in":"path","required":true,"description":"The record's id.","schema":{"type":"string","pattern":"^[0-9A-Za-z]{8}$"}}],"responses":{"200":{"description":"The record.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HealthRecord"}}}},"401":{"description":"Missing, unknown, revoked or expired API key (`api_key_required`, `invalid_api_key`, `api_key_revoked`, `api_key_expired`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"The key lacks the scope this operation needs (`insufficient_scope`; `required` names it).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"No such record (`health_record_not_found`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Over the key's per-minute limit (`rate_limited`). Wait for `Retry-After` seconds.","headers":{"Retry-After":{"description":"Seconds until the key may call again.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"patch":{"operationId":"updateHealthRecord","tags":["tutu"],"summary":"Change a health record","description":"Only the fields sent change; `vaccines` replaces the whole list. Nothing in this API deletes a record.\n\nRequires scope `health:write`.","x-scope":"health:write","security":[{"apiKey":[]}],"parameters":[{"name":"id","in":"path","required":true,"description":"The record's id.","schema":{"type":"string","pattern":"^[0-9A-Za-z]{8}$"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/HealthRecordPatch"}}}},"responses":{"200":{"description":"The record as stored now.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HealthRecord"}}}},"400":{"description":"Invalid input (`invalid_health_record_input`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Missing, unknown, revoked or expired API key (`api_key_required`, `invalid_api_key`, `api_key_revoked`, `api_key_expired`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"The key lacks the scope this operation needs (`insufficient_scope`; `required` names it).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"No such record (`health_record_not_found`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Over the key's per-minute limit (`rate_limited`). Wait for `Retry-After` seconds.","headers":{"Retry-After":{"description":"Seconds until the key may call again.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/tutu/health/vaccines":{"get":{"operationId":"listVaccines","tags":["tutu"],"summary":"图图's vaccines","description":"Every shot from his records, grouped by vaccine name, doses in date order.\n\nRequires scope `health:read`.","x-scope":"health:read","security":[{"apiKey":[]}],"responses":{"200":{"description":"The vaccines.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Vaccines"}}}},"401":{"description":"Missing, unknown, revoked or expired API key (`api_key_required`, `invalid_api_key`, `api_key_revoked`, `api_key_expired`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"The key lacks the scope this operation needs (`insufficient_scope`; `required` names it).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Over the key's per-minute limit (`rate_limited`). Wait for `Retry-After` seconds.","headers":{"Retry-After":{"description":"Seconds until the key may call again.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/connectors":{"get":{"operationId":"listConnectors","tags":["connectors"],"summary":"How each 同步源 (sync) stands","description":"Every source the hub syncs on its own schedule, with its last successful sync, its last run and error, and `health`. Read it before saying a platform has nothing new: `ok` with an old `lastItemAt` means nothing new was saved; `failing`, `late` or `needs_attention` means the sync is broken and new saves are not arriving.\n\nRequires scope `items:read`.","x-scope":"items:read","security":[{"apiKey":[]}],"responses":{"200":{"description":"Every 同步源.","content":{"application/json":{"schema":{"type":"object","properties":{"connectors":{"type":"array","items":{"$ref":"#/components/schemas/ConnectorStatus"}}},"required":["connectors"]}}}},"401":{"description":"Missing, unknown, revoked or expired API key (`api_key_required`, `invalid_api_key`, `api_key_revoked`, `api_key_expired`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"The key lacks the scope this operation needs (`insufficient_scope`; `required` names it).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Over the key's per-minute limit (`rate_limited`). Wait for `Retry-After` seconds.","headers":{"Retry-After":{"description":"Seconds until the key may call again.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/notify":{"post":{"operationId":"notify","tags":["notify"],"summary":"Tell the owner something","description":"A short notification to the owner: work finished, a PR to review, something that needs a decision. Plain `title` and `text`; it goes to the 通知频道 `channel`, by default `dev_bot` (the owner's dev bot). Only `dev_bot` may be named. At most 20 an hour per key; over that, 429 `notify_rate_limited` with `Retry-After`. Send once when something is done, not progress updates.\n\nRequires scope `notify:write`.","x-scope":"notify:write","security":[{"apiKey":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/NotifyInput"}}}},"responses":{"200":{"description":"Sent.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/NotifySent"}}}},"400":{"description":"Invalid input (`invalid_notify_input`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Missing, unknown, revoked or expired API key (`api_key_required`, `invalid_api_key`, `api_key_revoked`, `api_key_expired`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"A channel keys may not send to (`notify_channel_not_allowed`), or the key lacks `notify:write` (`insufficient_scope`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"422":{"description":"The channel's platform cannot take this message even cut down, e.g. a link that is not http(s) (`webhook_invalid_link`, `feishu_invalid_link`, `…_message_too_large`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Over 20 an hour for this key (`notify_rate_limited`; wait `Retry-After` seconds) or over the key's per-minute limit (`rate_limited`).","headers":{"Retry-After":{"description":"Seconds until the key may call again.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"502":{"description":"The channel's receiver refused or did not answer (`notification_failed`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"The channel is not set up yet, e.g. its token secret is missing (`notify_channel_unconfigured`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/tikhub":{"post":{"operationId":"callTikhub","tags":["tikhub"],"summary":"Make one TikHub request through the hub","description":"Calls one TikHub data route (Xiaohongshu, Douyin, TikTok, Instagram, Bilibili, WeChat articles, …; see https://api.tikhub.io/docs) with the hub's TikHub key, which you never see, and answers with TikHub's own JSON as it came. It spends the owner's TikHub balance: each request counts against the TikHub budget and is recorded with this key. For a link the owner wants kept, `POST /collect` instead (it reads the post once and stores it); this is for looking things up.\n\nRequires scope `tikhub:call`.","x-scope":"tikhub:call","security":[{"apiKey":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TikhubCall"}}}},"responses":{"200":{"description":"TikHub's response body, unchanged.","content":{"application/json":{"schema":{}}}},"400":{"description":"Invalid input (`invalid_tikhub_call`): `path` is not a TikHub data route, or a TikHub account route.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Missing, unknown, revoked or expired API key (`api_key_required`, `invalid_api_key`, `api_key_revoked`, `api_key_expired`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"The key lacks the scope this operation needs (`insufficient_scope`; `required` names it).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"The TikHub budget has no room now (`tikhub_budget_exhausted`; the owner raises it on the hub), or over the key's per-minute limit (`rate_limited`).","headers":{"Retry-After":{"description":"Seconds until the key may call again.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"502":{"description":"TikHub refused the request or answered an error (`tikhub_error`, with its status and detail), or did not answer (`tikhub_unreachable`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"The hub has no TikHub key (`tikhub_unconfigured`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/renders":{"post":{"operationId":"submitRender","tags":["renders"],"summary":"Render a Blender scene (queued)","description":"The body is the .blend itself (`Content-Type: application/octet-stream`, with `Content-Length`, at most 95 MB; pack textures into it first). It goes to the home render machines' store and the render waits in the jobs queue: most urgent first, in its window, one per machine. Each finished frame is stored at once; a failed try starts again from the first missing frame. Follow it with GET /renders/{id}. Files are kept 30 days.\n\nRequires scope `renders:write`.","x-scope":"renders:write","security":[{"apiKey":[]}],"parameters":[{"name":"frameStart","in":"query","required":true,"description":"First frame.","schema":{"type":"integer","minimum":0,"maximum":1000000}},{"name":"frameEnd","in":"query","required":true,"description":"Last frame (included).","schema":{"type":"integer","minimum":0,"maximum":1000000}},{"name":"frameStep","in":"query","required":false,"description":"Every n-th frame (default 1).","schema":{"default":1,"type":"integer","minimum":1,"maximum":1000}},{"name":"device","in":"query","required":false,"description":"Default gpu.","schema":{"type":"string","enum":["gpu","cpu"]}},{"name":"engine","in":"query","required":false,"description":"Default `scene`: the renderer set in the .blend.","schema":{"type":"string","enum":["scene","cycles","eevee"]}},{"name":"samples","in":"query","required":false,"description":"Samples; the .blend's when left out.","schema":{"type":"integer","minimum":1,"maximum":100000}},{"name":"resolutionPercent","in":"query","required":false,"description":"Resolution percentage; the .blend's when left out (25 or 50 for a test).","schema":{"type":"integer","minimum":1,"maximum":1000}},{"name":"imageFormat","in":"query","required":false,"description":"Default png.","schema":{"type":"string","enum":["png","jpeg","exr"]}},{"name":"window","in":"query","required":false,"description":"`night`: start only 22:00-08:00 Pacific, when the machines are free.","schema":{"type":"string","enum":["night"]}},{"name":"priority","in":"query","required":false,"description":"Place in line: `now` before collected links' follow-ups, `backlog` after everything (default `normal`).","schema":{"type":"string","enum":["now","normal","backlog"]}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{}},"application/octet-stream":{"schema":{"type":"string","format":"binary"}}}},"responses":{"202":{"description":"Queued.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Render"}}}},"400":{"description":"A parameter is malformed (`invalid_query`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Missing, unknown, revoked or expired API key (`api_key_required`, `invalid_api_key`, `api_key_revoked`, `api_key_expired`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"The key lacks the scope this operation needs (`insufficient_scope`; `required` names it).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"411":{"description":"No body or no Content-Length (`blend_required`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"413":{"description":"The .blend is over 95 MB (`blend_too_large`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Over the key's per-minute limit (`rate_limited`). Wait for `Retry-After` seconds.","headers":{"Retry-After":{"description":"Seconds until the key may call again.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"502":{"description":"The media gateway refused or failed (`media_gateway_failed`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/renders/{id}":{"get":{"operationId":"getRender","tags":["renders"],"summary":"Read a render: status and progress","description":"Where the render is: waiting, on which machine, frames done of total, seconds per frame and time left. Poll it every minute or so; progress is copied from the machine once a minute.\n\nRequires scope `renders:read`.","x-scope":"renders:read","security":[{"apiKey":[]}],"parameters":[{"name":"id","in":"path","required":true,"description":"Render id as returned by POST /renders.","schema":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"}}],"responses":{"200":{"description":"The render.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Render"}}}},"401":{"description":"Missing, unknown, revoked or expired API key (`api_key_required`, `invalid_api_key`, `api_key_revoked`, `api_key_expired`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"The key lacks the scope this operation needs (`insufficient_scope`; `required` names it).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"No such render (`render_not_found`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Over the key's per-minute limit (`rate_limited`). Wait for `Retry-After` seconds.","headers":{"Retry-After":{"description":"Seconds until the key may call again.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/renders/{id}/frames":{"get":{"operationId":"listRenderFrames","tags":["renders"],"summary":"List a render's frames","description":"The frames stored so far, in frame order; while the render runs, only those already out.\n\nRequires scope `renders:read`.","x-scope":"renders:read","security":[{"apiKey":[]}],"parameters":[{"name":"id","in":"path","required":true,"description":"Render id as returned by POST /renders.","schema":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"}},{"name":"cursor","in":"query","required":false,"description":"`nextCursor` of the previous page.","schema":{"type":"string","maxLength":1000}}],"responses":{"200":{"description":"One page of frames.","content":{"application/json":{"schema":{"type":"object","properties":{"items":{"type":"array","items":{"type":"object","properties":{"name":{"type":"string","description":"File name: zero-padded frame number and extension.","examples":["0001.png"]},"frame":{"type":"integer"},"bytes":{"type":"integer"},"url":{"type":"string","description":"Read the frame.","examples":["/renders/0b9c2f7e-4d1a-4c3b-9e8f-1a2b3c4d5e6f/frames/0001.png"]}},"required":["name","frame","bytes","url"]}},"nextCursor":{"type":["string","null"],"description":"Pass as `cursor` for the next page; null on the last page."}},"required":["items","nextCursor"]}}}},"401":{"description":"Missing, unknown, revoked or expired API key (`api_key_required`, `invalid_api_key`, `api_key_revoked`, `api_key_expired`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"The key lacks the scope this operation needs (`insufficient_scope`; `required` names it).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"No such render (`render_not_found`), or its files are past the 30 days (`render_files_gone`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Over the key's per-minute limit (`rate_limited`). Wait for `Retry-After` seconds.","headers":{"Retry-After":{"description":"Seconds until the key may call again.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/renders/{id}/frames/{name}":{"get":{"operationId":"getRenderFrame","tags":["renders"],"summary":"Read one frame","description":"The image as rendered, streamed from the render machines' store.\n\nRequires scope `renders:read`.","x-scope":"renders:read","security":[{"apiKey":[]}],"parameters":[{"name":"id","in":"path","required":true,"description":"Render id as returned by POST /renders.","schema":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"}},{"name":"name","in":"path","required":true,"description":"Frame file name.","schema":{"type":"string","pattern":"^[0-9]{4,6}\\.(png|jpg|exr)$"},"example":"0001.png"}],"responses":{"200":{"description":"The image (png, jpeg or exr).","content":{"image/*":{"schema":{"type":"string","format":"binary"}}}},"401":{"description":"Missing, unknown, revoked or expired API key (`api_key_required`, `invalid_api_key`, `api_key_revoked`, `api_key_expired`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"The key lacks the scope this operation needs (`insufficient_scope`; `required` names it).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"No such render or frame (`render_not_found`, `frame_not_found`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Over the key's per-minute limit (`rate_limited`). Wait for `Retry-After` seconds.","headers":{"Retry-After":{"description":"Seconds until the key may call again.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}}},"webhooks":{"item.collected":{"post":{"operationId":"itemCollected","summary":"Item collected: fetched, still processing","description":"The link was fetched and the item exists; media rehost, transcription and summary may still be running. Sent at most once per collect. Use it to show progress; do not read the timeline yet.","parameters":[{"name":"webhook-id","in":"header","required":true,"description":"The event id; deduplicate on it.","schema":{"type":"string"}},{"name":"webhook-timestamp","in":"header","required":true,"description":"Unix seconds when this attempt was signed. Refuse it if more than 5 minutes off your clock.","schema":{"type":"string"}},{"name":"webhook-signature","in":"header","required":true,"description":"`v1,<base64 HMAC-SHA256(secret bytes, id + \".\" + timestamp + \".\" + raw body)>`; several space-separated during a rotation, any one may match.","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookEvent"}}}},"responses":{"2XX":{"description":"Received. Answer within 10 seconds and do the work afterwards."},"default":{"description":"Anything else (or no answer) is retried: 1 min, 5 min, 15 min, 1 h, 3 h, 6 h, 12 h, 24 h, then given up. An endpoint failing for 3 days or 20 attempts in a row is disabled."}}}},"item.ready":{"post":{"operationId":"itemReady","summary":"Item ready: all processing done","description":"Nothing more is running or awaited for this item, so its content and timeline are final. For a video (a video platform, or a post whose format is video) the file is stored, so `GET /items/{id}/content` returns a signed video link. A transcript that failed (e.g. music, no speech) does not stop `item.ready`; check `stages.transcript`. Sent at most once per collect, and never together with item.failed.","parameters":[{"name":"webhook-id","in":"header","required":true,"description":"The event id; deduplicate on it.","schema":{"type":"string"}},{"name":"webhook-timestamp","in":"header","required":true,"description":"Unix seconds when this attempt was signed. Refuse it if more than 5 minutes off your clock.","schema":{"type":"string"}},{"name":"webhook-signature","in":"header","required":true,"description":"`v1,<base64 HMAC-SHA256(secret bytes, id + \".\" + timestamp + \".\" + raw body)>`; several space-separated during a rotation, any one may match.","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookEvent"}}}},"responses":{"2XX":{"description":"Received. Answer within 10 seconds and do the work afterwards."},"default":{"description":"Anything else (or no answer) is retried: 1 min, 5 min, 15 min, 1 h, 3 h, 6 h, 12 h, 24 h, then given up. An endpoint failing for 3 days or 20 attempts in a row is disabled."}}}},"item.failed":{"post":{"operationId":"itemFailed","summary":"Item failed, with reason","description":"The fetch failed, a video's file could not be stored, the item was removed, or it was still not done after 24 hours (`reason`: the error, `video_not_stored`, `item_not_found`, `timed_out`). Sent at most once per collect, and never together with item.ready.","parameters":[{"name":"webhook-id","in":"header","required":true,"description":"The event id; deduplicate on it.","schema":{"type":"string"}},{"name":"webhook-timestamp","in":"header","required":true,"description":"Unix seconds when this attempt was signed. Refuse it if more than 5 minutes off your clock.","schema":{"type":"string"}},{"name":"webhook-signature","in":"header","required":true,"description":"`v1,<base64 HMAC-SHA256(secret bytes, id + \".\" + timestamp + \".\" + raw body)>`; several space-separated during a rotation, any one may match.","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookEvent"}}}},"responses":{"2XX":{"description":"Received. Answer within 10 seconds and do the work afterwards."},"default":{"description":"Anything else (or no answer) is retried: 1 min, 5 min, 15 min, 1 h, 3 h, 6 h, 12 h, 24 h, then given up. An endpoint failing for 3 days or 20 attempts in a row is disabled."}}}},"webhook.test":{"post":{"operationId":"webhookTest","summary":"Test event","description":"Sent when the owner presses 发测试事件 or runs `webhooks test`. Verify the signature and answer 2xx; it is never retried.","parameters":[{"name":"webhook-id","in":"header","required":true,"description":"The event id; deduplicate on it.","schema":{"type":"string"}},{"name":"webhook-timestamp","in":"header","required":true,"description":"Unix seconds when this attempt was signed. Refuse it if more than 5 minutes off your clock.","schema":{"type":"string"}},{"name":"webhook-signature","in":"header","required":true,"description":"`v1,<base64 HMAC-SHA256(secret bytes, id + \".\" + timestamp + \".\" + raw body)>`; several space-separated during a rotation, any one may match.","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookEvent"}}}},"responses":{"2XX":{"description":"Received. Answer within 10 seconds and do the work afterwards."},"default":{"description":"Anything else (or no answer) is retried: 1 min, 5 min, 15 min, 1 h, 3 h, 6 h, 12 h, 24 h, then given up. An endpoint failing for 3 days or 20 attempts in a row is disabled."}}}}},"components":{"securitySchemes":{"apiKey":{"type":"http","scheme":"bearer","bearerFormat":"pih_ API key","description":"`Authorization: Bearer pih_...`; `x-api-key: pih_...` also works."}},"schemas":{"Error":{"type":"object","properties":{"error":{"type":"string","pattern":"^[a-z][a-z0-9_]*$","description":"Stable machine code (snake_case): branch on this.","examples":["not_found"]},"message":{"description":"What went wrong, for people. Wording may change; do not match on it.","type":"string"},"details":{"description":"Every validation issue, when the request body or query failed its schema.","type":"array","items":{"type":"object","properties":{"path":{"type":"string"},"message":{"type":"string"}},"required":["path","message"]}},"required":{"description":"The missing scope, on 403.","type":"string"}},"required":["error"],"description":"Every error: `error` is the stable code to branch on, `message` says what went wrong for people."},"Author":{"type":["object","null"],"properties":{"name":{"type":["string","null"]},"handle":{"type":["string","null"]}}},"Stages":{"type":"object","properties":{"text":{"type":"string","enum":["ok","failed","pending","none"]},"media":{"type":"string","enum":["ok","failed","pending","none"]},"transcript":{"type":"string","enum":["ok","failed","pending","none"]},"summary":{"type":"string","enum":["ok","failed","pending","none"]}},"required":["text","media","transcript","summary"],"description":"Each processing stage: `ok`, `failed`, `pending` (still running or awaited) or `none` (does not apply)."},"Processing":{"type":"object","properties":{"status":{"type":"string","enum":["processing","ready","failed"]},"stages":{"anyOf":[{"$ref":"#/components/schemas/Stages"},{"type":"null"}]},"reason":{"type":["string","null"],"description":"Why it failed (`timed_out`, a fetch error, `video_not_stored`, …)."}},"required":["status","stages","reason"],"description":"Where processing stands, judged exactly as callback webhook events are."},"InlineContent":{"type":"object","properties":{"readAs":{"type":"object","properties":{"route":{"type":"string","enum":["timeline","body","media","summary"]},"reason":{"type":"string"}},"required":["route","reason"],"description":"How to take the item in; the same verdict as `/items/{id}/content`."},"body":{"type":["string","null"],"description":"The article, post or thread text."},"note":{"type":["string","null"],"description":"The owner's note."},"mediaCount":{"type":"integer","description":"Stored images, videos and audio. Signed links to them are only in `/items/{id}/content`."}},"required":["readAs","body","note","mediaCount"],"description":"The item's own text. Transcripts are never inlined in a list (one can run to tens of thousands of characters): `transcript` on the item says whether there is one and how long; read it as timed sentences from `/items/{id}/content` or as a timeline from `/items/{id}/timeline`."},"TranscriptState":{"type":"object","properties":{"state":{"type":"string","enum":["none","partial","ready"],"description":"`ready`: a transcript of what is said is stored. `partial`: words are stored but are not the item's speech (`repeated_line`: Whisper repeated one line over music; do not quote them), or may not be (`possible_lyrics`: voice detection heard nobody yet Whisper found words, speech under loud music or song lyrics; check before quoting). `none`: no transcript; `reason` says why."},"reason":{"type":["string","null"],"enum":["not_spoken","no_speech","awaiting_video","processing","failed","not_captured","repeated_line","possible_lyrics",null],"description":"Why the transcript is not `ready`. `not_spoken`: nothing to transcribe (an article, a picture post). `no_speech`: the audio was checked and nobody speaks. `awaiting_video`: the video file has not arrived from the owner's Mac yet; it is transcribed after. `processing`: fetching or transcribing is still running. `failed`: transcription failed (`message` says why). `not_captured`: nothing is running and no transcript was made. `repeated_line`, `possible_lyrics`: see `partial`. Null when `ready`."},"message":{"type":["string","null"],"description":"What went wrong or what it is waiting for, in words (e.g. `download failed: HTTP 403 Forbidden`)."},"source":{"type":["string","null"],"description":"`platform_subtitle` (the publisher's own subtitles or the platform's captions), `whisper` (transcribed from the audio) or `emby_subtitle`; null without a transcript."},"language":{"type":["string","null"]},"chars":{"type":"integer","description":"Length of the transcript text in characters; 0 without one."}},"required":["state","reason","message","source","language","chars"],"description":"Whether the item's transcript can be read, and if not, why."},"Item":{"type":"object","properties":{"id":{"type":"string","description":"Short public id."},"source":{"type":"string","description":"Where it came from, e.g. `manual_url`, `twitter_bookmark`, `xiaohongshu`, `tiktok`, `douyin`, `instagram`, `youtube`, `bilibili`, `wechat_article`."},"url":{"type":["string","null"],"format":"uri","description":"The original link."},"title":{"type":["string","null"]},"summary":{"type":["string","null"]},"headline":{"type":["string","null"],"description":"A one-line Chinese title that states what the item says and its conclusion, written by the model after the summary (重写标题); jiajun.fyi's feed cards lead with it. Null until written, and for items too short to summarise."},"preview":{"type":["string","null"],"description":"First 280 characters of the body."},"author":{"$ref":"#/components/schemas/Author"},"language":{"type":["string","null"]},"tags":{"type":"array","items":{"type":"string"}},"starred":{"type":"boolean"},"readLaterAt":{"type":["string","null"],"format":"date-time","description":"When the owner marked it 稍后读 / 稍后看 (read or watch later, articles and videos alike); null when not marked."},"state":{"type":"string","enum":["processing","ready","error"]},"format":{"type":["string","null"],"description":"Post format when known: `video`, `image`, `text`, ..."},"durationSeconds":{"type":["number","null"]},"transcript":{"$ref":"#/components/schemas/TranscriptState"},"stats":{"type":["object","null"],"properties":{"likes":{"type":["integer","null"]},"saves":{"type":["integer","null"]},"comments":{"type":["integer","null"]},"views":{"type":["integer","null"]}},"required":["likes","saves","comments","views"]},"sound":{"type":["object","null"],"properties":{"id":{"description":"The platform's id for the sound; other videos using it share this id.","type":"string"},"title":{"type":"string"},"author":{"description":"Artist, or the creator for an original sound.","type":"string"},"album":{"type":"string"},"duration_s":{"type":["number","null"]},"original":{"type":["boolean","null"],"description":"True for the creator's own recording (Instagram: original audio, titled by the song it contains when known)."},"audio_url":{"description":"Where the platform serves the audio; often a signed link that expires.","type":"string"}},"description":"The music or original sound a short video is set to (TikTok, Douyin, Instagram reels); null when the platform names none."},"sourcePublishedAt":{"type":["string","null"],"format":"date-time"},"collectedAt":{"type":"string","format":"date-time"},"hubUrl":{"type":"string","format":"uri","description":"The item's canonical page on the hub (requires the owner's login)."},"content":{"description":"Only with `include=content` on a list or search.","$ref":"#/components/schemas/InlineContent"}},"required":["id","source","url","title","summary","headline","preview","author","language","tags","starred","readLaterAt","state","format","durationSeconds","transcript","stats","sound","sourcePublishedAt","collectedAt","hubUrl"]},"ItemDetail":{"allOf":[{"$ref":"#/components/schemas/Item"},{"type":"object","properties":{"note":{"type":["string","null"]},"collection":{"type":"object","properties":{"kinds":{"type":"array","items":{"type":"string"}},"firstObservedAt":{"type":["string","null"]}},"required":["kinds","firstObservedAt"]},"mediaCount":{"type":"integer"},"contentUrl":{"type":"string","format":"uri"},"savedAt":{"type":["string","null"],"format":"date-time","description":"When the owner saved it: the platform's own time, the moment it was collected, when a sync first saw it on a list, or (for old saves turned up by a first sync) an estimate from the list order."},"processing":{"$ref":"#/components/schemas/Processing"}},"required":["note","collection","mediaCount","contentUrl","savedAt","processing"]}],"description":"An item with what only one item's view carries: the owner's note, when it was saved and where processing stands."},"SearchMatch":{"type":"object","properties":{"kind":{"type":"string","enum":["exact","fuzzy","semantic"],"description":"`exact`: the whole query appears verbatim in `field` (case-insensitive; a query of Latin letters or digits as a word of its own, so `3DS` in `3dscene` does not count), or a pasted link names the item (`field: link`). `fuzzy`: only matched by word prefix, word stem, separate words or inside a longer word (searching `3DS` also finds `3D printing`); check `snippet` before relying on it. `semantic`: a transcript passage close in meaning to the query, none of its words needed (`snippet` is the passage); never returned with `exact=true`."},"field":{"type":["string","null"],"enum":["link","title","author","note","summary","body","url","translation","transcript","tags","derived",null],"description":"The text it matched in: `title`, `author`, `note`, `summary`, `body`, `url`, `translation`, `transcript` (the spoken words, every transcript read to its end however long, with `start` / `end`), `tags` (AI tags), `derived` (other text made from the item), or `link` (a pasted link). A video's body holds a copy of its transcript; a hit there is reported as `transcript`. Null when it cannot be told."},"snippet":{"type":["string","null"],"description":"The original text around the match, about 60 characters each side, `…` where cut; for `semantic`, the start of the matching passage. Null for `link`."},"start":{"type":["number","null"],"description":"For a `transcript` hit: seconds into the media where the matching passage (a 30–60 second piece of the transcript) starts. Null for other fields or a transcript without timing."},"end":{"type":["number","null"],"description":"Where that passage ends, in seconds. Null as for `start`."}},"required":["kind","field","snippet","start","end"],"description":"Where the result matched the query."},"SearchResult":{"type":"object","properties":{"type":{"type":"string","description":"`item` today; skip unknown types."},"match":{"$ref":"#/components/schemas/SearchMatch"},"item":{"$ref":"#/components/schemas/Item"}},"required":["type","match"]},"Media":{"type":"object","properties":{"id":{"type":"string"},"kind":{"type":"string","enum":["image","video","audio"]},"contentType":{"type":["string","null"]},"width":{"type":["integer","null"]},"height":{"type":["integer","null"]},"url":{"type":"string","format":"uri","description":"Signed download link; no key needed. Expires at `expiresAt` (one hour): use it right away, never store it."},"expiresAt":{"type":"string","format":"date-time"},"stableUrl":{"type":"string","format":"uri","description":"The file's stable address (`GET /items/{id}/media/{mediaId}`): never expires and may be stored, but needs the key with scope `media:read` on every fetch."}},"required":["id","kind","contentType","width","height","url","expiresAt","stableUrl"]},"TranscriptSegment":{"type":"object","properties":{"start":{"type":["number","null"],"description":"Second it starts at; null when the transcript has no timing."},"end":{"type":["number","null"],"description":"Second it ends at; null when the transcript has no timing."},"paragraph":{"type":"integer","description":"The paragraph it is in, numbered from 0: the transcript's own paragraphs, or pauses of 2.5 seconds or more where it has none."},"text":{"type":"string"}},"required":["start","end","paragraph","text"],"description":"One sentence of the transcript (or, where the transcript has no punctuation, one run of phrases between pauses)."},"Transcript":{"type":"object","properties":{"state":{"type":"string","enum":["none","partial","ready"],"description":"`ready`: a transcript of what is said is stored. `partial`: words are stored but are not the item's speech (`repeated_line`: Whisper repeated one line over music; do not quote them), or may not be (`possible_lyrics`: voice detection heard nobody yet Whisper found words, speech under loud music or song lyrics; check before quoting). `none`: no transcript; `reason` says why."},"reason":{"type":["string","null"],"enum":["not_spoken","no_speech","awaiting_video","processing","failed","not_captured","repeated_line","possible_lyrics",null],"description":"Why the transcript is not `ready`. `not_spoken`: nothing to transcribe (an article, a picture post). `no_speech`: the audio was checked and nobody speaks. `awaiting_video`: the video file has not arrived from the owner's Mac yet; it is transcribed after. `processing`: fetching or transcribing is still running. `failed`: transcription failed (`message` says why). `not_captured`: nothing is running and no transcript was made. `repeated_line`, `possible_lyrics`: see `partial`. Null when `ready`."},"message":{"type":["string","null"],"description":"What went wrong or what it is waiting for, in words (e.g. `download failed: HTTP 403 Forbidden`)."},"source":{"type":["string","null"],"description":"`platform_subtitle` (the publisher's own subtitles or the platform's captions), `whisper` (transcribed from the audio) or `emby_subtitle`; null without a transcript."},"language":{"type":["string","null"]},"chars":{"type":"integer","description":"Length of the transcript text in characters; 0 without one."},"segments":{"type":"array","items":{"$ref":"#/components/schemas/TranscriptSegment"},"description":"The whole transcript in order; empty when `state` is `none`."}},"required":["state","reason","message","source","language","chars","segments"],"description":"The transcript's state, and its text as timed sentences."},"ReadAs":{"type":"object","properties":{"route":{"type":"string","enum":["timeline","body","media","summary"],"description":"`timeline`: read GET /items/{id}/timeline. `body`: read `body`. `media`: watch, listen or look at the signed media links (no usable text). `summary`: only a title and summary are stored."},"reason":{"type":"string"}},"required":["route","reason"],"description":"The best way to take in this item."},"ItemContent":{"type":"object","properties":{"id":{"type":"string"},"title":{"type":["string","null"]},"summary":{"type":["string","null"]},"body":{"type":["string","null"]},"translatedBody":{"type":["string","null"]},"note":{"type":["string","null"]},"transcript":{"$ref":"#/components/schemas/Transcript"},"readAs":{"$ref":"#/components/schemas/ReadAs"},"media":{"type":"array","items":{"$ref":"#/components/schemas/Media"}}},"required":["id","title","summary","body","translatedBody","note","transcript","readAs","media"]},"CollectInput":{"type":"object","properties":{"url":{"type":"string","format":"uri"},"text":{"description":"Free text containing a link, e.g. what a share sheet hands over.","type":"string","minLength":1,"maxLength":20000},"title":{"type":"string","minLength":1,"maxLength":240},"note":{"type":"string","maxLength":2000},"tags":{"maxItems":20,"type":"array","items":{"type":"string","minLength":1,"maxLength":64}},"clientRef":{"description":"Your own id for this request (e.g. your asset id). Echoed in the response and in every webhook event about this item.","type":"string","minLength":1,"maxLength":200},"readLater":{"description":"`true`: also mark it 稍后读 / 稍后看 (read or watch later; one mark for articles, posts and videos alike), the same as `POST /items/{id}/later`; works on a link already in the library too. Use this instead of writing a todo for something to read or watch.","type":"boolean"}},"description":"Send `url` or `text`."},"CollectResult":{"type":"object","properties":{"status":{"type":"string","const":"accepted"},"itemId":{"type":"string"},"deduplicated":{"type":"boolean"},"collectedAt":{"type":["string","null"],"description":"When a deduplicated link was first collected."},"clientRef":{"type":["string","null"],"description":"The `clientRef` you sent. Collecting a link that is still being processed for this key keeps the first request's value."},"readLaterAt":{"type":["string","null"],"format":"date-time","description":"When it was marked 稍后读 (read later): set with `readLater: true`, or an earlier mark kept; null otherwise."},"itemUrl":{"type":"string","format":"uri"},"contentUrl":{"type":"string","format":"uri"},"timelineUrl":{"type":"string","format":"uri","description":"The item's timeline as JSON, final once `item.ready` arrives."},"webhookEndpoints":{"type":"integer","description":"How many callback webhook endpoints will hear about this item. 0: none registered for this key, so poll `itemUrl` until `processing.status` is `ready` or `failed`."}},"required":["status","itemId","deduplicated","collectedAt","clientRef","readLaterAt","itemUrl","contentUrl","timelineUrl","webhookEndpoints"]},"EnglishWord":{"type":"object","properties":{"word":{"type":"string"},"phonetic":{"type":["string","null"]},"meaning":{"type":["string","null"]},"context":{"type":["string","null"]},"addedAt":{"type":["string","null"]},"mastery":{"type":["integer","null"],"minimum":0,"maximum":5}},"required":["word","phonetic","meaning","context","addedAt","mastery"]},"PracticeRecord":{"type":"object","properties":{"id":{"type":"string"},"dimension":{"type":"string"},"sourceDate":{"type":["string","null"]},"mastery":{"type":["integer","null"]},"createdAt":{"type":"string"},"content":{"description":"Fields depend on the dimension (term, ipa, original, corrected, ...)."}},"required":["id","dimension","sourceDate","mastery","createdAt","content"]},"TimelineEntry":{"type":"object","properties":{"start":{"type":["number","null"],"description":"Seconds from the start; null when this paragraph's time is unknown (it keeps its place in order)."},"kind":{"type":"string","enum":["chapter","speech"],"description":"`chapter`: the publisher's own chapter title. `speech`: a paragraph of what is said."},"text":{"type":"string"}},"required":["start","kind","text"]},"Timeline":{"type":"object","properties":{"id":{"type":"string"},"title":{"type":["string","null"]},"source":{"type":"string"},"url":{"type":["string","null"]},"hubUrl":{"type":"string"},"durationSeconds":{"type":["number","null"]},"language":{"type":["string","null"]},"speech":{"type":"string","enum":["usable","unusable","none"],"description":"Whether the timeline carries what is said. `unusable`: Whisper repeated one line over music; watch the video instead."},"summary":{"type":["string","null"]},"body":{"type":["string","null"],"description":"The post's own text (caption, description)."},"readAs":{"$ref":"#/components/schemas/ReadAs"},"entries":{"type":"array","items":{"$ref":"#/components/schemas/TimelineEntry"}},"media":{"type":"array","items":{"$ref":"#/components/schemas/Media"}}},"required":["id","title","source","url","hubUrl","durationSeconds","language","speech","summary","body","readAs","entries","media"]},"WebhookEvent":{"type":"object","properties":{"id":{"type":"string","description":"Same as the `webhook-id` header: deduplicate on it (retries resend the same id)."},"type":{"type":"string","enum":["item.collected","item.ready","item.failed","webhook.test"]},"timestamp":{"type":"string","format":"date-time","description":"When the event was created (a retry keeps it)."},"data":{"type":"object","properties":{"item":{"type":"object","properties":{"id":{"type":"string","description":"The item's short id, as in /items/{id}."},"url":{"type":["string","null"]},"source":{"type":["string","null"],"description":"Lowercase source code: youtube, tiktok, xiaohongshu, …"},"title":{"type":["string","null"]},"collectedAt":{"type":["string","null"],"format":"date-time"}},"required":["id","url","source","title","collectedAt"]},"clientRef":{"type":["string","null"],"description":"The `clientRef` sent with /collect."},"status":{"type":"string","enum":["processing","ready","failed"]},"stages":{"anyOf":[{"$ref":"#/components/schemas/Stages"},{"type":"null"}]},"reason":{"type":["string","null"],"description":"Only on item.failed."},"links":{"type":"object","properties":{"item":{"type":"string","format":"uri"},"content":{"type":"string","format":"uri"},"timeline":{"type":"string","format":"uri"},"hub":{"type":"string","format":"uri"}},"required":["item","content","timeline","hub"]}},"required":["item","clientRef","status","stages","reason","links"]}},"required":["id","type","timestamp","data"],"examples":[{"id":"evt_5f0c6a3e9b2d4c1a8e7f6d5c4b3a2910","type":"item.ready","timestamp":"2026-10-06T17:04:00.000Z","data":{"item":{"id":"Ab3dE5gH","url":"https://www.tiktok.com/@someone/video/7420000000000000000","source":"tiktok","title":"A dance","collectedAt":"2026-10-06T17:00:12.000Z"},"clientRef":"asset_123","status":"ready","stages":{"text":"ok","media":"ok","transcript":"ok","summary":"ok"},"reason":null,"links":{"item":"https://pih-api.jiajun.fyi/items/Ab3dE5gH","content":"https://pih-api.jiajun.fyi/items/Ab3dE5gH/content","timeline":"https://pih-api.jiajun.fyi/items/Ab3dE5gH/timeline?format=json","hub":"https://jiajun.fyi/i/Ab3dE5gH/a-dance"}}}]},"SongCover":{"type":"object","properties":{"url":{"type":"string","format":"uri"},"width":{"type":["integer","null"],"description":"Pixels, when the CDN URL states it"},"height":{"type":["integer","null"]},"source":{"type":"string","enum":["spotify","apple","netease"],"description":"Which platform's release the cover belongs to"}},"required":["url","width","height","source"],"description":"Album art of one release, hotlinked from the platform's CDN (https). Each link and candidate has its own: they are often different releases (original album vs compilation)."},"SongLink":{"type":"object","properties":{"status":{"type":"string","enum":["pending","matched","needs_review","not_found","failed","manual"]},"trackId":{"type":["string","null"]},"url":{"type":["string","null"]},"name":{"type":["string","null"]},"artist":{"type":["string","null"]},"album":{"type":["string","null"]},"score":{"type":["number","null"],"description":"Match score; 70 and above is trusted."},"reason":{"type":["string","null"],"description":"Why this candidate scored what it did (Chinese)."},"checkedAt":{"type":["string","null"]},"releasedAt":{"description":"Release date of the linked track, as the service reports it (auto-matched links only)","type":"string"},"cover":{"anyOf":[{"$ref":"#/components/schemas/SongCover"},{"type":"null"}],"description":"This track's cover; null when there is no track or the cover is not fetched yet (filled when the link is set, or within the hour)."},"store":{"description":"Apple: the storefront the match was found in (tw / cn).","type":"string"},"usStore":{"type":["boolean","null"],"description":"Apple: the track is sold in the US store (the url then points there)."},"overseasBlocked":{"description":"NetEase: greyed out outside mainland China (plays inside).","type":"boolean"},"alternatives":{"description":"needs_review only: the next candidates, with score, reason and their own cover.","type":"array","items":{"type":"object","properties":{"trackId":{"type":"string"},"name":{"type":"string"},"artist":{"type":"string"},"album":{"type":["string","null"]},"releasedAt":{"type":["string","null"]},"url":{"type":"string"},"overseasBlocked":{"description":"NetEase only, for candidates found after 2026-10","type":"boolean"},"score":{"type":"number"},"reason":{"type":"string"},"cover":{"anyOf":[{"$ref":"#/components/schemas/SongCover"},{"type":"null"}]}},"required":["trackId","name","artist","url","score","reason","cover"]}}},"required":["status","url","cover"],"description":"The song's track on one service. `status`: `pending` (not matched yet), `matched` (trusted automatic match), `needs_review` (best candidate, not trusted; `alternatives` lists the runners-up), `not_found` (searched, no version with the right title and artist), `failed` (search error, retried later), `manual` (set by hand; never re-matched). Only `matched` and `manual` links count as linked."},"Song":{"type":"object","properties":{"key":{"type":"string","description":"「歌名|第一位歌手」, e.g. `好汉歌|刘欢`; URL-encode it in paths."},"title":{"type":"string"},"artist":{"type":"string"},"cover":{"anyOf":[{"$ref":"#/components/schemas/SongCover"},{"type":"null"}],"description":"The cover to show for the song: a checked link's (matched or set by hand) before an unchecked candidate's; among those Apple Music, then Spotify, then NetEase."},"links":{"type":"object","properties":{"spotify":{"$ref":"#/components/schemas/SongLink"},"apple":{"$ref":"#/components/schemas/SongLink"},"netease":{"$ref":"#/components/schemas/SongLink"}},"required":["spotify","apple","netease"]},"mentions":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"episode":{"type":"integer"},"at":{"type":["string","null"],"description":"hh:mm:ss into the episode"},"how":{"type":["string","null"],"description":"sung | hummed | mentioned"},"who":{"type":["string","null"]},"workType":{"type":["string","null"]},"work":{"type":["string","null"]},"role":{"type":["string","null"]},"year":{"type":["string","null"]},"lyric":{"type":["string","null"],"description":"The line as sung or hummed in the show"},"note":{"type":["string","null"],"description":"Context: what was going on in the show at this point"},"fixed":{"type":["string","null"],"description":"What checking corrected (work, role, artist…) and the source; null when nothing was corrected"}},"required":["id","episode","at","how","who","workType","work","role","year","lyric","note","fixed"]},"description":"Where the song comes up in the show."}},"required":["key","title","artist","cover","links","mentions"]},"TracklistSummary":{"type":"object","properties":{"id":{"type":"string","description":"`1001tracklists:<set id>`; URL-encode it in paths."},"source":{"type":"string"},"sourceId":{"type":"string"},"url":{"type":"string"},"title":{"type":["string","null"]},"status":{"type":"string","enum":["queued","fetched","parsed","failed"],"description":"queued: waiting for the logged-in Chrome to open it; fetched: page stored, no rows read yet; parsed: rows read."},"error":{"type":["string","null"]},"details":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}},"requestedAt":{"type":"string"},"fetchedAt":{"type":["string","null"]},"parsedAt":{"type":["string","null"]},"entries":{"type":"integer","description":"Number of rows read."}},"required":["id","source","sourceId","url","title","status","error","details","requestedAt","fetchedAt","parsedAt","entries"],"description":"One set in the list, with how many rows were read."},"ScoreTakeSummary":{"type":"object","properties":{"id":{"type":"string"},"title":{"type":["string","null"]},"songKey":{"type":["string","null"],"description":"The songbook song it was recorded for, 「歌名|歌手」."},"createdAt":{"type":"string","format":"date-time"},"durationSeconds":{"type":"number"},"bpm":{"type":"number","description":"Estimated from the onsets (or set by hand on the page); quarter notes per minute."},"meter":{"type":"string","description":"`4/4`, `3/4` or `2/4`."},"key":{"type":["object","null"],"properties":{"tonic":{"type":"integer","description":"Pitch class, 0 = C."},"mode":{"type":"string","enum":["major","minor"]}},"required":["tonic","mode"]},"keyName":{"type":["string","null"],"description":"The key in Chinese, e.g. 「C 大调」."},"noteCount":{"type":"integer"},"lyrics":{"type":"object","properties":{"status":{"type":"string","enum":["pending","ready","none","failed","unavailable"],"description":"`pending`: Whisper is still listening (about a minute). `ready`: `text` has the words and the MusicXML has them under the notes. `none`: nobody sings words. `failed`: see `error`. `unavailable`: no recording, or no inference machine to send it to."},"text":{"type":["string","null"],"description":"The sung words, one line per phrase."},"language":{"type":["string","null"]},"error":{"type":["string","null"]}},"required":["status","text","language","error"]}},"required":["id","title","songKey","createdAt","durationSeconds","bpm","meter","key","keyName","noteCount","lyrics"],"description":"One take saved from 实时扒谱 (jiajun.fyi/music-transcription), without its MusicXML. Shape: `ScoreTakeSummary` in packages/music-score/types.ts."},"ScoreTake":{"type":"object","properties":{"id":{"type":"string"},"title":{"type":["string","null"]},"songKey":{"type":["string","null"],"description":"The songbook song it was recorded for, 「歌名|歌手」."},"createdAt":{"type":"string","format":"date-time"},"durationSeconds":{"type":"number"},"bpm":{"type":"number","description":"Estimated from the onsets (or set by hand on the page); quarter notes per minute."},"meter":{"type":"string","description":"`4/4`, `3/4` or `2/4`."},"key":{"type":["object","null"],"properties":{"tonic":{"type":"integer","description":"Pitch class, 0 = C."},"mode":{"type":"string","enum":["major","minor"]}},"required":["tonic","mode"]},"keyName":{"type":["string","null"],"description":"The key in Chinese, e.g. 「C 大调」."},"noteCount":{"type":"integer"},"lyrics":{"type":"object","properties":{"status":{"type":"string","enum":["pending","ready","none","failed","unavailable"],"description":"`pending`: Whisper is still listening (about a minute). `ready`: `text` has the words and the MusicXML has them under the notes. `none`: nobody sings words. `failed`: see `error`. `unavailable`: no recording, or no inference machine to send it to."},"text":{"type":["string","null"],"description":"The sung words, one line per phrase."},"language":{"type":["string","null"]},"error":{"type":["string","null"]}},"required":["status","text","language","error"]},"musicxml":{"type":"string"}},"required":["id","title","songKey","createdAt","durationSeconds","bpm","meter","key","keyName","noteCount","lyrics","musicxml"],"description":"A take with its score: MusicXML 4.0 score-partwise, one part, chords as `<chord/>`, lyrics as `<lyric>` under the notes once ready."},"Tracklist":{"type":"object","properties":{"id":{"type":"string","description":"`1001tracklists:<set id>`; URL-encode it in paths."},"source":{"type":"string"},"sourceId":{"type":"string"},"url":{"type":"string"},"title":{"type":["string","null"]},"status":{"type":"string","enum":["queued","fetched","parsed","failed"],"description":"queued: waiting for the logged-in Chrome to open it; fetched: page stored, no rows read yet; parsed: rows read."},"error":{"type":["string","null"]},"details":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}},"requestedAt":{"type":"string"},"fetchedAt":{"type":["string","null"]},"parsedAt":{"type":["string","null"]},"entries":{"type":"array","items":{"type":"object","properties":{"seq":{"type":"integer"},"position":{"type":["integer","null"]},"layer":{"type":"string","enum":["main","with","mashup_part"]},"cue":{"type":["string","null"],"description":"h:mm:ss into the set"},"artist":{"type":["string","null"],"description":"null when the page says ID"},"title":{"type":["string","null"]},"version":{"type":["string","null"]},"label":{"type":["string","null"]},"rawText":{"type":"string"},"sourceTrackId":{"type":["string","null"]}},"required":["seq","position","layer","cue","artist","title","version","label","rawText","sourceTrackId"]}}},"required":["id","source","sourceId","url","title","status","error","details","requestedAt","fetchedAt","parsedAt","entries"],"description":"A set and its rows in page order."},"SongSheet":{"type":"object","properties":{"key":{"type":"string","description":"The songbook song key 「歌名|歌手」."},"title":{"type":"string"},"artist":{"type":"string"},"hasScore":{"type":"boolean"},"format":{"type":["string","null"],"enum":["abc","musicxml",null],"description":"How to read `score`; null while there is none."},"score":{"type":["string","null"],"description":"The melody as ABC (verse 1 under the notes as `w:` lines) or MusicXML text (lyrics as `<lyric>`); null: 缺谱."},"lyrics":{"type":["string","null"],"description":"Every verse as plain text, a blank line between verses."},"coverMotif":{"type":["string","null"],"enum":["star","letters","tiger","cake","sheep","bridge","boat","gourd","horse","heart","notes",null],"description":"The picture on the cover; null: picked from the title."},"source":{"type":["string","null"],"description":"Where the melody came from."},"updatedAt":{"type":["string","null"],"format":"date-time","description":"When the sheet last changed; null: nothing set yet."}},"required":["key","title","artist","hasScore","format","score","lyrics","coverMotif","source","updatedAt"],"description":"A songbook song's 图图乐谱 sheet: its melody as ABC or MusicXML text, every verse of the lyrics, and the motif of its drawn cover. `hasScore: false` is 缺谱: no melody entered yet."},"WantedInput":{"type":"object","properties":{"text":{"description":"What the owner said, as said: `Graza 橄榄油 Whole Foods 12.99`, or share text with a link. A model reads name, brand, price and shop from it; the first link in it counts as `url`.","type":"string","maxLength":2000},"url":{"description":"A product page. Kept on the product; 查商品 reads it for the official picture and price.","type":"string","maxLength":1000,"format":"uri"},"name":{"description":"The product's name, when the caller already knows it (skips the model).","type":"string","maxLength":200},"brand":{"type":"string","maxLength":120},"price":{"description":"Dollars, e.g. `12.99` or `\"$12.99\"`. Becomes a price observation (source `user`).","anyOf":[{"type":"number"},{"type":"string"},{"type":"null"}]},"where":{"description":"Where that price was seen: a shop or site (`Whole Foods`, `target.com`).","type":"string","maxLength":120},"note":{"description":"Why, for whom, which flavour. Kept on the product once.","type":"string","maxLength":1000},"status":{"description":"Default 想买. The owner said it, so it is recorded with status source `user` and no model's guess changes it later.","default":"wanted","type":"string","enum":["wanted","owned","passed","想买","已买","不要了"]},"images":{"description":"https addresses of photos (JPEG / PNG). They go through 图片收录 and name the product.","maxItems":4,"type":"array","items":{"type":"string","format":"uri"}},"dedupeKey":{"description":"Your own id for this request. The same key again returns the first answer and adds nothing. Without it, the same content on the same day (Pacific time) counts as the same request.","type":"string","maxLength":200}},"description":"At least one of `text`, `url`, `name`, `images` (or an image file in a multipart body)."},"WantedResult":{"type":"object","properties":{"message":{"type":"string","description":"One sentence to show as is: `已记到想买：Graza Drizzle 橄榄油（新建）`, `已经在想买里了：Graza Drizzle，记下了新价格 $12.99（Whole Foods）`, `已改成想买：…（之前是不要了）`."},"created":{"type":"boolean","description":"A new product was made (otherwise an existing one was updated)."},"product":{"type":"object","properties":{"publicId":{"type":"string"},"path":{"type":"string"},"url":{"type":"string","description":"Its page on things.jiajun.fyi."},"name":{"type":"string"},"brand":{"type":["string","null"]},"status":{"type":"string","enum":["wanted","owned","passed"]},"statusSource":{"type":"string","enum":["model","purchase","user"]}},"required":["publicId","path","url","name","brand","status","statusSource"]},"changes":{"type":"object","properties":{"status":{"type":["object","null"],"properties":{"from":{"type":"string"},"to":{"type":"string"}},"required":["from","to"]},"price":{"type":["integer","null"],"description":"Cents recorded."},"note":{"type":"boolean"},"link":{"type":"boolean"}},"required":["status","price","note","link"]},"readBy":{"description":"What named the product: the fields given, the model reading `text`, the words themselves (model unavailable), a photo, the page at `url`, or the address alone.","type":"string","enum":["fields","model","words","picture","page","link"]},"repeated":{"description":"This is the stored answer of an earlier identical request; nothing was added.","type":"boolean"}},"required":["message","created","product"]},"ThingsProduct":{"type":"object","properties":{"id":{"type":"string","description":"The product's 短 ID."},"name":{"type":"string"},"brand":{"type":["string","null"]},"kind":{"type":"string","enum":["food","appliance","household","other"]},"status":{"type":"string","enum":["wanted","owned","passed"]},"url":{"type":"string"},"lastPrice":{"type":["object","null"],"properties":{"cents":{"type":"integer"},"retailer":{"type":["string","null"]},"where":{"type":["string","null"]},"at":{"type":"string"}},"required":["cents","retailer","where","at"],"description":"The latest price seen."},"lowestPrice":{"type":["object","null"],"properties":{"cents":{"type":"integer"},"retailer":{"type":["string","null"]},"where":{"type":["string","null"]},"at":{"type":"string"}},"required":["cents","retailer","where","at"],"description":"The lowest price seen."},"lastBought":{"type":["string","null"]}},"required":["id","name","brand","kind","status","url","lastPrice","lowestPrice","lastBought"]},"WantedWebhook":{"type":"object","properties":{"configured":{"type":"boolean"},"url":{"type":["string","null"],"description":"The webhook address (secret). null until made."},"updatedAt":{"type":["string","null"],"format":"date-time"},"shortcut":{"type":"object","properties":{"ready":{"type":"boolean","description":"A Shortcut signed with the current address is ready to download."},"url":{"type":["string","null"],"description":"Where to download it (GET with the key); null while not ready."},"signedAt":{"type":["string","null"],"format":"date-time"}},"required":["ready","url","signedAt"]}},"required":["configured","url","updatedAt","shortcut"]},"RestaurantInput":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":100,"description":"The name as the restaurant writes it (Latin script when it has one): `Hwa Mi Won`."},"nameLocal":{"type":["string","null"],"maxLength":100,"description":"The name in its own script: 화미원, 金子半之助."},"area":{"description":"`sf` (三藩), `peninsula` (半岛), `south_bay` (南湾), `east_bay` (东湾), `north_bay` (北湾), `other` (周边), `travel` (外地: outside the Bay Area and 周边, the travel list; needs `country` and `city`); the Chinese words work too.","type":"string","enum":["sf","peninsula","south_bay","east_bay","north_bay","other","travel","三藩","旧金山","三藩市","SF","半岛","南湾","东湾","北湾","周边","其他","外地","旅行"]},"country":{"description":"ISO 3166-1 alpha-2 code: `US`, `CN`, `JP`, `KR`; 美国 / 中国 / 日本 / 韩国 work too. Default `US` outside 外地.","type":"string"},"city":{"type":["string","null"],"maxLength":80,"description":"City, plus the neighbourhood in brackets: `San Francisco (Richmond)`, `San Mateo`; for 外地 the city alone, in its usual English or Chinese name (`Seattle`, `上海`), which is what the travel list groups by."},"address":{"type":["string","null"],"maxLength":300,"description":"Street address. With the name, it tells stores apart: another address is another restaurant (linked to the same-name ones)."},"phone":{"type":["string","null"],"maxLength":40},"hours":{"type":["string","null"],"maxLength":300,"description":"Opening hours as written: `周三–周日 17:30–22:30`."},"inMall":{"description":"Inside a mall, food court or estate: photos up to 100 m away count as being there (75 m otherwise).","type":"boolean"},"location":{"type":["object","null"],"properties":{"latitude":{"type":"number","minimum":-90,"maximum":90},"longitude":{"type":"number","minimum":-180,"maximum":180}},"required":["latitude","longitude"],"description":"The point on the map, set by hand when the automatic lookup is wrong; `null` looks it up again from the address."},"cuisine":{"type":["string","null"],"maxLength":60,"description":"Short Chinese label, reused across restaurants: 韩餐, 日料, 越南菜, 上海菜, 粤菜."},"priceLevel":{"type":["integer","null"],"minimum":1,"maximum":4,"description":"1-4, like $ to $$$$."},"status":{"description":"`want` (想去) or `been` (去过); the Chinese words work too.","type":"string","enum":["want","been","想去","去过","吃过"]},"dishes":{"description":"Recommended dishes.","maxItems":50,"type":"array","items":{"anyOf":[{"type":"string","minLength":1,"maxLength":100},{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":100},"note":{"type":"string","maxLength":300}},"required":["name"]}],"description":"A dish name, or `{name, note}` (price, what is in it, how to order it)."}},"tips":{"description":"Practical notes, one per line: reservations, waits, hours, what to skip (`可订位`, `周二休息`, `雪浓汤偏淡可不点`).","maxItems":30,"type":"array","items":{"type":"string","minLength":1,"maxLength":300}},"links":{"description":"Official site, Yelp, reviews.","maxItems":20,"type":"array","items":{"anyOf":[{"type":"string","maxLength":1000,"format":"uri"},{"type":"object","properties":{"url":{"type":"string","maxLength":1000,"format":"uri"},"label":{"type":"string","maxLength":60}},"required":["url"]}],"description":"A URL, or `{url, label}` (`官网`, `Yelp`, `The Infatuation`)."}},"notes":{"type":["string","null"],"maxLength":2000,"description":"Anything else: the chef, who recommended it. Another branch is a restaurant of its own (with its address), linked to this one."},"sourceItemIds":{"description":"Collected posts (item ids from `/search` or `/items`) that recommended it.","maxItems":20,"type":"array","items":{"type":"string","minLength":1,"maxLength":80}},"visit":{"description":"Also record a visit (and so status 去过), in the same call.","$ref":"#/components/schemas/VisitInput"}},"required":["name"],"additionalProperties":false},"RestaurantPatch":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":100},"nameLocal":{"type":["string","null"],"maxLength":100,"description":"The name in its own script: 화미원, 金子半之助."},"area":{"description":"`sf` (三藩), `peninsula` (半岛), `south_bay` (南湾), `east_bay` (东湾), `north_bay` (北湾), `other` (周边), `travel` (外地: outside the Bay Area and 周边, the travel list; needs `country` and `city`); the Chinese words work too.","type":"string","enum":["sf","peninsula","south_bay","east_bay","north_bay","other","travel","三藩","旧金山","三藩市","SF","半岛","南湾","东湾","北湾","周边","其他","外地","旅行"]},"country":{"description":"ISO 3166-1 alpha-2 code: `US`, `CN`, `JP`, `KR`; 美国 / 中国 / 日本 / 韩国 work too. Default `US` outside 外地.","type":"string"},"city":{"type":["string","null"],"maxLength":80,"description":"City, plus the neighbourhood in brackets: `San Francisco (Richmond)`, `San Mateo`; for 外地 the city alone, in its usual English or Chinese name (`Seattle`, `上海`), which is what the travel list groups by."},"address":{"type":["string","null"],"maxLength":300,"description":"Street address. With the name, it tells stores apart: another address is another restaurant (linked to the same-name ones)."},"phone":{"type":["string","null"],"maxLength":40},"hours":{"type":["string","null"],"maxLength":300,"description":"Opening hours as written: `周三–周日 17:30–22:30`."},"inMall":{"description":"Inside a mall, food court or estate: photos up to 100 m away count as being there (75 m otherwise).","type":"boolean"},"location":{"type":["object","null"],"properties":{"latitude":{"type":"number","minimum":-90,"maximum":90},"longitude":{"type":"number","minimum":-180,"maximum":180}},"required":["latitude","longitude"],"description":"The point on the map, set by hand when the automatic lookup is wrong; `null` looks it up again from the address."},"cuisine":{"type":["string","null"],"maxLength":60,"description":"Short Chinese label, reused across restaurants: 韩餐, 日料, 越南菜, 上海菜, 粤菜."},"priceLevel":{"type":["integer","null"],"minimum":1,"maximum":4,"description":"1-4, like $ to $$$$."},"status":{"description":"`want` (想去) or `been` (去过); the Chinese words work too.","type":"string","enum":["want","been","想去","去过","吃过"]},"dishes":{"description":"Recommended dishes.","maxItems":50,"type":"array","items":{"anyOf":[{"type":"string","minLength":1,"maxLength":100},{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":100},"note":{"type":"string","maxLength":300}},"required":["name"]}],"description":"A dish name, or `{name, note}` (price, what is in it, how to order it)."}},"tips":{"description":"Practical notes, one per line: reservations, waits, hours, what to skip (`可订位`, `周二休息`, `雪浓汤偏淡可不点`).","maxItems":30,"type":"array","items":{"type":"string","minLength":1,"maxLength":300}},"links":{"description":"Official site, Yelp, reviews.","maxItems":20,"type":"array","items":{"anyOf":[{"type":"string","maxLength":1000,"format":"uri"},{"type":"object","properties":{"url":{"type":"string","maxLength":1000,"format":"uri"},"label":{"type":"string","maxLength":60}},"required":["url"]}],"description":"A URL, or `{url, label}` (`官网`, `Yelp`, `The Infatuation`)."}},"notes":{"type":["string","null"],"maxLength":2000,"description":"Anything else: the chef, who recommended it. Another branch is a restaurant of its own (with its address), linked to this one."},"sourceItemIds":{"description":"Collected posts (item ids from `/search` or `/items`) that recommended it.","maxItems":20,"type":"array","items":{"type":"string","minLength":1,"maxLength":80}}},"additionalProperties":false,"description":"Only the fields sent change; a list sent replaces the whole list; `null` clears a text field."},"VisitInput":{"type":"object","properties":{"date":{"description":"YYYY-MM-DD; default today (Pacific time).","type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}$"},"who":{"description":"Who went: `全家`, `和老婆`.","type":"string","maxLength":200},"ordered":{"description":"What we ordered.","maxItems":50,"type":"array","items":{"type":"string","minLength":1,"maxLength":100}},"rating":{"description":"1-5.","type":"integer","minimum":1,"maximum":5},"notes":{"type":"string","maxLength":2000}},"additionalProperties":false,"description":"One meal. Every field is optional: `{}` just records that we went today."},"RestaurantLinkInput":{"type":"object","properties":{"restaurantId":{"type":"string","minLength":1,"maxLength":80,"description":"The other store (its id)."},"kind":{"default":"chain","type":"string","enum":["chain"]}},"required":["restaurantId"],"additionalProperties":false,"description":"Link another store: a person saying so, so the link is 已确认."},"RestaurantLinkPatch":{"type":"object","properties":{"status":{"type":"string","enum":["confirmed","review","removed"],"description":"`confirmed` 已确认, `review` 待清洗 (to sort out later), `removed`: not related, the link goes."}},"required":["status"],"additionalProperties":false},"Visit":{"type":"object","properties":{"id":{"type":"string"},"date":{"type":"string","description":"YYYY-MM-DD."},"source":{"type":"string","enum":["manual","photos"],"description":"`manual`: a person or an agent recorded it; `photos` (相册识别): made from photos taken there."},"photos":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","description":"The photo's 照片短 ID."},"kind":{"type":"string","description":"`image`, or `video` (its still)."},"takenAt":{"type":"string","format":"date-time"},"width":{"type":["integer","null"]},"height":{"type":["integer","null"]},"url":{"type":"string","format":"uri","description":"Signed link to a 1024 px JPEG (no location or camera data), valid for an hour."},"thumbUrl":{"type":"string","format":"uri","description":"The same link (the public API has one size)."}},"required":["id","kind","takenAt","width","height","url","thumbUrl"]},"description":"Photos taken there that day (from Apple Photos, 相册识别), oldest first."},"who":{"type":["string","null"]},"ordered":{"type":"array","items":{"type":"string"}},"rating":{"type":["integer","null"],"description":"1-5."},"notes":{"type":["string","null"]},"createdAt":{"type":"string","format":"date-time"}},"required":["id","date","source","photos","who","ordered","rating","notes","createdAt"]},"Restaurant":{"type":"object","properties":{"id":{"type":"string","description":"The restaurant's 短 ID."},"name":{"type":"string"},"nameLocal":{"type":["string","null"]},"area":{"type":"string","enum":["sf","peninsula","south_bay","east_bay","north_bay","other","travel"],"description":"`sf` 三藩, `peninsula` 半岛, `south_bay` 南湾, `east_bay` 东湾, `north_bay` 北湾, `other` 周边, `travel` 外地 (the travel list)."},"country":{"type":"string","description":"ISO 3166-1 alpha-2 code: `US`, `CN`, `JP`."},"countryName":{"type":"string","description":"The country in Chinese: 美国, 中国, 日本."},"city":{"type":["string","null"],"description":"City (neighbourhood in brackets in the Bay Area); always set for 外地, where the travel list groups by it."},"address":{"type":["string","null"]},"phone":{"type":["string","null"]},"hours":{"type":["string","null"],"description":"Opening hours as written: `周三–周日 17:30–22:30`."},"location":{"type":["object","null"],"properties":{"latitude":{"type":"number"},"longitude":{"type":"number"},"precision":{"type":"string","enum":["address","place","street","city"],"description":"`address` 门牌 (house number), `place` 商家 (the restaurant itself on the map), `street` 路段, `city` 城市 (only the town)."},"setByHand":{"type":"boolean"}},"required":["latitude","longitude","precision","setByHand"],"description":"店的坐标, looked up from the address on OpenStreetMap; null until found."},"inMall":{"type":"boolean","description":"Inside a mall, food court or estate (photos up to 100 m away count)."},"cuisine":{"type":["string","null"]},"priceLevel":{"type":["integer","null"],"description":"1-4, like $ to $$$$."},"status":{"type":"string","enum":["want","been"],"description":"`want` 想去, `been` 去过."},"dishes":{"type":"array","items":{"type":"object","properties":{"name":{"type":"string"},"note":{"type":"string"}},"required":["name"]},"description":"Recommended dishes."},"tips":{"type":"array","items":{"type":"string"},"description":"Reservations, waits, hours, what to skip."},"links":{"type":"array","items":{"type":"object","properties":{"url":{"type":"string"},"label":{"type":"string"}},"required":["url"]}},"notes":{"type":["string","null"]},"sources":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"title":{"type":["string","null"]},"url":{"type":"string","format":"uri","description":"The item on the hub."},"originalUrl":{"type":"string","description":"The post itself, where it was collected from."},"source":{"type":"string","description":"Where it was collected from: `xiaohongshu`, `instagram`, ..."}},"required":["id","title","url","originalUrl","source"]},"description":"Collected posts that recommended it."},"addedBy":{"type":"string","enum":["manual","auto"],"description":"How it first got here: `manual` (someone added it) or `auto` (自动识别 found it in a newly collected post, see `sources`)."},"relatedCount":{"type":"integer","description":"How many other stores are linked to this one (关联的店: other branches of the same restaurant)."},"visitCount":{"type":"integer"},"lastVisitedOn":{"type":["string","null"]},"averageRating":{"type":["number","null"],"description":"Mean of the visits' ratings."},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"}},"required":["id","name","nameLocal","area","country","countryName","city","address","phone","hours","location","inMall","cuisine","priceLevel","status","dishes","tips","links","notes","sources","addedBy","relatedCount","visitCount","lastVisitedOn","averageRating","createdAt","updatedAt"]},"RelatedRestaurant":{"type":"object","properties":{"id":{"type":"string","description":"The other store's 短 ID."},"name":{"type":"string"},"nameLocal":{"type":["string","null"]},"area":{"type":"string","enum":["sf","peninsula","south_bay","east_bay","north_bay","other","travel"]},"country":{"type":"string"},"countryName":{"type":"string"},"city":{"type":["string","null"]},"address":{"type":["string","null"]},"status":{"type":"string","enum":["want","been"]},"link":{"type":"object","properties":{"id":{"type":"string","description":"The link's id (for PATCH /restaurants/{id}/links/{linkId})."},"kind":{"type":"string","enum":["chain"],"description":"`chain`: 同品牌 / 分店, the same restaurant somewhere else."},"confidence":{"type":"number","description":"0-1."},"reason":{"type":["string","null"]},"status":{"type":"string","enum":["auto","confirmed","review"],"description":"`auto` 自动关联 (same name), `confirmed` 已确认, `review` 待清洗 (likely wrong, e.g. the same name in another country): say so rather than state it as fact."},"statusLabel":{"type":"string","description":"The status in Chinese."}},"required":["id","kind","confidence","reason","status","statusLabel"]}},"required":["id","name","nameLocal","area","country","countryName","city","address","status","link"]},"RestaurantDetail":{"type":"object","properties":{"id":{"type":"string","description":"The restaurant's 短 ID."},"name":{"type":"string"},"nameLocal":{"type":["string","null"]},"area":{"type":"string","enum":["sf","peninsula","south_bay","east_bay","north_bay","other","travel"],"description":"`sf` 三藩, `peninsula` 半岛, `south_bay` 南湾, `east_bay` 东湾, `north_bay` 北湾, `other` 周边, `travel` 外地 (the travel list)."},"country":{"type":"string","description":"ISO 3166-1 alpha-2 code: `US`, `CN`, `JP`."},"countryName":{"type":"string","description":"The country in Chinese: 美国, 中国, 日本."},"city":{"type":["string","null"],"description":"City (neighbourhood in brackets in the Bay Area); always set for 外地, where the travel list groups by it."},"address":{"type":["string","null"]},"phone":{"type":["string","null"]},"hours":{"type":["string","null"],"description":"Opening hours as written: `周三–周日 17:30–22:30`."},"location":{"type":["object","null"],"properties":{"latitude":{"type":"number"},"longitude":{"type":"number"},"precision":{"type":"string","enum":["address","place","street","city"],"description":"`address` 门牌 (house number), `place` 商家 (the restaurant itself on the map), `street` 路段, `city` 城市 (only the town)."},"setByHand":{"type":"boolean"}},"required":["latitude","longitude","precision","setByHand"],"description":"店的坐标, looked up from the address on OpenStreetMap; null until found."},"inMall":{"type":"boolean","description":"Inside a mall, food court or estate (photos up to 100 m away count)."},"cuisine":{"type":["string","null"]},"priceLevel":{"type":["integer","null"],"description":"1-4, like $ to $$$$."},"status":{"type":"string","enum":["want","been"],"description":"`want` 想去, `been` 去过."},"dishes":{"type":"array","items":{"type":"object","properties":{"name":{"type":"string"},"note":{"type":"string"}},"required":["name"]},"description":"Recommended dishes."},"tips":{"type":"array","items":{"type":"string"},"description":"Reservations, waits, hours, what to skip."},"links":{"type":"array","items":{"type":"object","properties":{"url":{"type":"string"},"label":{"type":"string"}},"required":["url"]}},"notes":{"type":["string","null"]},"sources":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"title":{"type":["string","null"]},"url":{"type":"string","format":"uri","description":"The item on the hub."},"originalUrl":{"type":"string","description":"The post itself, where it was collected from."},"source":{"type":"string","description":"Where it was collected from: `xiaohongshu`, `instagram`, ..."}},"required":["id","title","url","originalUrl","source"]},"description":"Collected posts that recommended it."},"addedBy":{"type":"string","enum":["manual","auto"],"description":"How it first got here: `manual` (someone added it) or `auto` (自动识别 found it in a newly collected post, see `sources`)."},"relatedCount":{"type":"integer","description":"How many other stores are linked to this one (关联的店: other branches of the same restaurant)."},"visitCount":{"type":"integer"},"lastVisitedOn":{"type":["string","null"]},"averageRating":{"type":["number","null"],"description":"Mean of the visits' ratings."},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"},"related":{"type":"array","items":{"$ref":"#/components/schemas/RelatedRestaurant"},"description":"关联的店: other stores linked to this one. Each store is its own restaurant (its own dishes, visits, reviews); a link only says they belong together."},"visits":{"type":"array","items":{"$ref":"#/components/schemas/Visit"}}},"required":["id","name","nameLocal","area","country","countryName","city","address","phone","hours","location","inMall","cuisine","priceLevel","status","dishes","tips","links","notes","sources","addedBy","relatedCount","visitCount","lastVisitedOn","averageRating","createdAt","updatedAt","related","visits"],"description":"A restaurant with its linked stores and every visit, newest first."},"RestaurantAdded":{"type":"object","properties":{"message":{"type":"string","description":"One sentence to show as is: `已记到餐厅：Hwa Mi Won（新建，想去）`, `已经在餐厅里了：Daeho，补上了 phone、tips`."},"created":{"type":"boolean","description":"A new restaurant was made (otherwise the one already there was found)."},"filled":{"type":"array","items":{"type":"string"},"description":"On a found restaurant: the fields this call filled in or added to (nothing is overwritten)."},"restaurant":{"$ref":"#/components/schemas/RestaurantDetail"},"visit":{"anyOf":[{"$ref":"#/components/schemas/Visit"},{"type":"null"}],"description":"The visit recorded with `visit`, else null."}},"required":["message","created","filled","restaurant","visit"]},"VisitAdded":{"type":"object","properties":{"message":{"type":"string","description":"One sentence to show as is: `记下了 2026-10-11 去 Hwa Mi Won 吃，已改成去过`."},"created":{"type":"boolean","description":"False when that day already had a visit, which this call filled in."},"visit":{"$ref":"#/components/schemas/Visit"},"restaurant":{"$ref":"#/components/schemas/RestaurantDetail"}},"required":["message","created","visit","restaurant"]},"ReviewItem":{"type":"object","properties":{"id":{"type":"string"},"author":{"type":["string","null"],"description":"The reviewer's display name as the source shows it (forum handle, Google / Yelp name)."},"rating":{"type":["number","null"],"description":"On the listing's `ratingScale`."},"text":{"type":["string","null"],"description":"An excerpt (up to about 400 characters), never the whole text."},"publishedAt":{"type":["string","null"]},"url":{"type":["string","null"],"description":"The review or post itself."}},"required":["id","author","rating","text","publishedAt","url"]},"ReviewListing":{"type":"object","properties":{"id":{"type":"string","description":"The listing's id (for PATCH `listingId`)."},"source":{"type":"string","enum":["google","yelp","opentable","infatuation","hungry_onion","article"]},"url":{"type":"string","format":"uri","description":"The page on the source: link out to it."},"title":{"type":["string","null"]},"matchStatus":{"type":"string","enum":["confirmed","pending"],"description":"`confirmed` 已确认: show as fact. `pending` 待确认: it may be another store of the same name; say so, and settle it with PATCH."},"confidence":{"type":"number","description":"0-1: how sure the match is."},"matchReason":{"type":["string","null"],"description":"Why it is believed to be this restaurant, in Chinese."},"manual":{"type":"boolean","description":"Named by a person (a link on the restaurant, or PATCH)."},"rating":{"type":["number","null"],"description":"Google and Yelp: stars out of 5; The Infatuation: x/10 (newer reviews have none)."},"ratingScale":{"type":["number","null"],"description":"5, 10, or null when the source gives no rating."},"reviewCount":{"type":["integer","null"],"description":"Google / Yelp: number of reviews; Hungry Onion: posts in the thread."},"priceLevel":{"type":["integer","null"],"description":"1-4, like $ to $$$$."},"summary":{"type":["string","null"],"description":"One or two lines: a Chinese summary for editorial sources (the source's own words when no model was free), Google's review summary when it has one."},"dishes":{"type":"array","items":{"type":"string"},"description":"Dishes the review recommends (editorial sources)."},"skip":{"type":"array","items":{"type":"string"},"description":"Dishes it says to skip."},"hours":{"type":"array","items":{"type":"string"},"description":"Opening hours as the platform lists them."},"linkOnly":{"type":"boolean","description":"OpenTable: only the page's address is kept (its robots.txt allows no fetching)."},"publishedAt":{"type":["string","null"]},"fetchedAt":{"type":"string","format":"date-time"},"stale":{"type":"boolean","description":"Older than the platform lets us keep it (Google 30 days, Yelp 24 hours): a refetch has been queued."}},"required":["id","source","url","title","matchStatus","confidence","matchReason","manual","rating","ratingScale","reviewCount","priceLevel","summary","dishes","skip","hours","linkOnly","publishedAt","fetchedAt","stale"]},"ReviewListingWithItems":{"type":"object","properties":{"id":{"type":"string","description":"The listing's id (for PATCH `listingId`)."},"source":{"type":"string","enum":["google","yelp","opentable","infatuation","hungry_onion","article"]},"url":{"type":"string","format":"uri","description":"The page on the source: link out to it."},"title":{"type":["string","null"]},"matchStatus":{"type":"string","enum":["confirmed","pending"],"description":"`confirmed` 已确认: show as fact. `pending` 待确认: it may be another store of the same name; say so, and settle it with PATCH."},"confidence":{"type":"number","description":"0-1: how sure the match is."},"matchReason":{"type":["string","null"],"description":"Why it is believed to be this restaurant, in Chinese."},"manual":{"type":"boolean","description":"Named by a person (a link on the restaurant, or PATCH)."},"rating":{"type":["number","null"],"description":"Google and Yelp: stars out of 5; The Infatuation: x/10 (newer reviews have none)."},"ratingScale":{"type":["number","null"],"description":"5, 10, or null when the source gives no rating."},"reviewCount":{"type":["integer","null"],"description":"Google / Yelp: number of reviews; Hungry Onion: posts in the thread."},"priceLevel":{"type":["integer","null"],"description":"1-4, like $ to $$$$."},"summary":{"type":["string","null"],"description":"One or two lines: a Chinese summary for editorial sources (the source's own words when no model was free), Google's review summary when it has one."},"dishes":{"type":"array","items":{"type":"string"},"description":"Dishes the review recommends (editorial sources)."},"skip":{"type":"array","items":{"type":"string"},"description":"Dishes it says to skip."},"hours":{"type":"array","items":{"type":"string"},"description":"Opening hours as the platform lists them."},"linkOnly":{"type":"boolean","description":"OpenTable: only the page's address is kept (its robots.txt allows no fetching)."},"publishedAt":{"type":["string","null"]},"fetchedAt":{"type":"string","format":"date-time"},"stale":{"type":"boolean","description":"Older than the platform lets us keep it (Google 30 days, Yelp 24 hours): a refetch has been queued."},"items":{"type":"array","items":{"$ref":"#/components/schemas/ReviewItem"}}},"required":["id","source","url","title","matchStatus","confidence","matchReason","manual","rating","ratingScale","reviewCount","priceLevel","summary","dishes","skip","hours","linkOnly","publishedAt","fetchedAt","stale","items"]},"RestaurantReviews":{"type":"object","properties":{"restaurantId":{"type":"string"},"name":{"type":"string"},"sources":{"type":"array","items":{"type":"object","properties":{"source":{"type":"string","enum":["google","yelp","opentable","infatuation","hungry_onion","article"]},"name":{"type":"string","description":"The source's name to show: Google 地图, Yelp, OpenTable, The Infatuation, Hungry Onion, 文章."},"status":{"type":"string","enum":["pending","running","ok","not_found","not_applicable","no_key","blocked","error"],"description":"`ok`, `not_found` (looked, nothing there), `not_applicable` (the source does not cover it: no English name, a city it does not write about), `no_key` (the source needs an API key not set yet), `blocked` (the site refused, or its robots.txt), `error`, `pending` / `running` (being fetched)."},"note":{"type":["string","null"],"description":"Why, for anything but `ok`."},"checkedAt":{"type":["string","null"]},"nextCheckAt":{"type":["string","null"]},"listings":{"type":"array","items":{"$ref":"#/components/schemas/ReviewListing"}}},"required":["source","name","status","note","checkedAt","nextCheckAt","listings"]}}},"required":["restaurantId","name","sources"],"description":"Every source's listings for one restaurant."},"RestaurantReviewsOfSource":{"type":"object","properties":{"restaurantId":{"type":"string"},"name":{"type":"string"},"sources":{"type":"array","items":{"type":"object","properties":{"source":{"type":"string","enum":["google","yelp","opentable","infatuation","hungry_onion","article"]},"name":{"type":"string","description":"The source's name to show: Google 地图, Yelp, OpenTable, The Infatuation, Hungry Onion, 文章."},"status":{"type":"string","enum":["pending","running","ok","not_found","not_applicable","no_key","blocked","error"],"description":"`ok`, `not_found` (looked, nothing there), `not_applicable` (the source does not cover it: no English name, a city it does not write about), `no_key` (the source needs an API key not set yet), `blocked` (the site refused, or its robots.txt), `error`, `pending` / `running` (being fetched)."},"note":{"type":["string","null"],"description":"Why, for anything but `ok`."},"checkedAt":{"type":["string","null"]},"nextCheckAt":{"type":["string","null"]},"listings":{"type":"array","items":{"$ref":"#/components/schemas/ReviewListingWithItems"}}},"required":["source","name","status","note","checkedAt","nextCheckAt","listings"]}}},"required":["restaurantId","name","sources"],"description":"One source's listings, each with its review items."},"ReviewPatch":{"anyOf":[{"type":"object","properties":{"source":{"type":"string","enum":["google","yelp","opentable","infatuation","hungry_onion","article"]},"url":{"description":"The page on that source: an Infatuation review, a Hungry Onion topic, an OpenTable page, an article.","type":"string","maxLength":1000,"format":"uri"},"externalId":{"description":"Google place id or Yelp business id.","type":"string","minLength":1,"maxLength":300}},"required":["source"],"additionalProperties":false},{"type":"object","properties":{"listingId":{"type":"string","minLength":1,"maxLength":80},"status":{"type":"string","enum":["confirmed","rejected"],"description":"`confirmed` 是它; `rejected` 不是它 (never shown again, not even after a refetch)."}},"required":["listingId","status"],"additionalProperties":false}],"description":"Pin a listing by its page or id (fetched within a minute and shown as 已确认), or settle a 待确认 one."},"ReviewRefresh":{"type":"object","properties":{"source":{"description":"Only this source; default every source.","type":"string","enum":["google","yelp","opentable","infatuation","hungry_onion","article"]}},"additionalProperties":false},"Book":{"type":"object","properties":{"id":{"type":"string","description":"WeRead's bookId (an album id for an audiobook)."},"kind":{"type":"string","enum":["book","album"],"description":"`book` (e-book, imported book, 公众号) or `album` (有声书; no notes or progress)."},"title":{"type":"string"},"author":{"type":["string","null"]},"translator":{"type":["string","null"]},"cover":{"type":["string","null"],"format":"uri","description":"Cover image on WeRead's CDN."},"category":{"type":["string","null"]},"status":{"type":"string","enum":["reading","finished","unread"],"description":"`reading` (在读: opened, not finished), `finished` (读完: marked finished on WeRead or at 100%), `unread` (on the shelf, never opened)."},"progress":{"type":["integer","null"],"description":"Percent read, 0-100 (1 means 1%)."},"readingSeconds":{"type":["integer","null"],"description":"Time spent in this book, seconds."},"lastReadAt":{"type":["string","null"],"format":"date-time"},"finishedAt":{"type":["string","null"],"format":"date-time"},"highlightCount":{"type":"integer","description":"划线 WeRead counts on the book."},"thoughtCount":{"type":"integer","description":"想法 and reviews WeRead counts on the book."},"bookmarkCount":{"type":"integer","description":"书签 (only counted; their places are not exported)."},"notesAt":{"type":["string","null"],"format":"date-time","description":"When the newest note was made."},"private":{"type":"boolean","description":"私密阅读 on WeRead. Left out unless `includePrivate=true`."},"top":{"type":"boolean"},"onShelf":{"type":"boolean","description":"Still on the shelf (false: taken off, kept for its notes)."},"rating":{"type":["integer","null"],"description":"WeRead's reader rating, per mille (890 = 89% 推荐)."},"readerLink":{"type":["string","null"],"format":"uri","description":"The book in WeRead's web reader (weread.qq.com). Use as is; never build one."},"deepLink":{"type":["string","null"],"format":"uri","description":"WeRead's book page."},"appLink":{"type":["string","null"],"format":"uri","description":"Opens the book in the WeRead app on a phone (`weread://`, at the last reading position); does nothing where the app is not installed, so give `readerLink` on a computer. Null for audiobooks."}},"required":["id","kind","title","author","translator","cover","category","status","progress","readingSeconds","lastReadAt","finishedAt","highlightCount","thoughtCount","bookmarkCount","notesAt","private","top","onShelf","rating","readerLink","deepLink","appLink"]},"BookThought":{"type":"object","properties":{"id":{"type":"string"},"kind":{"type":"string","enum":["highlight","chapter","book"]},"content":{"type":"string"},"abstract":{"type":["string","null"],"description":"The passage it is about, for `highlight`."},"chapterTitle":{"type":["string","null"]},"star":{"type":["integer","null"],"description":"A review's rating, when given."},"createdAt":{"type":["string","null"],"format":"date-time"}},"required":["id","kind","content","abstract","chapterTitle","star","createdAt"],"description":"A 想法: on a passage (`highlight`), a chapter note (`chapter`) or a review of the whole book (`book`)."},"BookHighlight":{"type":"object","properties":{"id":{"type":"string"},"text":{"type":"string","description":"The highlighted passage (划线)."},"createdAt":{"type":["string","null"],"format":"date-time"},"thoughts":{"type":"array","items":{"$ref":"#/components/schemas/BookThought"},"description":"Thoughts written on exactly this passage."}},"required":["id","text","createdAt","thoughts"]},"BookDetail":{"type":"object","properties":{"id":{"type":"string","description":"WeRead's bookId (an album id for an audiobook)."},"kind":{"type":"string","enum":["book","album"],"description":"`book` (e-book, imported book, 公众号) or `album` (有声书; no notes or progress)."},"title":{"type":"string"},"author":{"type":["string","null"]},"translator":{"type":["string","null"]},"cover":{"type":["string","null"],"format":"uri","description":"Cover image on WeRead's CDN."},"category":{"type":["string","null"]},"status":{"type":"string","enum":["reading","finished","unread"],"description":"`reading` (在读: opened, not finished), `finished` (读完: marked finished on WeRead or at 100%), `unread` (on the shelf, never opened)."},"progress":{"type":["integer","null"],"description":"Percent read, 0-100 (1 means 1%)."},"readingSeconds":{"type":["integer","null"],"description":"Time spent in this book, seconds."},"lastReadAt":{"type":["string","null"],"format":"date-time"},"finishedAt":{"type":["string","null"],"format":"date-time"},"highlightCount":{"type":"integer","description":"划线 WeRead counts on the book."},"thoughtCount":{"type":"integer","description":"想法 and reviews WeRead counts on the book."},"bookmarkCount":{"type":"integer","description":"书签 (only counted; their places are not exported)."},"notesAt":{"type":["string","null"],"format":"date-time","description":"When the newest note was made."},"private":{"type":"boolean","description":"私密阅读 on WeRead. Left out unless `includePrivate=true`."},"top":{"type":"boolean"},"onShelf":{"type":"boolean","description":"Still on the shelf (false: taken off, kept for its notes)."},"rating":{"type":["integer","null"],"description":"WeRead's reader rating, per mille (890 = 89% 推荐)."},"readerLink":{"type":["string","null"],"format":"uri","description":"The book in WeRead's web reader (weread.qq.com). Use as is; never build one."},"deepLink":{"type":["string","null"],"format":"uri","description":"WeRead's book page."},"appLink":{"type":["string","null"],"format":"uri","description":"Opens the book in the WeRead app on a phone (`weread://`, at the last reading position); does nothing where the app is not installed, so give `readerLink` on a computer. Null for audiobooks."},"intro":{"type":["string","null"]},"publisher":{"type":["string","null"]},"publishTime":{"type":["string","null"]},"isbn":{"type":["string","null"]},"wordCount":{"type":["integer","null"]},"ratingCount":{"type":["integer","null"]},"chapters":{"type":"array","items":{"type":"object","properties":{"chapterUid":{"type":["integer","null"]},"chapterIdx":{"type":["integer","null"]},"title":{"type":["string","null"]},"highlights":{"type":"array","items":{"$ref":"#/components/schemas/BookHighlight"}},"thoughts":{"type":"array","items":{"$ref":"#/components/schemas/BookThought"},"description":"Chapter notes, and thoughts on passages that are not highlighted."}},"required":["chapterUid","chapterIdx","title","highlights","thoughts"]},"description":"In book order; only chapters with notes."},"reviews":{"type":"array","items":{"$ref":"#/components/schemas/BookThought"},"description":"Reviews of the whole book (书评)."}},"required":["id","kind","title","author","translator","cover","category","status","progress","readingSeconds","lastReadAt","finishedAt","highlightCount","thoughtCount","bookmarkCount","notesAt","private","top","onShelf","rating","readerLink","deepLink","appLink","intro","publisher","publishTime","isbn","wordCount","ratingCount","chapters","reviews"],"description":"A book with its highlights by chapter, thoughts under the passage they are about."},"BookNote":{"type":"object","properties":{"type":{"type":"string","enum":["highlight","thought"]},"id":{"type":"string"},"book":{"type":["object","null"],"properties":{"id":{"type":"string"},"title":{"type":"string"},"author":{"type":["string","null"]},"cover":{"type":["string","null"],"format":"uri"},"readerLink":{"type":["string","null"],"format":"uri"},"appLink":{"type":["string","null"],"format":"uri"}},"required":["id","title","author","cover","readerLink","appLink"]},"chapterTitle":{"type":["string","null"]},"text":{"type":["string","null"]},"createdAt":{"type":["string","null"],"format":"date-time"},"thoughts":{"type":"array","items":{"$ref":"#/components/schemas/BookThought"}}},"required":["type","id","book","chapterTitle","text","createdAt","thoughts"],"description":"One search hit: a highlight with its thoughts, or (`thought`) a thought on a passage that is not highlighted, the passage as `text`."},"BookSync":{"type":"object","properties":{"configured":{"type":"boolean","description":"A WeRead key is set (jiajun.fyi 管理 → 设置 → 微信读书)."},"keyStatus":{"type":"string","enum":["unknown","valid","invalid"],"description":"`invalid`: WeRead refused the key; nothing syncs until the owner pastes a new one."},"lastSuccessAt":{"type":["string","null"],"format":"date-time"},"lastError":{"type":["string","null"]},"pending":{"type":"integer","description":"Books whose highlights, thoughts or progress are still being fetched."},"running":{"type":"boolean"}},"required":["configured","keyStatus","lastSuccessAt","lastError","pending","running"],"description":"How the WeRead sync stands."},"ReadingStats":{"type":"object","properties":{"range":{"type":"string","enum":["week","month","year","total"]},"periodStart":{"type":["string","null"],"format":"date-time","description":"Start of this week, month or year (Beijing midnight); null for total."},"totalSeconds":{"type":"integer"},"readDays":{"type":"integer","description":"Days with at least a minute of reading."},"dayAverageSeconds":{"type":"integer","description":"Per calendar day so far, not per reading day."},"compare":{"type":["number","null"],"description":"Change of the day average against the period before (0.2 = +20%)."},"buckets":{"type":"array","items":{"type":"object","properties":{"start":{"type":"string","format":"date-time"},"seconds":{"type":"integer"}},"required":["start","seconds"]},"description":"WeRead's own buckets: days (week, month), months (year), years (total)."},"days":{"type":"array","items":{"type":"object","properties":{"date":{"type":"string"},"seconds":{"type":"integer"}},"required":["date","seconds"]},"description":"Seconds per day in this period, `date` a Beijing (UTC+8) calendar day as WeRead counts it. Empty for total."},"longest":{"type":"array","items":{"type":"object","properties":{"bookId":{"type":["string","null"]},"title":{"type":"string"},"author":{"type":["string","null"]},"cover":{"type":["string","null"]},"seconds":{"type":"integer"}},"required":["bookId","title","author","cover","seconds"]}},"summary":{"type":"array","items":{"type":"object","properties":{"stat":{"type":"string"},"counts":{"type":"string"}},"required":["stat","counts"]},"description":"WeRead's summary lines, e.g. 读过 8本, 笔记 17条."},"preferCategory":{"type":["string","null"]},"fetchedAt":{"type":["string","null"],"format":"date-time","description":"When WeRead was last asked (week and month hourly, year and total daily)."}},"required":["range","periodStart","totalSeconds","readDays","dayAverageSeconds","compare","buckets","days","longest","summary","preferCategory","fetchedAt"],"description":"阅读时长 as WeRead counts it."},"FeedbackInput":{"type":"object","properties":{"kind":{"type":"string","enum":["bug","missing","idea","note"],"description":"`bug` (something answered wrong or failed), `missing` (a capability or scope you needed and did not have), `idea`, `note`."},"title":{"type":"string","minLength":1,"maxLength":200,"description":"One line: what went wrong or is missing."},"message":{"type":"string","minLength":1,"maxLength":8000,"description":"What you tried, what you expected and what happened."},"request":{"description":"The call that went wrong, as you saw it.","type":"object","properties":{"method":{"type":"string","maxLength":10},"path":{"description":"Path and query you called, e.g. `/items?source=x`.","type":"string","maxLength":500},"status":{"type":"integer","minimum":0,"maximum":999},"error":{"description":"The `error` code (and message) you got back.","type":"string","maxLength":2000}},"additionalProperties":false},"context":{"type":"object","properties":{"task":{"description":"What the user asked you to do.","type":"string","maxLength":2000},"agent":{"description":"Your model or client, e.g. `Claude Code / claude-opus`.","type":"string","maxLength":200},"session":{"description":"Your session or conversation id, if you have one.","type":"string","maxLength":200}},"additionalProperties":false},"logs":{"description":"What you saw, as text (at most 32 KB): tool output, stack traces, the responses. Never include secrets or API keys.","type":"string"}},"required":["kind","title","message"],"additionalProperties":false},"FeedbackPatch":{"type":"object","properties":{"status":{"description":"`resolved` (fixed), `wont_fix`, or `open` again. On a duplicate, any of them undoes the duplicate.","type":"string","enum":["open","resolved","wont_fix"]},"resolution":{"type":["string","null"],"maxLength":4000,"description":"What was done, or why not; `null` clears it."},"kind":{"description":"Re-file it under another kind.","type":"string","enum":["bug","missing","idea","note"]},"title":{"type":"string","minLength":1,"maxLength":200},"priority":{"type":["string","null"],"enum":["p0","p1","p2",null],"description":"`p0` (now), `p1`, `p2`; `null` clears it."},"tags":{"description":"Replaces the whole tag list, e.g. `[\"search\", \"scopes\"]`.","maxItems":20,"type":"array","items":{"type":"string","minLength":1,"maxLength":40}},"duplicateOf":{"description":"Another report's id or `FB-<number>`: this one is the same problem. Its status becomes `duplicate` and its occurrences are added to that report's.","type":"string","minLength":1,"maxLength":200}},"additionalProperties":false,"description":"Only the fields sent change."},"Feedback":{"type":"object","properties":{"id":{"type":"string","description":"The report's id; keeps working everywhere."},"number":{"type":"integer","description":"The report's number in this library, counted from 1 in the order they were filed."},"ref":{"type":"string","description":"`FB-<number>`, e.g. `FB-12`: how to name the report to people. GET / PATCH /feedback/{id} take it too."},"slug":{"type":"string","description":"The title as a short kebab, to tell reports apart at a glance (cosmetic; may change with the title)."},"kind":{"type":"string","enum":["bug","missing","idea","note"]},"title":{"type":"string"},"message":{"type":"string"},"status":{"type":"string","enum":["open","resolved","wont_fix","duplicate"],"description":"`open`, `resolved`, `wont_fix`, or `duplicate` (folded into `duplicateOf`)."},"resolution":{"type":["string","null"],"description":"What was done about it, or why not."},"priority":{"type":["string","null"],"enum":["p0","p1","p2",null],"description":"`p0` (now), `p1`, `p2`, set by the owner."},"tags":{"type":"array","items":{"type":"string"},"description":"The owner's tags."},"duplicateOf":{"type":["string","null"],"description":"The report this one is a duplicate of."},"key":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"}},"required":["id","name"],"description":"The API key that sent it: which agent."},"request":{"type":["object","null"],"propertyNames":{"type":"string"},"additionalProperties":{},"description":"The call that went wrong, as the agent sent it."},"context":{"type":["object","null"],"propertyNames":{"type":"string"},"additionalProperties":{},"description":"`task`, `agent`, `session`, as the agent sent them."},"userAgent":{"type":["string","null"]},"occurrences":{"type":"integer","description":"How many times this key sent it while it was open (repeats within 24 hours are folded in)."},"createdAt":{"type":"string","format":"date-time"},"lastSeenAt":{"type":"string","format":"date-time","description":"The latest occurrence."},"resolvedAt":{"type":["string","null"],"format":"date-time"}},"required":["id","number","ref","slug","kind","title","message","status","resolution","priority","tags","duplicateOf","key","request","context","userAgent","occurrences","createdAt","lastSeenAt","resolvedAt"],"description":"A report an agent sent through POST /feedback."},"FeedbackDetail":{"type":"object","properties":{"id":{"type":"string","description":"The report's id; keeps working everywhere."},"number":{"type":"integer","description":"The report's number in this library, counted from 1 in the order they were filed."},"ref":{"type":"string","description":"`FB-<number>`, e.g. `FB-12`: how to name the report to people. GET / PATCH /feedback/{id} take it too."},"slug":{"type":"string","description":"The title as a short kebab, to tell reports apart at a glance (cosmetic; may change with the title)."},"kind":{"type":"string","enum":["bug","missing","idea","note"]},"title":{"type":"string"},"message":{"type":"string"},"status":{"type":"string","enum":["open","resolved","wont_fix","duplicate"],"description":"`open`, `resolved`, `wont_fix`, or `duplicate` (folded into `duplicateOf`)."},"resolution":{"type":["string","null"],"description":"What was done about it, or why not."},"priority":{"type":["string","null"],"enum":["p0","p1","p2",null],"description":"`p0` (now), `p1`, `p2`, set by the owner."},"tags":{"type":"array","items":{"type":"string"},"description":"The owner's tags."},"duplicateOf":{"type":["string","null"],"description":"The report this one is a duplicate of."},"key":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"}},"required":["id","name"],"description":"The API key that sent it: which agent."},"request":{"type":["object","null"],"propertyNames":{"type":"string"},"additionalProperties":{},"description":"The call that went wrong, as the agent sent it."},"context":{"type":["object","null"],"propertyNames":{"type":"string"},"additionalProperties":{},"description":"`task`, `agent`, `session`, as the agent sent them."},"userAgent":{"type":["string","null"]},"occurrences":{"type":"integer","description":"How many times this key sent it while it was open (repeats within 24 hours are folded in)."},"createdAt":{"type":"string","format":"date-time"},"lastSeenAt":{"type":"string","format":"date-time","description":"The latest occurrence."},"resolvedAt":{"type":["string","null"],"format":"date-time"},"logs":{"type":["string","null"]},"calls":{"type":"array","items":{"type":"object","properties":{"method":{"type":"string"},"path":{"type":"string"},"status":{"type":"integer"},"at":{"type":"string","format":"date-time"}},"required":["method","path","status","at"]},"description":"The key's latest calls (up to 30, newest first) when it was sent, attached by the server."}},"required":["id","number","ref","slug","kind","title","message","status","resolution","priority","tags","duplicateOf","key","request","context","userAgent","occurrences","createdAt","lastSeenAt","resolvedAt","logs","calls"],"description":"A report with the agent's logs and the key's calls before it."},"FeedbackFiled":{"type":"object","properties":{"id":{"type":"string","description":"The report's id."},"number":{"type":"integer","description":"The report's number in this library, counted from 1 in the order they were filed."},"ref":{"type":"string","description":"`FB-<number>`, e.g. `FB-12`: how to name the report to people. GET / PATCH /feedback/{id} take it too."},"slug":{"type":"string","description":"The title as a short kebab, to tell reports apart at a glance (cosmetic; may change with the title)."},"status":{"type":"string","enum":["open","resolved","wont_fix","duplicate"]},"deduplicated":{"type":"boolean","description":"`true`: you already sent this (same kind and title, still open); that report was updated instead."}},"required":["id","number","ref","slug","status","deduplicated"]},"Person":{"type":"object","properties":{"id":{"type":"string","description":"Short public id."},"name":{"type":"string","description":"The name they are best known by: Chinese for Chinese people, their own Latin name otherwise."},"aliases":{"type":"array","items":{"type":"string"},"description":"Other full names: translations, English names, full or short forms."},"description":{"type":["string","null"],"description":"Who they are, one line."},"link":{"type":["string","null"],"format":"uri","description":"A page about them, when someone added one."},"avatarUrl":{"type":["string","null"],"format":"uri"},"addedBy":{"type":"string","enum":["auto","manual"],"description":"`auto` (人物识别 found them in an item) or `manual`."},"itemCount":{"type":"integer","description":"Items about them (links someone removed do not count)."},"roles":{"type":"object","properties":{"interviewee":{"type":"integer"},"host":{"type":"integer"},"author":{"type":"integer"},"subject":{"type":"integer"}},"required":["interviewee","host","author","subject"],"description":"How many of those items in each role."},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"}},"required":["id","name","aliases","description","link","avatarUrl","addedBy","itemCount","roles","createdAt","updatedAt"],"description":"Someone worth remembering from the library: interviewed, hosting, writing their own piece, or written about."},"PersonItem":{"type":"object","properties":{"id":{"type":"string","description":"The item's id (use with `/items/{id}`)."},"title":{"type":["string","null"]},"url":{"type":"string","format":"uri","description":"The item on the hub."},"originalUrl":{"type":["string","null"],"format":"uri","description":"The post, video or article itself."},"source":{"type":"string"},"author":{"type":["string","null"],"description":"Who posted it (often a channel, not the person)."},"publishedAt":{"type":["string","null"],"format":"date-time"},"savedAt":{"type":["string","null"],"format":"date-time"},"role":{"type":"string","enum":["interviewee","host","author","subject"],"description":"`interviewee` 受访, `host` 主持, `author` 作者 (their own piece), `subject` 主题 (about them)."},"evidence":{"type":["string","null"],"description":"The line that shows it."},"origin":{"type":"string","enum":["auto","manual"],"description":"`auto` (人物识别) or `manual`."}},"required":["id","title","url","originalUrl","source","author","publishedAt","savedAt","role","evidence","origin"],"description":"An item about the person, and their role in it."},"PersonDetail":{"type":"object","properties":{"id":{"type":"string","description":"Short public id."},"name":{"type":"string","description":"The name they are best known by: Chinese for Chinese people, their own Latin name otherwise."},"aliases":{"type":"array","items":{"type":"string"},"description":"Other full names: translations, English names, full or short forms."},"description":{"type":["string","null"],"description":"Who they are, one line."},"link":{"type":["string","null"],"format":"uri","description":"A page about them, when someone added one."},"avatarUrl":{"type":["string","null"],"format":"uri"},"addedBy":{"type":"string","enum":["auto","manual"],"description":"`auto` (人物识别 found them in an item) or `manual`."},"itemCount":{"type":"integer","description":"Items about them (links someone removed do not count)."},"roles":{"type":"object","properties":{"interviewee":{"type":"integer"},"host":{"type":"integer"},"author":{"type":"integer"},"subject":{"type":"integer"}},"required":["interviewee","host","author","subject"],"description":"How many of those items in each role."},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"},"items":{"type":"array","items":{"$ref":"#/components/schemas/PersonItem"}}},"required":["id","name","aliases","description","link","avatarUrl","addedBy","itemCount","roles","createdAt","updatedAt","items"],"description":"A person with their items, newest first."},"PersonInput":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":80,"description":"The name they are best known by: Chinese for Chinese people, their own Latin name otherwise (`Rick Rubin`)."},"aliases":{"description":"Other full names: a translation, an English name, a full or short form (`鲁豫` for 陈鲁豫, `里克·鲁宾`). Not a first or last name alone.","maxItems":30,"type":"array","items":{"type":"string","minLength":1,"maxLength":80}},"description":{"type":["string","null"],"maxLength":120,"description":"Who they are, one short line: `苹果硬件技术高级副总裁`."},"link":{"type":["string","null"],"maxLength":1000,"format":"uri","description":"A page about them (their site, Wikipedia)."},"items":{"description":"Items to link right away.","maxItems":50,"type":"array","items":{"type":"object","properties":{"itemId":{"type":"string","minLength":1,"maxLength":80,"description":"An item id from `/search` or `/items`."},"role":{"description":"`interviewee` (受访: the guest of an interview, podcast or talk), `host` (主持), `author` (作者: their own essay, post or talk), `subject` (主题: the item is about them); the Chinese words work too.","type":"string","enum":["interviewee","host","author","subject","受访","嘉宾","主持","作者","主题"]},"evidence":{"type":["string","null"],"maxLength":200}},"required":["itemId","role"]}}},"required":["name"],"additionalProperties":false},"PersonPatch":{"type":"object","properties":{"name":{"description":"A new display name; the old one stays as an alias.","type":"string","minLength":1,"maxLength":80},"aliases":{"description":"Replaces the whole alias list.","maxItems":30,"type":"array","items":{"type":"string","minLength":1,"maxLength":80}},"description":{"type":["string","null"],"maxLength":120},"link":{"type":["string","null"],"maxLength":1000,"format":"uri"}},"additionalProperties":false,"description":"Only the fields sent change; `null` clears a text field."},"PersonMerge":{"type":"object","properties":{"personId":{"type":"string","minLength":1,"maxLength":40,"description":"The other person (the same human recorded twice); its items and names move here and it is removed."}},"required":["personId"],"additionalProperties":false},"PersonItemInput":{"type":"object","properties":{"itemId":{"type":"string","minLength":1,"maxLength":80,"description":"An item id from `/search` or `/items`."},"role":{"description":"`interviewee` (受访: the guest of an interview, podcast or talk), `host` (主持), `author` (作者: their own essay, post or talk), `subject` (主题: the item is about them); the Chinese words work too.","type":"string","enum":["interviewee","host","author","subject","受访","嘉宾","主持","作者","主题"]},"evidence":{"type":["string","null"],"maxLength":200,"description":"Why: a line from the item."}},"required":["itemId","role"],"additionalProperties":false},"PersonItemPatch":{"type":"object","properties":{"role":{"description":"`interviewee` (受访: the guest of an interview, podcast or talk), `host` (主持), `author` (作者: their own essay, post or talk), `subject` (主题: the item is about them); the Chinese words work too.","type":"string","enum":["interviewee","host","author","subject","受访","嘉宾","主持","作者","主题"]},"removed":{"description":"`true`: not about this person; 人物识别 will not link it again. `false` puts it back.","type":"boolean"}},"additionalProperties":false},"RecipeSourceItem":{"type":"object","properties":{"id":{"type":"string","description":"The item's id (use with `/items/{id}`)."},"title":{"type":["string","null"]},"source":{"type":"string","description":"Source code, e.g. `xiaohongshu`, `youtube`, `apple_notes`."},"author":{"type":["string","null"],"description":"Who posted it."},"url":{"type":["string","null"],"format":"uri","description":"The post or video itself."},"hubUrl":{"type":"string","format":"uri","description":"The item on the hub."}},"required":["id","title","source","author","url","hubUrl"],"description":"The library item a recipe was worked out from."},"RecipeProducer":{"type":"object","properties":{"kind":{"type":"string","enum":["internal","external"],"description":"`internal`: PIH's own pipeline (after collect, or POST /recipes/import); `external`: an agent wrote it with its API key (POST /recipes)."},"name":{"type":"string","description":"`PIH`, or the name of the agent's API key as the owner named it."},"keyId":{"type":["string","null"],"description":"The API key's id for an external version: compare with your own to find yours."},"model":{"type":["string","null"],"description":"The model PIH used, for an internal version."},"session":{"type":["string","null"],"description":"What the agent said about where it came from, for an external version."}},"required":["kind","name","keyId","model","session"],"description":"Who made this version of the recipe (出品方)."},"Recipe":{"type":"object","properties":{"id":{"type":"string","description":"菜谱短 ID of this version. A video that cooks five dishes has five dishes; a dish with several versions has one id per version."},"title":{"type":"string","description":"The dish name."},"part":{"type":["string","null"],"description":"Which stretch of the item this dish is (做法段, e.g. `seg:13-136`, seconds into the video); null when the item is one dish."},"tags":{"type":"array","items":{"type":"string"}},"servings":{"type":["string","null"],"description":"As written, e.g. `2人份`."},"totalTimeMin":{"type":["number","null"],"description":"Minutes, when the source says."},"notes":{"type":["string","null"],"description":"Substitutions and tips."},"want":{"type":"boolean","description":"The owner marked it 想做 (want to cook)."},"cookedCount":{"type":"integer","description":"Times the owner marked it 做过 (cooked)."},"lastCookedAt":{"type":["string","null"],"format":"date-time","description":"The latest 做过."},"item":{"$ref":"#/components/schemas/RecipeSourceItem"},"recipeUrl":{"type":"string","format":"uri","description":"The dish on recipes.jiajun.fyi."},"createdAt":{"type":"string","format":"date-time","description":"When this version of the recipe was worked out."},"producer":{"$ref":"#/components/schemas/RecipeProducer"},"shown":{"type":"boolean","description":"This is the version the dish shows (the owner's pick, else PIH's, else the newest external one). Lists hold only shown versions."},"versions":{"type":"integer","description":"How many current versions the dish has; more than 1: list them with GET /recipes/{id}/versions."},"ingredientList":{"type":"array","items":{"type":"string"},"description":"Ingredients as `name amount` lines; group and structure are in GET /recipes/{id}."}},"required":["id","title","part","tags","servings","totalTimeMin","notes","want","cookedCount","lastCookedAt","item","recipeUrl","createdAt","producer","shown","versions","ingredientList"],"description":"A dish from the library, with the owner's marks."},"RecipeDetail":{"type":"object","properties":{"id":{"type":"string","description":"菜谱短 ID of this version. A video that cooks five dishes has five dishes; a dish with several versions has one id per version."},"title":{"type":"string","description":"The dish name."},"part":{"type":["string","null"],"description":"Which stretch of the item this dish is (做法段, e.g. `seg:13-136`, seconds into the video); null when the item is one dish."},"tags":{"type":"array","items":{"type":"string"}},"servings":{"type":["string","null"],"description":"As written, e.g. `2人份`."},"totalTimeMin":{"type":["number","null"],"description":"Minutes, when the source says."},"notes":{"type":["string","null"],"description":"Substitutions and tips."},"want":{"type":"boolean","description":"The owner marked it 想做 (want to cook)."},"cookedCount":{"type":"integer","description":"Times the owner marked it 做过 (cooked)."},"lastCookedAt":{"type":["string","null"],"format":"date-time","description":"The latest 做过."},"item":{"$ref":"#/components/schemas/RecipeSourceItem"},"recipeUrl":{"type":"string","format":"uri","description":"The dish on recipes.jiajun.fyi."},"createdAt":{"type":"string","format":"date-time","description":"When this version of the recipe was worked out."},"producer":{"$ref":"#/components/schemas/RecipeProducer"},"shown":{"type":"boolean","description":"This is the version the dish shows (the owner's pick, else PIH's, else the newest external one). Lists hold only shown versions."},"versions":{"type":"integer","description":"How many current versions the dish has; more than 1: list them with GET /recipes/{id}/versions."},"ingredients":{"type":"array","items":{"type":"object","properties":{"name":{"type":"string"},"amount":{"type":["string","null"],"description":"As written, e.g. `2个`, `适量`."},"group":{"type":["string","null"],"description":"`主料` main, `调料` seasoning, `腌料` marinade, `装饰` garnish."}},"required":["name","amount","group"]}},"steps":{"type":"array","items":{"type":"object","properties":{"order":{"type":"integer"},"text":{"type":"string"},"startS":{"type":["number","null"],"description":"Seconds into the video where this step is shown."},"durationMin":{"type":["number","null"],"description":"How long the step takes, only for slow steps."}},"required":["order","text","startS","durationMin"]}},"startS":{"type":["number","null"],"description":"Where this dish starts in the video, seconds."},"endS":{"type":["number","null"],"description":"Where it ends."},"confidence":{"type":["number","null"],"description":"The model's confidence in the extraction, 0-1."},"needsReview":{"type":"boolean","description":"Confidence under 0.5: shown as 待确认; check against the source."},"cookLog":{"type":"array","items":{"type":"object","properties":{"at":{"type":"string","format":"date-time"},"note":{"type":["string","null"]}},"required":["at","note"]},"description":"Every 做过, newest first, with the owner's note."},"plans":{"type":"array","items":{"type":"object","properties":{"date":{"type":"string","description":"YYYY-MM-DD (Pacific)."},"meal":{"type":"string","enum":["breakfast","lunch","dinner"]}},"required":["date","meal"]},"description":"排餐: the days it is planned for, from today on."}},"required":["id","title","part","tags","servings","totalTimeMin","notes","want","cookedCount","lastCookedAt","item","recipeUrl","createdAt","producer","shown","versions","ingredients","steps","startS","endS","confidence","needsReview","cookLog","plans"],"description":"A dish with its ingredients, steps and cook log."},"RecipeVersionList":{"type":"object","properties":{"items":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","description":"菜谱短 ID of this version. A video that cooks five dishes has five dishes; a dish with several versions has one id per version."},"title":{"type":"string","description":"The dish name."},"part":{"type":["string","null"],"description":"Which stretch of the item this dish is (做法段, e.g. `seg:13-136`, seconds into the video); null when the item is one dish."},"tags":{"type":"array","items":{"type":"string"}},"servings":{"type":["string","null"],"description":"As written, e.g. `2人份`."},"totalTimeMin":{"type":["number","null"],"description":"Minutes, when the source says."},"notes":{"type":["string","null"],"description":"Substitutions and tips."},"want":{"type":"boolean","description":"The owner marked it 想做 (want to cook)."},"cookedCount":{"type":"integer","description":"Times the owner marked it 做过 (cooked)."},"lastCookedAt":{"type":["string","null"],"format":"date-time","description":"The latest 做过."},"item":{"$ref":"#/components/schemas/RecipeSourceItem"},"recipeUrl":{"type":"string","format":"uri","description":"The dish on recipes.jiajun.fyi."},"createdAt":{"type":"string","format":"date-time","description":"When this version of the recipe was worked out."},"producer":{"$ref":"#/components/schemas/RecipeProducer"},"shown":{"type":"boolean","description":"This is the version the dish shows (the owner's pick, else PIH's, else the newest external one). Lists hold only shown versions."},"versions":{"type":"integer","description":"How many current versions the dish has; more than 1: list them with GET /recipes/{id}/versions."},"ingredients":{"type":"array","items":{"type":"object","properties":{"name":{"type":"string"},"amount":{"type":["string","null"],"description":"As written, e.g. `2个`, `适量`."},"group":{"type":["string","null"],"description":"`主料` main, `调料` seasoning, `腌料` marinade, `装饰` garnish."}},"required":["name","amount","group"]}},"steps":{"type":"array","items":{"type":"object","properties":{"order":{"type":"integer"},"text":{"type":"string"},"startS":{"type":["number","null"],"description":"Seconds into the video where this step is shown."},"durationMin":{"type":["number","null"],"description":"How long the step takes, only for slow steps."}},"required":["order","text","startS","durationMin"]}}},"required":["id","title","part","tags","servings","totalTimeMin","notes","want","cookedCount","lastCookedAt","item","recipeUrl","createdAt","producer","shown","versions","ingredients","steps"]}}},"required":["items"],"description":"Every current version of one dish, the shown one first."},"RecipeVersionInput":{"type":"object","properties":{"item":{"type":"string","minLength":1,"maxLength":64,"description":"The library item this recipe is from (its id from `/items`, `/search` or a recipe's `item.id`)."},"part":{"description":"Which dish of a multi-dish video, as a recipe's `part` from GET /recipes (e.g. `seg:13-136`). Or send `startS` + `endS`. Leave out when the item has one dish.","type":"string","pattern":"^seg:(\\d+)-(\\d+)$"},"startS":{"description":"Where this dish starts in the video, seconds (with `endS`, instead of `part`).","type":"number","minimum":0,"maximum":86400},"endS":{"description":"Where it ends.","type":"number","minimum":0,"maximum":86400},"title":{"type":"string","minLength":1,"maxLength":80,"description":"The dish name."},"servings":{"description":"As written, e.g. `2人份`.","type":"string","minLength":1,"maxLength":40},"totalTimeMin":{"description":"Total minutes.","type":"integer","exclusiveMinimum":0,"maximum":10000},"ingredients":{"minItems":1,"maxItems":100,"type":"array","items":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":60,"description":"The ingredient, e.g. `鸡蛋`."},"amount":{"description":"As written, e.g. `3个`, `200g`; default `适量`.","type":"string","minLength":1,"maxLength":40},"group":{"description":"`主料` main (default), `调料` seasoning, `腌料` marinade, `装饰` garnish.","type":"string","enum":["主料","调料","腌料","装饰"]}},"required":["name"],"additionalProperties":false},"description":"Every ingredient with its amount, 1-100."},"steps":{"minItems":1,"maxItems":100,"type":"array","items":{"type":"object","properties":{"text":{"type":"string","minLength":1,"maxLength":400,"description":"What to do in this step."},"startS":{"description":"Seconds into the item's video where this step is shown; must fall inside the dish's stretch when it has one.","type":"number","minimum":0,"maximum":86400},"durationMin":{"description":"How long the step takes, only for slow steps (simmer, marinate, bake).","type":"number","exclusiveMinimum":0,"maximum":10000}},"required":["text"],"additionalProperties":false},"description":"The steps in order, 1-100."},"tags":{"description":"From the fixed vocabulary only (cuisine, meal, style, main ingredient); left out: worked out from the name and ingredients.","maxItems":8,"type":"array","items":{"type":"string","enum":["川菜","粤菜","湘菜","东北菜","西餐","日料","韩料","东南亚","早餐","午餐","晚餐","夜宵","快手菜","家常菜","素菜","甜品","汤品","凉菜","烘焙","减脂餐","下饭菜","便当","鸡肉","牛肉","猪肉","海鲜","蔬菜","豆腐","面食","米饭","蛋"]}},"notes":{"description":"Tips: substitutions, what to watch out for.","type":"string","minLength":1,"maxLength":1000},"session":{"description":"A line about where this came from, e.g. the conversation with the owner; shown with your version.","type":"string","minLength":1,"maxLength":200}},"required":["item","title","ingredients","steps"],"additionalProperties":false},"RecipeVersionWritten":{"type":"object","properties":{"id":{"type":"string","description":"Its 菜谱短 ID (read it with GET /recipes/{id})."},"item":{"type":"string","description":"The item it belongs to."},"part":{"type":["string","null"],"description":"The dish within the item (做法段); null when the item is one dish."},"shown":{"type":"boolean","description":"Whether the dish now shows this version: only when there is no PIH version and no owner's pick for another one, or the owner picked it."},"versions":{"type":"integer","description":"How many current versions the dish has now."},"replaced":{"type":"boolean","description":"Your key's earlier version of this dish was replaced by this one (same id)."},"recipeUrl":{"type":"string","format":"uri","description":"This version on recipes.jiajun.fyi."}},"required":["id","item","part","shown","versions","replaced","recipeUrl"],"description":"The version just written."},"RecipeWant":{"type":"object","properties":{"want":{"type":"boolean","description":"`true` marks 想做, `false` takes it back."}},"required":["want"],"additionalProperties":false},"RecipeCooked":{"type":"object","properties":{"date":{"description":"YYYY-MM-DD the dish was cooked; default today (Pacific time). Not in the future.","type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}$"},"note":{"description":"How it went, what to change next time.","type":"string","maxLength":500}},"additionalProperties":false},"RecipePlan":{"type":"object","properties":{"date":{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}$","description":"YYYY-MM-DD (Pacific)."},"meal":{"description":"Default `dinner`.","type":"string","enum":["breakfast","lunch","dinner"]},"remove":{"description":"`true` takes the dish off that day (that meal only when `meal` is sent).","type":"boolean"}},"required":["date"],"additionalProperties":false},"GroceryInput":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":80,"description":"What to buy."},"qty":{"description":"How much, as written, e.g. `2盒`.","type":"string","maxLength":40},"week":{"description":"Any day of the week the list is for; default this week (Pacific).","type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}$"}},"required":["name"],"additionalProperties":false},"GroceryItem":{"type":"object","properties":{"id":{"type":["integer","null"],"description":"The line's id on the recipes site."},"name":{"type":"string"},"qty":{"type":["string","null"]},"weekStart":{"type":"string","description":"Monday of the list's week, YYYY-MM-DD."}},"required":["id","name","qty","weekStart"],"description":"A hand-added line on a week's grocery list."},"RecipeImport":{"type":"object","properties":{"items":{"minItems":1,"maxItems":20,"type":"array","items":{"type":"string","minLength":1,"maxLength":64},"description":"Item ids (the short id from `/items` or `/search`), 1-20."},"force":{"description":"`true`: work them out again even if recipes exist (e.g. a wrong dish split).","type":"boolean"}},"required":["items"],"additionalProperties":false},"RecipeImportResult":{"type":"object","properties":{"items":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","description":"The item's id (its short id when found, else as sent)."},"status":{"type":"string","enum":["queued","exists","not_found","not_ready"],"description":"`queued`: being worked out now, poll `GET /recipes?item={id}` (a minute or a few for a long video); `exists`: already has PIH's recipes (send `force` to redo; agents' own versions do not count); `not_found`: no such item; `not_ready`: no text, transcript or media to read yet, try again later."},"recipeIds":{"description":"Its current PIH recipes' 菜谱短 IDs, when it has any.","type":"array","items":{"type":"string"}}},"required":["id","status"]}}},"required":["items"],"description":"What happened to each item asked for."},"Todo":{"type":"object","properties":{"id":{"type":"string","description":"Short id."},"title":{"type":"string"},"note":{"type":["string","null"]},"status":{"type":"string","enum":["open","done","dropped"],"description":"`open` (to do), `done`, or `dropped` (given up)."},"priority":{"type":["string","null"],"enum":["p0","p1","p2",null],"description":"`p0` most urgent, `p1`, `p2`; null for none."},"dueDate":{"type":["string","null"],"description":"`YYYY-MM-DD`, the owner's (Pacific) day."},"tags":{"type":"array","items":{"type":"string"}},"area":{"type":"string","enum":["work","life"],"description":"`work` or `life`."},"project":{"type":["string","null"],"description":"The project it belongs to (a free name such as `PIH`); null for none."},"createdBy":{"type":"string","description":"`owner` (added on the hub) or the name of the API key that added it."},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"},"doneAt":{"type":["string","null"],"format":"date-time","description":"When it was marked done or dropped; null while open."},"sortOrder":{"type":"integer","description":"The owner's hand order inside one priority and due date."},"item":{"type":["object","null"],"properties":{"id":{"type":"string","description":"The item's id, as in /items/{id}."},"title":{"type":["string","null"]},"hubUrl":{"type":"string","format":"uri","description":"The item's page on the hub."}},"required":["id","title","hubUrl"],"description":"The library item this todo is about (set with `itemId`); null for none."}},"required":["id","title","note","status","priority","dueDate","tags","area","project","createdBy","createdAt","updatedAt","doneAt","sortOrder","item"],"description":"One 待办 (todo) on the owner's list."},"TodoInput":{"type":"object","properties":{"title":{"type":"string","minLength":1,"maxLength":300,"description":"What to do, one line."},"note":{"type":["string","null"],"maxLength":4000,"description":"Details, links, context. `null` clears it."},"priority":{"type":["string","null"],"enum":["p0","p1","p2",null],"description":"`p0` (most urgent), `p1`, `p2`, or `null` for none."},"dueDate":{"type":["string","null"],"pattern":"^\\d{4}-\\d{2}-\\d{2}$","description":"`YYYY-MM-DD`, the owner's own (Pacific) day; `null` for none."},"tags":{"description":"Free tags, e.g. `视频`, `买东西`. Work or life is `area`, the project is `project`. Replaces the whole list.","maxItems":20,"type":"array","items":{"type":"string","minLength":1,"maxLength":40}},"itemId":{"type":["string","null"],"minLength":1,"maxLength":64,"description":"The library item this todo is about (its id from /items, /search or /collect), e.g. a video to watch; `null` unlinks it."},"area":{"type":"string","enum":["work","life"],"description":"`work` or `life` (the default): which side of the owner's life it belongs to."},"project":{"type":["string","null"],"maxLength":60,"description":"The project it belongs to, a free name such as `PIH` or `图图`; reuse the names already in use (GET /todos shows them). `null` clears it."}},"required":["title"],"additionalProperties":false},"TodoPatch":{"type":"object","properties":{"title":{"type":"string","minLength":1,"maxLength":300},"note":{"type":["string","null"],"maxLength":4000,"description":"Details, links, context. `null` clears it."},"priority":{"type":["string","null"],"enum":["p0","p1","p2",null],"description":"`p0` (most urgent), `p1`, `p2`, or `null` for none."},"dueDate":{"type":["string","null"],"pattern":"^\\d{4}-\\d{2}-\\d{2}$","description":"`YYYY-MM-DD`, the owner's own (Pacific) day; `null` for none."},"tags":{"description":"Free tags, e.g. `视频`, `买东西`. Work or life is `area`, the project is `project`. Replaces the whole list.","maxItems":20,"type":"array","items":{"type":"string","minLength":1,"maxLength":40}},"itemId":{"type":["string","null"],"minLength":1,"maxLength":64,"description":"The library item this todo is about (its id from /items, /search or /collect), e.g. a video to watch; `null` unlinks it."},"area":{"type":"string","enum":["work","life"],"description":"`work` or `life` (the default): which side of the owner's life it belongs to."},"project":{"type":["string","null"],"maxLength":60,"description":"The project it belongs to, a free name such as `PIH` or `图图`; reuse the names already in use (GET /todos shows them). `null` clears it."},"status":{"description":"`done` completes it, `dropped` gives it up, `open` brings it back.","type":"string","enum":["open","done","dropped"]}},"additionalProperties":false,"description":"Only the fields sent change."},"CardNote":{"type":"object","properties":{"id":{"type":"string","description":"Short id."},"title":{"type":"string"},"body":{"type":"string","description":"Markdown; empty when the card is only its title."},"tags":{"type":"array","items":{"type":"string"},"description":"In the order given."},"createdBy":{"type":"string","description":"The name of the API key that wrote it."},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"},"removedAt":{"type":["string","null"],"format":"date-time","description":"When it was taken back (撤销); null while in the box."}},"required":["id","title","body","tags","createdBy","createdAt","updatedAt","removedAt"],"description":"One 卡片笔记 (card note) in the owner's 卡片盒 (card box): a title, a Markdown body and tags."},"CardNoteInput":{"type":"object","properties":{"title":{"type":"string","minLength":1,"maxLength":200,"description":"The card's one-line point."},"body":{"type":"string","maxLength":20000,"description":"The note itself, Markdown. Empty for a card that is only its title."},"tags":{"description":"Short tags, e.g. `读书`, `产品设计`; at most 20, 40 characters each. A leading `#` is dropped, repeats (ignoring case) are kept once. Replaces the whole list.","maxItems":20,"type":"array","items":{"type":"string"}}},"required":["title"],"additionalProperties":false},"CardNotePatch":{"type":"object","properties":{"title":{"type":"string","minLength":1,"maxLength":200,"description":"The card's one-line point."},"body":{"type":"string","maxLength":20000,"description":"The note itself, Markdown. Empty for a card that is only its title."},"tags":{"description":"Short tags, e.g. `读书`, `产品设计`; at most 20, 40 characters each. A leading `#` is dropped, repeats (ignoring case) are kept once. Replaces the whole list.","maxItems":20,"type":"array","items":{"type":"string"}},"removed":{"description":"`true` takes the card back (撤销): it leaves the list and the tag counts, nothing is deleted. `false` brings it back.","type":"boolean"}},"additionalProperties":false,"description":"Only the fields sent change."},"ConnectorStatus":{"type":"object","properties":{"source":{"type":"string","description":"Lowercase source code, as on items: `bilibili`, `youtube`, `xiaohongshu`, `twitter_bookmark`, …"},"health":{"type":"string","enum":["ok","late","failing","needs_attention","paused"],"description":"`ok`: syncing on schedule. `late`: its next run is overdue by more than a cadence and nothing ran (e.g. no Mac online). `failing`: the last run failed (`lastError`). `needs_attention`: only the owner can fix it, e.g. log in again (`attention`). `paused`: switched off by the owner."},"status":{"type":"string","description":"The stored state: `active`, `error`, `auth_required` or `paused`."},"lastSyncAt":{"type":["string","null"],"format":"date-time","description":"The last successful sync; null if it never succeeded."},"lastError":{"type":["string","null"],"description":"Why the last run failed, while it is failing."},"nextSyncAt":{"type":["string","null"],"format":"date-time","description":"When the next run is due."},"cadenceMinutes":{"type":"integer","description":"How often it normally syncs."},"lastRun":{"type":["object","null"],"properties":{"status":{"type":"string","description":"`done`, `failed`, or `queued` / `sent` while running."},"at":{"type":"string","format":"date-time","description":"When it finished (or was queued, if not finished)."},"error":{"type":["string","null"]}},"required":["status","at","error"],"description":"The newest run sent to the owner's Mac (browser syncs); null for sources synced otherwise."},"attention":{"type":["object","null"],"properties":{"reason":{"type":"string","description":"`auth_required`, `verification_required`, …"},"since":{"type":"string","format":"date-time"},"detail":{"type":["string","null"]}},"required":["reason","since","detail"],"description":"An open 需要处理: a problem only the owner can solve; its runs wait until it is cleared."},"lastItemAt":{"type":["string","null"],"format":"date-time","description":"When the newest item from this source was collected (by sync or any other way)."}},"required":["source","health","status","lastSyncAt","lastError","nextSyncAt","cadenceMinutes","lastRun","attention","lastItemAt"],"description":"One 同步源: a source the hub pulls on its own schedule (X bookmarks, Xiaohongshu, YouTube, Bilibili, …)."},"NotifyInput":{"type":"object","properties":{"title":{"type":"string","minLength":1,"maxLength":240,"description":"One line, e.g. `PR #112 is ready for review`."},"text":{"type":"string","minLength":1,"maxLength":4000,"description":"The message, plain text: what is done, links, what is needed next."},"channel":{"description":"Which 通知频道. Default `dev_bot` (the owner's dev bot); allowed: `dev_bot`.","type":"string","minLength":1,"maxLength":64}},"required":["title","text"],"additionalProperties":false},"TikhubCall":{"type":"object","properties":{"path":{"type":"string","pattern":"^\\/api\\/v1(\\/[A-Za-z0-9_-]+){2,}$","description":"The TikHub route as in TikHub's docs (https://api.tikhub.io/docs), e.g. `/api/v1/xiaohongshu/app_v2/get_image_note_detail`. Not `/api/v1/tikhub/...` (TikHub's own account)."},"method":{"default":"GET","description":"The route's HTTP method in TikHub's docs (default `GET`).","type":"string","enum":["GET","POST"]},"query":{"default":{},"description":"Its query parameters, e.g. `{\"note_id\": \"...\"}`.","type":"object","propertyNames":{"type":"string"},"additionalProperties":{"anyOf":[{"type":"string"},{"type":"number"},{"type":"boolean"}]}},"body":{"description":"The JSON body, for a `POST` route."}},"required":["path"],"additionalProperties":false},"NotifySent":{"type":"object","properties":{"status":{"type":"string","const":"sent","description":"Delivered to the channel's transport."},"channel":{"type":"string","description":"The channel it went to."},"messageId":{"type":["string","null"],"description":"The transport's id for it (for the dev bot, the automation run's id)."}},"required":["status","channel","messageId"]},"TutuMedium":{"type":"object","properties":{"id":{"type":"string","description":"The 照片短 ID (8 characters), the same one in tutu.jiajun.fyi/p/<id> links."},"kind":{"type":"string","enum":["image","video"]},"source":{"type":"string","enum":["brightwheel","family_photos"],"description":"`brightwheel` (the daycare's posts) or `family_photos` (家里拍的, from the Photos library)."},"takenAt":{"type":"string","format":"date-time","description":"When it was taken (a Brightwheel photo: when the teacher posted it)."},"day":{"type":"string","description":"The Pacific calendar day it belongs to, `YYYY-MM-DD`."},"width":{"type":["integer","null"],"description":"Pixels, when known (family photos); null for Brightwheel."},"height":{"type":["integer","null"],"description":"Pixels, when known (family photos); null for Brightwheel."},"caption":{"type":["string","null"],"description":"The Chinese 照片描述 (photo caption), in 图图's voice when he can be recognised."},"captionEn":{"type":["string","null"],"description":"The same caption in English."},"note":{"type":["string","null"],"description":"The teacher's note on the Brightwheel post; null for family photos."},"place":{"type":["string","null"],"description":"Where: a place typed by hand, else the city or 家 from Photos; null when unknown."},"favorite":{"type":"boolean","description":"Marked favourite in Photos."},"print":{"type":["object","null"],"properties":{"lastPrintedOn":{"type":["string","null"],"description":"The last day it was printed (冲印), `YYYY-MM-DD`."},"waiting":{"type":["string","null"],"enum":["queued","unconfirmed",null],"description":"`queued` 待冲印, `unconfirmed` 待确认, null when not waiting."}},"required":["lastPrintedOn","waiting"],"description":"Its 冲印 (print) record; null when never printed nor waiting."},"url":{"type":"string","format":"uri","description":"The file (photo or video), a signed link fetched without the key; supports `Range`. Valid until `linksExpireAt`."},"thumbUrl":{"type":["string","null"],"format":"uri","description":"A small JPEG (longest side 640px) when one is stored (family photos, also as a video's poster); null otherwise: use `url`."},"pageUrl":{"type":"string","format":"uri","description":"Its page on tutu.jiajun.fyi (requires the owner's login)."}},"required":["id","kind","source","takenAt","day","width","height","caption","captionEn","note","place","favorite","print","url","thumbUrl","pageUrl"],"description":"One photo or video of 图图 on the growth timeline (成长时间线), from Brightwheel or the family's Photos library."},"TutuDay":{"type":"object","properties":{"day":{"type":"string","description":"The Pacific calendar day, `YYYY-MM-DD`."},"arrivedAt":{"type":["string","null"],"format":"date-time","description":"Checked in at daycare (入园); null when not logged."},"leftAt":{"type":["string","null"],"format":"date-time","description":"Picked up (离园); null while he is still there or when not logged."},"naps":{"type":"array","items":{"type":"object","properties":{"startAt":{"type":"string","format":"date-time","description":"Fell asleep."},"endAt":{"type":["string","null"],"format":"date-time","description":"Woke up; null while he is still asleep, or when the teacher did not log it."},"minutes":{"type":["integer","null"],"description":"How long he slept, in minutes; null without `endAt`."}},"required":["startAt","endAt","minutes"]},"description":"Naps that day, oldest first."}},"required":["day","arrivedAt","leftAt","naps"],"description":"One daycare day of 图图 from Brightwheel: when he arrived (入园), when he was picked up (离园) and his naps (午睡). Naps at home and night sleep are not recorded."},"HomeLog":{"type":"object","properties":{"id":{"type":"string","description":"8-character id."},"clientId":{"type":["string","null"],"description":"The app's own id for the tap, when it sent one."},"kind":{"type":"string","enum":["sleep","wake","meal","poop","moment"],"description":"`sleep` 睡着了, `wake` 起床了, `meal` 吃饭, `poop` 大便, `moment` 只拍照."},"at":{"type":"string","format":"date-time","description":"When it happened."},"day":{"type":"string","description":"The Pacific calendar day of `at`, `YYYY-MM-DD`."},"meal":{"type":["string","null"],"enum":["breakfast","lunch","dinner","snack",null],"description":"Which meal, on kind `meal`; null otherwise or not said."},"eaten":{"type":["string","null"],"enum":["little","half","most","all",null],"description":"吃了多少 (`little`, `half`, `most`, `all`), on kind `meal`; null otherwise or not said."},"poop":{"type":["object","null"],"properties":{"texture":{"type":["string","null"],"enum":["hard","firm","normal","soft","watery",null]},"color":{"type":["string","null"],"enum":["yellow","tan","brown","green","black","red","white",null]},"place":{"type":["string","null"],"enum":["toilet","diaper","accident",null]},"amount":{"type":["string","null"],"enum":["small","medium","large",null]}},"required":["texture","color","place","amount"],"description":"软硬, 颜色, 在哪, 量 on kind `poop` (each null when not said); null on other kinds."},"note":{"type":["string","null"],"description":"The one line written with it."},"photos":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"width":{"type":["integer","null"],"description":"Pixels, as the app sent them."},"height":{"type":["integer","null"],"description":"Pixels, as the app sent them."},"createdAt":{"type":"string","format":"date-time"},"url":{"type":"string","format":"uri","description":"The photo, a signed link fetched without the key, valid until `linksExpireAt` (the same link within a 6-hour window, so it can be cached by URL). A poop photo is stored as taken: blur it until asked."}},"required":["id","width","height","createdAt","url"]},"description":"Photos taken with it, oldest first."},"createdBy":{"type":"string","description":"The API key's name, or `owner` from the web."},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"},"removedAt":{"type":["string","null"],"format":"date-time","description":"When it was taken back (撤销); lists leave such records out."}},"required":["id","clientId","kind","at","day","meal","eaten","poop","note","photos","createdBy","createdAt","updatedAt","removedAt"],"description":"One 家里记录 (home log) of 图图: a tap in the 图图 App when he fell asleep, woke up, ate, pooped, or just a photo, with any photos taken with it."},"HomeLogInput":{"type":"object","properties":{"clientId":{"description":"The app's own id for this tap. Sending the same one again (a retried upload) answers the record already logged instead of logging twice.","type":"string","minLength":1,"maxLength":64},"kind":{"type":"string","enum":["sleep","wake","meal","poop","moment"],"description":"`sleep` 睡着了, `wake` 起床了, `meal` 吃饭, `poop` 大便, `moment` 只拍照."},"at":{"type":"string","format":"date-time","pattern":"^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$","description":"When it happened, ISO 8601 with an offset; default now. Not in the future."},"meal":{"type":["string","null"],"enum":["breakfast","lunch","dinner","snack",null],"description":"Which meal (`breakfast` 早饭, `lunch` 午饭, `dinner` 晚饭, `snack` 加餐); only on kind `meal`."},"eaten":{"type":["string","null"],"enum":["little","half","most","all",null],"description":"吃了多少: `little` 没怎么吃, `half` 一半, `most` 吃了大半, `all` 吃完了; only on kind `meal`."},"poop":{"type":["object","null"],"properties":{"texture":{"type":["string","null"],"enum":["hard","firm","normal","soft","watery",null],"description":"软硬: `hard` 硬球, `firm` 偏硬, `normal` 正常, `soft` 偏软, `watery` 稀水."},"color":{"type":["string","null"],"enum":["yellow","tan","brown","green","black","red","white",null],"description":"颜色: `yellow` 黄, `tan` 黄褐, `brown` 棕, `green` 绿, `black` 黑, `red` 红, `white` 灰白."},"place":{"type":["string","null"],"enum":["toilet","diaper","accident",null],"description":"在哪: `toilet` 马桶, `diaper` 尿布, `accident` 尿裤子了."},"amount":{"type":["string","null"],"enum":["small","medium","large",null],"description":"量: `small` 少, `medium` 中, `large` 多."}},"additionalProperties":false},"note":{"type":["string","null"],"maxLength":500,"description":"One line, optional; `null` clears it."}},"required":["kind"],"additionalProperties":false},"HomeLogPatch":{"type":"object","properties":{"kind":{"description":"Change what it is (the camera's mode strip): meal fields go when it stops being a meal, poop fields when it stops being a poop.","type":"string","enum":["sleep","wake","meal","poop","moment"]},"at":{"type":"string","format":"date-time","pattern":"^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$","description":"When it happened, ISO 8601 with an offset; default now. Not in the future."},"meal":{"type":["string","null"],"enum":["breakfast","lunch","dinner","snack",null],"description":"Which meal (`breakfast` 早饭, `lunch` 午饭, `dinner` 晚饭, `snack` 加餐); only on kind `meal`."},"eaten":{"type":["string","null"],"enum":["little","half","most","all",null],"description":"吃了多少: `little` 没怎么吃, `half` 一半, `most` 吃了大半, `all` 吃完了; only on kind `meal`."},"poop":{"type":["object","null"],"properties":{"texture":{"type":["string","null"],"enum":["hard","firm","normal","soft","watery",null],"description":"软硬: `hard` 硬球, `firm` 偏硬, `normal` 正常, `soft` 偏软, `watery` 稀水."},"color":{"type":["string","null"],"enum":["yellow","tan","brown","green","black","red","white",null],"description":"颜色: `yellow` 黄, `tan` 黄褐, `brown` 棕, `green` 绿, `black` 黑, `red` 红, `white` 灰白."},"place":{"type":["string","null"],"enum":["toilet","diaper","accident",null],"description":"在哪: `toilet` 马桶, `diaper` 尿布, `accident` 尿裤子了."},"amount":{"type":["string","null"],"enum":["small","medium","large",null],"description":"量: `small` 少, `medium` 中, `large` 多."}},"additionalProperties":false,"description":"The poop's fields; the ones sent change, `null` clears them all."},"note":{"type":["string","null"],"maxLength":500,"description":"One line, optional; `null` clears it."},"removed":{"description":"`true`: 撤销 (take it back). It leaves every list; its photos stay stored.","type":"boolean","const":true},"removePhotos":{"description":"Photo ids to take off it (换照片).","maxItems":50,"type":"array","items":{"type":"string","pattern":"^[0-9A-Za-z]{8}$"}}},"additionalProperties":false},"GrowthMeasurement":{"type":"object","properties":{"id":{"type":"string"},"date":{"type":"string","description":"`YYYY-MM-DD`, the Pacific day it was measured."},"time":{"type":["string","null"],"description":"`HH:MM` when known."},"ageDays":{"type":"integer","description":"His age that day, in days."},"weightKg":{"type":["number","null"]},"heightCm":{"type":["number","null"]},"headCm":{"type":["number","null"]},"place":{"type":"string","enum":["clinic","home","other"],"description":"`clinic` (诊所), `home` (家里) or `other`."},"source":{"type":"string","enum":["manual","imported","record"],"description":"`manual` (typed in), `imported` (a pasted table) or `record` (measured at a 医疗记录)."},"percentiles":{"type":"object","properties":{"weight":{"type":["object","null"],"properties":{"z":{"type":"number","description":"WHO z-score (standard deviations from the median)."},"percentile":{"type":"number","description":"WHO percentile, 0-100 with one decimal: 50 is the median of boys his age."}},"required":["z","percentile"]},"height":{"type":["object","null"],"properties":{"z":{"type":"number","description":"WHO z-score (standard deviations from the median)."},"percentile":{"type":"number","description":"WHO percentile, 0-100 with one decimal: 50 is the median of boys his age."}},"required":["z","percentile"]},"head":{"type":["object","null"],"properties":{"z":{"type":"number","description":"WHO z-score (standard deviations from the median)."},"percentile":{"type":"number","description":"WHO percentile, 0-100 with one decimal: 50 is the median of boys his age."}},"required":["z","percentile"]}},"required":["weight","height","head"],"description":"WHO Child Growth Standards percentile of each value (0-5 years); null when not measured or past 5 years."},"recordId":{"type":["string","null"],"description":"The 医疗记录 it was measured at. Only with scope `health:read`."},"note":{"type":["string","null"],"description":"The family's note. Only with scope `health:read`."},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"}},"required":["id","date","time","ageDays","weightKg","heightCm","headCm","place","source","percentiles","createdAt","updatedAt"],"description":"One 生长测量 (growth measurement) of 图图: any of weight, height (length before 2) and head circumference on one day, each with its WHO percentile for his age."},"GrowthChart":{"type":"object","properties":{"metric":{"type":"string","enum":["weight","height","head"]},"unit":{"type":"string","enum":["kg","cm"]},"sex":{"type":"string","enum":["female","male"]},"birthday":{"type":"string"},"points":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"date":{"type":"string"},"months":{"type":"number","description":"Age in months (days / 30.4375)."},"value":{"type":"number"},"percentile":{"type":["object","null"],"properties":{"z":{"type":"number","description":"WHO z-score (standard deviations from the median)."},"percentile":{"type":"number","description":"WHO percentile, 0-100 with one decimal: 50 is the median of boys his age."}},"required":["z","percentile"]}},"required":["id","date","months","value","percentile"]}},"bands":{"type":"array","items":{"type":"object","properties":{"percentile":{"type":"number","description":"3, 15, 50, 85 or 97."},"points":{"type":"array","items":{"type":"object","properties":{"months":{"type":"integer"},"value":{"type":"number"}},"required":["months","value"]}}},"required":["percentile","points"]},"description":"WHO curves month by month from birth to a little past today (at most 60 months). Below 24 months height is length lying down, as WHO measures it."}},"required":["metric","unit","sex","birthday","points","bands"],"description":"One metric of 图图's growth chart: his measurements and the WHO percentile bands to draw behind them."},"GrowthInput":{"type":"object","properties":{"date":{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}$","description":"`YYYY-MM-DD`, the Pacific day it was measured."},"time":{"type":["string","null"],"pattern":"^([01]\\d|2[0-3]):[0-5]\\d$","description":"`HH:MM` (Pacific), optional."},"weightKg":{"type":["number","null"],"exclusiveMinimum":0,"exclusiveMaximum":100,"description":"Weight in kilograms; `null` for not measured."},"heightCm":{"type":["number","null"],"exclusiveMinimum":20,"exclusiveMaximum":200,"description":"Length (lying down, under 2) or height (standing) in centimetres; `null` for not measured."},"headCm":{"type":["number","null"],"exclusiveMinimum":20,"exclusiveMaximum":70,"description":"Head circumference in centimetres; `null` for not measured."},"place":{"description":"`clinic` (诊所), `home` (家里, the default) or `other`.","type":"string","enum":["clinic","home","other"]},"note":{"type":["string","null"],"maxLength":1000},"recordId":{"type":["string","null"],"minLength":1,"maxLength":20,"description":"The 医疗记录 it was measured at; its source is then `record`."}},"required":["date"],"additionalProperties":false},"HealthRecord":{"type":"object","properties":{"id":{"type":"string"},"date":{"type":"string","description":"`YYYY-MM-DD`."},"kind":{"type":"string","enum":["checkup","vaccine","sick_visit","medication","allergy","lab","other"],"description":"`checkup` 体检, `vaccine` 疫苗, `sick_visit` 生病就诊, `medication` 用药, `allergy` 过敏, `lab` 化验检查, `other` 其他."},"title":{"type":"string"},"provider":{"type":["string","null"]},"doctor":{"type":["string","null"]},"summary":{"type":["string","null"]},"vaccines":{"type":"array","items":{"type":"object","properties":{"name":{"type":"string"},"dose":{"type":["integer","null"]}},"required":["name","dose"]}},"medication":{"type":["object","null"],"properties":{"name":{"type":"string"},"dosage":{"type":["string","null"]},"startOn":{"type":["string","null"]},"endOn":{"type":["string","null"]}},"required":["name","dosage","startOn","endOn"]},"measurements":{"type":"array","items":{"$ref":"#/components/schemas/GrowthMeasurement"}},"attachments":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"fileName":{"type":["string","null"]},"contentType":{"type":"string"},"bytes":{"type":"integer"},"createdAt":{"type":"string","format":"date-time"},"url":{"type":"string","format":"uri","description":"A signed link that works for one hour without the key."}},"required":["id","fileName","contentType","bytes","createdAt","url"]}},"createdBy":{"type":"string","description":"`owner`, or the API key's name."},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"}},"required":["id","date","kind","title","provider","doctor","summary","vaccines","medication","measurements","attachments","createdBy","createdAt","updatedAt"],"description":"One 医疗记录 (health record) of 图图: a visit, shot, illness, medicine, allergy or test, with what was measured and photos or PDFs of the paperwork."},"HealthRecordInput":{"type":"object","properties":{"date":{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}$","description":"`YYYY-MM-DD`, the Pacific day of the visit, shot or start."},"kind":{"type":"string","enum":["checkup","vaccine","sick_visit","medication","allergy","lab","other"],"description":"`checkup` 体检, `vaccine` 疫苗, `sick_visit` 生病就诊, `medication` 用药, `allergy` 过敏, `lab` 化验检查, `other` 其他."},"title":{"type":"string","minLength":1,"maxLength":200},"provider":{"type":["string","null"],"maxLength":200,"description":"The clinic or hospital."},"doctor":{"type":["string","null"],"maxLength":120},"summary":{"type":["string","null"],"maxLength":8000,"description":"What happened, what the doctor said, follow-ups."},"vaccines":{"description":"Shots given at this record. Replaces the whole list.","maxItems":20,"type":"array","items":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":80,"description":"The vaccine as written, e.g. `DTaP`, `MMR`, `Flu`."},"dose":{"type":["integer","null"],"minimum":1,"maximum":20,"description":"Which dose (1, 2, …), when known."}},"required":["name"],"additionalProperties":false}},"medication":{"type":["object","null"],"properties":{"name":{"type":"string","minLength":1,"maxLength":120},"dosage":{"type":["string","null"],"maxLength":200,"description":"How much and how often, as written, e.g. `5 ml 每天两次`."},"startOn":{"type":["string","null"],"pattern":"^\\d{4}-\\d{2}-\\d{2}$"},"endOn":{"type":["string","null"],"pattern":"^\\d{4}-\\d{2}-\\d{2}$"}},"required":["name"],"additionalProperties":false,"description":"The medicine a 用药 record is about; `null` clears it."},"measurement":{"description":"Measured at this visit: written as a growth measurement on the record's day, at the clinic.","type":"object","properties":{"time":{"type":["string","null"],"pattern":"^([01]\\d|2[0-3]):[0-5]\\d$","description":"`HH:MM` (Pacific), optional."},"weightKg":{"type":["number","null"],"exclusiveMinimum":0,"exclusiveMaximum":100,"description":"Weight in kilograms; `null` for not measured."},"heightCm":{"type":["number","null"],"exclusiveMinimum":20,"exclusiveMaximum":200,"description":"Length (lying down, under 2) or height (standing) in centimetres; `null` for not measured."},"headCm":{"type":["number","null"],"exclusiveMinimum":20,"exclusiveMaximum":70,"description":"Head circumference in centimetres; `null` for not measured."}},"additionalProperties":false}},"required":["date","kind","title"],"additionalProperties":false},"HealthRecordPatch":{"type":"object","properties":{"date":{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}$","description":"`YYYY-MM-DD`, the Pacific day of the visit, shot or start."},"kind":{"type":"string","enum":["checkup","vaccine","sick_visit","medication","allergy","lab","other"],"description":"`checkup` 体检, `vaccine` 疫苗, `sick_visit` 生病就诊, `medication` 用药, `allergy` 过敏, `lab` 化验检查, `other` 其他."},"title":{"type":"string","minLength":1,"maxLength":200},"provider":{"type":["string","null"],"maxLength":200,"description":"The clinic or hospital."},"doctor":{"type":["string","null"],"maxLength":120},"summary":{"type":["string","null"],"maxLength":8000,"description":"What happened, what the doctor said, follow-ups."},"vaccines":{"description":"Shots given at this record. Replaces the whole list.","maxItems":20,"type":"array","items":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":80,"description":"The vaccine as written, e.g. `DTaP`, `MMR`, `Flu`."},"dose":{"type":["integer","null"],"minimum":1,"maximum":20,"description":"Which dose (1, 2, …), when known."}},"required":["name"],"additionalProperties":false}},"medication":{"type":["object","null"],"properties":{"name":{"type":"string","minLength":1,"maxLength":120},"dosage":{"type":["string","null"],"maxLength":200,"description":"How much and how often, as written, e.g. `5 ml 每天两次`."},"startOn":{"type":["string","null"],"pattern":"^\\d{4}-\\d{2}-\\d{2}$"},"endOn":{"type":["string","null"],"pattern":"^\\d{4}-\\d{2}-\\d{2}$"}},"required":["name"],"additionalProperties":false,"description":"The medicine a 用药 record is about; `null` clears it."}},"additionalProperties":false,"description":"Only the fields sent change."},"Vaccines":{"type":"object","properties":{"vaccines":{"type":"array","items":{"type":"object","properties":{"name":{"type":"string"},"doses":{"type":"array","items":{"type":"object","properties":{"date":{"type":"string"},"dose":{"type":["integer","null"]},"recordId":{"type":"string"},"recordTitle":{"type":"string"},"provider":{"type":["string","null"]}},"required":["date","dose","recordId","recordTitle","provider"]}}},"required":["name","doses"]}}},"required":["vaccines"],"description":"Every shot 图图 has had, by vaccine, doses in date order."},"Render":{"type":"object","properties":{"id":{"type":"string"},"status":{"type":"string","enum":["queued","running","completed","failed","cancelled"],"description":"`queued` waits for its turn (or its window, or a free machine); `running` is on a machine."},"frames":{"type":"object","properties":{"start":{"type":"integer"},"end":{"type":"integer"},"step":{"type":"integer"}},"required":["start","end","step"]},"options":{"type":"object","properties":{"device":{"type":"string","enum":["gpu","cpu"]},"engine":{"type":"string","enum":["scene","cycles","eevee"]},"samples":{"type":["integer","null"]},"resolutionPercent":{"type":["integer","null"]},"format":{"type":"string","enum":["png","jpeg","exr"]}},"required":["device","engine","samples","resolutionPercent","format"]},"priority":{"type":"integer","description":"Higher first: 100 now, 50 normal, 10 backlog."},"window":{"type":["string","null"],"enum":["night",null],"description":"`night`: starts only 22:00-08:00 Pacific."},"progress":{"type":["object","null"],"properties":{"unit":{"type":"string","const":"frame"},"done":{"type":"integer","description":"Frames stored so far.","examples":[9]},"total":{"type":"integer","description":"Frames in the render.","examples":[24]},"current":{"type":["integer","null"],"description":"The frame being rendered now."},"secondsPerFrame":{"type":["number","null"],"description":"Average so far."},"etaSeconds":{"type":["number","null"],"description":"Time left, by that average."}},"required":["unit","done","total"]},"node":{"type":["string","null"],"description":"The machine rendering it (hornet, goblin)."},"attempts":{"type":"integer"},"error":{"type":["string","null"],"description":"Why the last try failed, or why it waits."},"createdAt":{"type":"string"},"startedAt":{"type":["string","null"]},"completedAt":{"type":["string","null"]},"nextTryAt":{"type":["string","null"],"description":"When a queued render is looked at next."},"framesUrl":{"type":"string","description":"The frames stored so far.","examples":["/renders/0b9c2f7e-4d1a-4c3b-9e8f-1a2b3c4d5e6f/frames"]}},"required":["id","status","frames","options","priority","window","progress","node","attempts","error","createdAt","startedAt","completedAt","nextTryAt","framesUrl"],"description":"A Blender render in the jobs queue."},"ShotFrame":{"type":"object","properties":{"url":{"type":"string","format":"uri","description":"The frame at the video's own resolution (`width` × `height`)."},"smallUrl":{"type":["string","null"],"format":"uri","description":"The same frame about 480 pixels wide, for grids; null when the tool sent none."},"atS":{"type":["number","null"],"description":"The second it is taken at."},"width":{"type":["integer","null"]},"height":{"type":["integer","null"]}},"required":["url","smallUrl","atS","width","height"],"description":"One frame of a shot, taken by the tool that cut the video at `atS`. Signed links, valid for one hour: use them right away, never store them."},"Shot":{"type":"object","properties":{"position":{"type":"integer","description":"1, 2, … in the video's order."},"startS":{"type":"number"},"endS":{"type":"number"},"frame":{"anyOf":[{"$ref":"#/components/schemas/ShotFrame"},{"type":"null"}]},"speech":{"type":"string","description":"The transcript sentences whose middle falls in this shot, joined; empty when nothing is said."},"scene":{"type":["string","null"],"description":"One sentence on the picture."},"action":{"type":["string","null"],"description":"What happens."},"setting":{"type":["string","null"],"description":"Where it is."},"onScreenText":{"type":["string","null"],"description":"Text in the picture (OCR), burned-in subtitles included."},"subjects":{"type":"array","items":{"type":"string"},"description":"Who and what is in it."},"tags":{"type":"array","items":{"type":"string"}},"extra":{"type":["object","null"],"propertyNames":{"type":"string"},"additionalProperties":{},"description":"Anything else the recognizer produced, as it sent it."},"describedBy":{"type":["string","null"],"description":"The recognizer that wrote the fields above."}},"required":["position","startS","endS","frame","speech","scene","action","setting","onScreenText","subjects","tags","extra","describedBy"],"description":"One shot: from one cut to the next. The words said over it come from the transcript; the rest is what a recognizer saw, null or empty until one has looked."},"ShotSentence":{"type":"object","properties":{"position":{"type":"integer"},"paragraph":{"type":"integer","description":"Numbered from 0; sentences sharing it are one paragraph."},"startS":{"type":"number"},"endS":{"type":"number"},"text":{"type":"string"},"shot":{"type":["integer","null"],"description":"The shot its middle falls in."}},"required":["position","paragraph","startS","endS","text","shot"],"description":"One sentence of the speech, in order: read them in a row to read the video as an article, and show `shot`'s frame beside them."},"ShotVideo":{"type":"object","properties":{"item":{"type":"object","properties":{"id":{"type":"string"},"publicId":{"type":["string","null"]},"title":{"type":["string","null"]},"source":{"type":"string","description":"Lowercase source code."},"url":{"type":["string","null"]},"hubUrl":{"type":"string","format":"uri"}},"required":["id","publicId","title","source","url","hubUrl"]},"mediaId":{"type":"string","description":"The video file (`media[].id` in `/items/{id}/content`)."},"video":{"type":["object","null"],"properties":{"url":{"type":"string","format":"uri","description":"Signed link to the whole file, valid one hour; served with Range, so a player that seeks to a shot's `startS` fetches only the bytes around it. Use it right away, never store it."},"stableUrl":{"type":["string","null"],"format":"uri","description":"The file's stable address (`GET /items/{id}/media/{mediaId}`): may be stored, needs the key with `media:read` on every fetch."},"contentType":{"type":["string","null"]},"width":{"type":["integer","null"]},"height":{"type":["integer","null"]}},"required":["url","stableUrl","contentType","width","height"],"description":"The stored video the shots play from: load it and seek to a shot's `startS`. There are no per-shot files. Null when the file is not stored."},"durationS":{"type":"number"},"slicedBy":{"type":"string","description":"The tool and method that cut it."},"slicedAt":{"type":"string","format":"date-time"},"shots":{"type":"array","items":{"$ref":"#/components/schemas/Shot"}},"sentences":{"type":"array","items":{"$ref":"#/components/schemas/ShotSentence"}}},"required":["item","mediaId","video","durationS","slicedBy","slicedAt","shots","sentences"],"description":"One video file laid out as its shots, with its speech as sentences."}}}}