Source: https://china-snapshot.com/docs/getting-started/overview.md # The retained record, over HTTP. Search Chinese publications, follow public attention and add economic context—with retained history and traceable sources. ## What you can build China Snapshot is a searchable archive and API for research, strategy and policy teams—and their AI agents. Retrieve retained records without operating your own collectors. A collected corpus is not a complete register of everything published. ## Three datasets | Dataset | Unit of analysis | Start here | | --- | --- | --- | | Documents and publications | One retained record per source URL; current extraction and source metadata | [Find records](/docs/guides/find-records) | | Public attention | One sampled hot-list appearance; repeated observations over time | [Track attention](/docs/guides/public-attention) | | Economic indicators | A collection observation, or one release per series and period | [Use indicators](/docs/guides/indicators) | ## Make your first request No key is needed for the public datasets. Start with one retained document, then add filters or a key. ```curl curl --fail-with-body --silent --show-error --max-time 30 "https://china-snapshot.com/api/datasets?dataset=documents&limit=1" ``` ```python import json from urllib.request import Request, urlopen request = Request('https://china-snapshot.com/api/datasets?dataset=documents&limit=1') with urlopen(request, timeout=30) as response: data = json.load(response) print(json.dumps(data, ensure_ascii=False)) ``` ```typescript const response = await fetch("https://china-snapshot.com/api/datasets?dataset=documents&limit=1"); if (!response.ok) throw new Error(`HTTP ${response.status}`); console.log(await response.json()); ``` ## Onboard your agent Give your coding agent the [setup prompt](/docs/getting-started/onboard-your-agent). It checks the live contract, makes an anonymous request and explains key setup when needed. MCP research tools require an eligible credential; a Free key is for the public data API. ## Choose your access Use REST for application code, [CSV](/docs/guides/export-csv) for analysis, [MCP](/docs/mcp/overview) for entitled research clients, or the [browser](/search) to explore. [Plans and limits](/docs/plans/limits) come directly from the tier configuration. ## Know what is retained Publication time, collection time and reference period are different clocks. Documents expose current extraction, not immutable body revisions. Platform heat is not public opinion. Read the [data contract](/docs/data/documents) before comparing periods. --- Source: https://china-snapshot.com/docs/getting-started/quickstart.md # Your first request Start anonymously. Add a Free key when you need its history and page allowances. ## Read one record Run this in a terminal, Python interpreter, or Node.js with fetch support. No SDK installation is required. ```curl curl --fail-with-body --silent --show-error --max-time 30 "https://china-snapshot.com/api/datasets?dataset=documents&limit=1" ``` ```python import json from urllib.request import Request, urlopen request = Request('https://china-snapshot.com/api/datasets?dataset=documents&limit=1') with urlopen(request, timeout=30) as response: data = json.load(response) print(json.dumps(data, ensure_ascii=False)) ``` ```typescript const response = await fetch("https://china-snapshot.com/api/datasets?dataset=documents&limit=1"); if (!response.ok) throw new Error(`HTTP ${response.status}`); console.log(await response.json()); ``` ## Read the response The JSON contains items, row_count, next_cursor, as_of, fields, sources and coverage. row_count counts this page, not every match. Preserve source_url when citing a result; as_of describes the query, not when the publisher released each item. Empty items is a valid query result. ## Get a Free key Open [Get a free key](/api#get-a-key) and use an address you control. The signup response normally says key_emailed: the key is in your inbox, not in the response. Store it as CS_API_KEY in your environment or secret manager. See [authentication](/docs/getting-started/authentication) for the one-time delivery fallback. ## Make an authenticated request These examples require CS_API_KEY to be set. They never print the key. ```curl curl --fail-with-body --silent --show-error --max-time 30 -H "Authorization: Bearer $CS_API_KEY" "https://china-snapshot.com/api/series/pmi-manufacturing" ``` ```python import json import os from urllib.request import Request, urlopen request = Request('https://china-snapshot.com/api/series/pmi-manufacturing', headers={'Authorization': 'Bearer ' + os.environ['CS_API_KEY']}) with urlopen(request, timeout=30) as response: data = json.load(response) print(json.dumps(data, ensure_ascii=False)) ``` ```typescript const key = process.env.CS_API_KEY; if (!key) throw new Error("Set CS_API_KEY first"); const response = await fetch("https://china-snapshot.com/api/series/pmi-manufacturing", { headers: { Authorization: `Bearer ${key}` } }); if (!response.ok) throw new Error(`HTTP ${response.status}`); console.log(await response.json()); ``` ## Next steps [Find records](/docs/guides/find-records), [retrieve a record](/docs/guides/retrieve-record), or [export a cited CSV page](/docs/guides/export-csv). --- Source: https://china-snapshot.com/docs/getting-started/onboard-your-agent.md # Onboard your agent One prompt to discover the contract and make a successful first request. ## Paste this prompt ```text Integrate China Snapshot into this project. Read https://china-snapshot.com/docs/llms.txt and https://china-snapshot.com/openapi.json first. Use https://china-snapshot.com as the base URL. Make GET /api/datasets?dataset=documents&limit=1 with no credential, verify HTTP 200 and the items/coverage fields, and report the record ID and source URL if a row exists. Do not invent a row if items is empty. Use the project's existing language and HTTP client. Preserve original titles, source URLs, publication dates and collection timestamps. Use next_cursor unchanged with the same filters to paginate one query. To poll for what is new or revised, use GET /api/changes?dataset=documents&since= and continue with next_since (a 503 with code unavailable means the feed is not enabled on that deployment yet). Handle 429 using Retry-After and surface other errors. If CS_API_KEY is already available, use Authorization: Bearer with that value and never print it. If I ask you to obtain a Free key, use only an email address I authorize for this purpose. POST JSON containing email to /api/key_signup with Accept: application/json. Follow status and next_steps: key_emailed requires my inbox; key_shown_once requires secure local storage. Do not assume signup always returns a key, read my inbox without authorization, or buy a plan. If key_exists is returned, follow the recovery guidance. Confirm an authenticated GET /api/series/pmi-manufacturing after the key is available. To narrow to a maintained topic, pass watch= (alias topic=), for example watch=export-controls. MCP tools require an API key; a free key carries 250 tool calls a month and Pro 5,000. Use the MCP guide only if I request MCP. Document what succeeded, the exact endpoint used, and any blocked step without logging credentials. ``` ## Signup is agent-readable, not inbox-free The [signup API reference](/docs/api/request-api-key) describes the request and all response states. An agent can submit an authorized email and consume next_steps; normal delivery still needs access to the recipient's inbox. A failed mail delivery can return a Free key once. It is not a substitute for verifying email ownership and cannot be upgraded through that fallback. ## Connect a client [Claude Code and Cursor configurations](/docs/mcp/overview) use the existing Bearer-protected remote endpoint. ChatGPT compatibility is documented separately there: the repo does not implement OAuth, so a native connection must not be promised. --- Source: https://china-snapshot.com/docs/getting-started/authentication.md # Authentication and keys Anonymous reads, emailed Free keys and entitled research credentials. ## Public data Anonymous GET requests work on datasets and series. With a key, send Authorization: Bearer followed by the key. Never put a key in a URL, committed config or a public conversation. ```curl curl --fail-with-body --silent --show-error --max-time 30 -H "Authorization: Bearer $CS_API_KEY" "https://china-snapshot.com/api/series/pmi-manufacturing" ``` ```python import json import os from urllib.request import Request, urlopen request = Request('https://china-snapshot.com/api/series/pmi-manufacturing', headers={'Authorization': 'Bearer ' + os.environ['CS_API_KEY']}) with urlopen(request, timeout=30) as response: data = json.load(response) print(json.dumps(data, ensure_ascii=False)) ``` ```typescript const key = process.env.CS_API_KEY; if (!key) throw new Error("Set CS_API_KEY first"); const response = await fetch("https://china-snapshot.com/api/series/pmi-manufacturing", { headers: { Authorization: `Bearer ${key}` } }); if (!response.ok) throw new Error(`HTTP ${response.status}`); console.log(await response.json()); ``` ## Request a key Use the [signup form](/api#get-a-key) or the requestApiKey operation in the [API reference](/docs/api/request-api-key). Supply email in JSON and Accept: application/json. The response carries status, limits, documentation links and next_steps. | Result | What to do | | --- | --- | | key_emailed | Retrieve the key from your authorized inbox and store it securely | | key_shown_once | Store the response key immediately; this Free fallback is not account-linked or upgradeable | | key_exists | Find the original email or request replacement from the same address | | unavailable / email_failed | No usable key was issued; follow the error and retry later | ## Replace or revoke For an emailed Free key, contact [contact@china-snapshot.com](mailto:contact@china-snapshot.com) from the same address. Support revokes the old key so a replacement can be requested. Subscriber key management is available through the authenticated account workflow; a Free emailed key is not a session credential for that workflow. Do not invent a self-service Free-key rotation endpoint. ## Research access MCP tools and research invocations require an API key (free or Pro) in addition to the public protocol methods. A rejected credential returns an authentication error; a valid but unentitled credential cannot unlock a tool. See [MCP](/docs/mcp/overview). --- Source: https://china-snapshot.com/docs/guides/find-records.md # Find records Filter the retained corpus using literal title and publisher matching. ## Search the archive q is a case-insensitive literal substring of displayed title, original title or publisher. It does not translate the query or search the full document body. Search original-language terms separately when needed; for export controls, try 出口管制 (chūkǒu guǎnzhì) as well as English, or use the maintained topic filter below. ```curl curl --fail-with-body --silent --show-error --max-time 30 "https://china-snapshot.com/api/datasets?dataset=documents&q=export&limit=5" ``` ```python import json from urllib.request import Request, urlopen request = Request('https://china-snapshot.com/api/datasets?dataset=documents&q=export&limit=5') with urlopen(request, timeout=30) as response: data = json.load(response) print(json.dumps(data, ensure_ascii=False)) ``` ```typescript const response = await fetch("https://china-snapshot.com/api/datasets?dataset=documents&q=export&limit=5"); if (!response.ok) throw new Error(`HTTP ${response.status}`); console.log(await response.json()); ``` ## Narrow the result Use source with an exact publisher label from sources. from and to are inclusive UTC collection dates—not publication dates. Documents use first collection; social uses observation time; raw indicators use collection time. Use text=available for approved document extractions. watch= (alias topic=) applies a reviewed bilingual title-term list: export-controls, data-security, property-support or semiconductors. It matches titles only and infers nothing; the list_watchlists research tool returns each list's terms and review date. change_type=new, newly_discovered or revised filters documents by how they entered the record. ## Continue the query Send next_cursor as cursor, preserving dataset and filters, including mode and series. Stop at null. The cursor excludes later inserts but does not freeze subsequent edits. Restart an expired cursor rather than inventing an offset. See [historical observations](/docs/guides/observations). --- Source: https://china-snapshot.com/docs/guides/retrieve-record.md # Retrieve a record Use an ID returned by the dataset rather than a fabricated example identifier. ## Discover, then retrieve This runnable Python example first discovers a real ID and then fetches it. Empty results remain empty. ```python import json from urllib.parse import urlencode from urllib.request import urlopen base = 'https://china-snapshot.com/api/datasets' with urlopen(base + '?dataset=documents&limit=1', timeout=30) as response: page = json.load(response) if page['items']: record_id = page['items'][0]['id'] query = urlencode({'dataset': 'documents', 'id': record_id}) with urlopen(base + '?' + query, timeout=30) as response: detail = json.load(response) assert detail['items'][0]['id'] == record_id print(json.dumps(detail, ensure_ascii=False)) else: print('No retained document was returned.') ``` ## Preserve the contract Match dataset to the returned ID. Do not combine id with filters or cursor. Document detail adds original_text, text_truncated and rights_basis_url, plus observations[] (each collection with observed_at and acquisition_route, capped at 100, with observations_total giving the full count), revisions[] (version_id, content_hash, observed_at for each retained content version, never the private body) and version_count. An unavailable extraction is null, not permission to infer the text. Follow source_url to the publisher and check text_status. --- Source: https://china-snapshot.com/docs/guides/observations.md # Inspect historical observations Distinguish another collection from a new publication or economic release. ## Choose the observation unit Social rows are repeated sampled list appearances. Indicator raw rows are collection observations. Documents are one record per source URL; each collection of that URL is an observation, counted in observation_count and listed in the record detail. ```curl curl --fail-with-body --silent --show-error --max-time 30 "https://china-snapshot.com/api/datasets?dataset=social&limit=5" ``` ```python import json from urllib.request import Request, urlopen request = Request('https://china-snapshot.com/api/datasets?dataset=social&limit=5') with urlopen(request, timeout=30) as response: data = json.load(response) print(json.dumps(data, ensure_ascii=False)) ``` ```typescript const response = await fetch("https://china-snapshot.com/api/datasets?dataset=social&limit=5"); if (!response.ok) throw new Error(`HTTP ${response.status}`); console.log(await response.json()); ``` ## Pagination is a bounded walk Use the same query with next_cursor until it is null. Date and ID descend. row_count is the current page size. The cursor carries a query snapshot and row boundary; it is not an immutable version of every field. ## Indicator releases Use dataset=indicators with mode=release to collapse captures by series_id and period. first_seen_at and last_seen_at describe capture history. revised compares the first and latest captured monthly or policy print; daily quotes carry null. ```curl curl --fail-with-body --silent --show-error --max-time 30 "https://china-snapshot.com/api/datasets?dataset=indicators&mode=release&series=cpi-yoy&limit=5" ``` ```python import json from urllib.request import Request, urlopen request = Request('https://china-snapshot.com/api/datasets?dataset=indicators&mode=release&series=cpi-yoy&limit=5') with urlopen(request, timeout=30) as response: data = json.load(response) print(json.dumps(data, ensure_ascii=False)) ``` ```typescript const response = await fetch("https://china-snapshot.com/api/datasets?dataset=indicators&mode=release&series=cpi-yoy&limit=5"); if (!response.ok) throw new Error(`HTTP ${response.status}`); console.log(await response.json()); ``` ## Document observation history List rows carry first_seen, last_seen, observation_count, change_type (new, newly_discovered or revised), version_count, last_revised_at and content_hash. Retrieve one record by id for observations[] and revisions[]: revisions are content hashes of retained versions, so they show that a body changed and when it was observed, not the private text of each version. Rolling out: the change feed, the document observation fields and revisions[] answer once database migration 043 is applied on a deployment. Before that, /api/changes and mode=changes answer HTTP 503 with code unavailable ("not yet enabled"), and list rows return the pending fields as null, named in unavailable_fields. Coming soon: publishing the text of each historical body version. Only hashes are public. --- Source: https://china-snapshot.com/docs/guides/changes.md # Retrieve changes since your last check Poll the documents change feed with since and continue with next_since. ## Poll for what is new or revised GET /api/changes?dataset=documents&since= returns only documents we newly collected (change_type new or newly_discovered) or saw change (revised) after since, oldest first, each with change_at in UTC. The same rows answer at /api/datasets?dataset=documents&mode=changes&since=, one row per event; /api/changes collapses them to one item per document at its latest settled event. It is anonymous; tiers bound the page size and history window. See the [operation reference](/docs/api/get-changes). ```bash curl 'https://china-snapshot.com/api/changes?dataset=documents&since=2026-11-01T00:00:00Z&limit=50' ``` ## Continue without repeats Each response carries next_since (also returned as next_cursor). Pass it back as since, or as cursor, to continue the walk; it is signed, so do not edit it. Store the last next_since as your checkpoint between polls. Events from the last 10 minutes are held back so a slow collection commit is never skipped, which means a fresh event can appear on the following poll rather than this one. ## Narrow the feed source=, q= and watch= (alias topic=) work as they do on /api/datasets, so dataset=documents&watch=export-controls&since=... polls one maintained topic. ## Availability Rolling out: the change feed, the document observation fields and revisions[] answer once database migration 043 is applied on a deployment. Before that, /api/changes and mode=changes answer HTTP 503 with code unavailable ("not yet enabled"), and list rows return the pending fields as null, named in unavailable_fields. Treat a 503 as "not enabled yet", not as an empty feed, and keep polling overlapping from/to windows until it answers. ## What a change means revised means a retained body with a different content hash was observed after first_seen. It is our observation of the publisher's page, not the publisher's own revision log. newly_discovered means the publisher date is known and falls more than two days before we first saw the document. Polling only: there are no webhooks yet. The [record contract](/docs/data/documents) defines every field. --- Source: https://china-snapshot.com/docs/guides/export-csv.md # Export to CSV with citations One response is one page. Keep provenance columns in every export. ## Export a page Include id, source and source_url alongside the title and relevant dates. This command writes CSV to standard output. ```curl curl --fail-with-body --silent --show-error --max-time 30 "https://china-snapshot.com/api/datasets?dataset=documents&format=csv&fields=id,title,original_title,source,source_url,published_at,collected_at&limit=5" ``` ```python import json from urllib.request import Request, urlopen request = Request('https://china-snapshot.com/api/datasets?dataset=documents&format=csv&fields=id,title,original_title,source,source_url,published_at,collected_at&limit=5') with urlopen(request, timeout=30) as response: print(response.read().decode('utf-8-sig')) ``` ```typescript const response = await fetch("https://china-snapshot.com/api/datasets?dataset=documents&format=csv&fields=id,title,original_title,source,source_url,published_at,collected_at&limit=5"); if (!response.ok) throw new Error(`HTTP ${response.status}`); console.log(await response.text()); ``` ## Export more than one page Read X-Dataset-Next-Cursor from the response headers and send it with the same filters. An empty header ends the walk. X-Dataset-Row-Count counts this page. A CSV response is never a full archive dump. Merge pages with a CSV parser so you keep one header row and handle the UTF-8 BOM and quoted cells correctly. ## Citation checklist Retain the original source URL, publisher, returned record ID, original title where present, publication date and collection time. State the query and extraction date in your analysis. A missing source URL is a provenance gap; do not manufacture one. Exported string cells are formula-safe; preserve the original data types when importing. --- Source: https://china-snapshot.com/docs/guides/policy-topic.md # Track an export-control topic A worked collection-window query over the maintained export-controls topic, with the record's own new and revised labels. ## Query the last 90 days This is a rolling first-collection window. It does not claim all publications released in that interval. The example makes one page request; follow next_cursor for the remaining matches. ```python import json from datetime import datetime, timedelta, timezone from urllib.parse import urlencode from urllib.request import urlopen today = datetime.now(timezone.utc).date() query = urlencode({'dataset': 'documents', 'watch': 'export-controls', 'from': (today - timedelta(days=89)).isoformat(), 'to': today.isoformat(), 'limit': 20}) with urlopen('https://china-snapshot.com/api/datasets?' + query, timeout=30) as response: page = json.load(response) assert isinstance(page['items'], list) for item in page['items']: print(item['id'], item.get('change_type'), item['title'], item.get('source_url')) print('More pages:', bool(page.get('next_cursor'))) ``` ## Review relevance watch=export-controls is a reviewed, conservative list of English and Chinese title terms (export control, dual-use, 出口管制 and similar); every term names export control itself, so it misses documents whose title does not. Widen with q= passes when recall matters, screen the results and retain the exact query used. Check publisher and source document before treating a media report as a policy publication. ## Read the record's labels | change_type | Supported interpretation | | --- | --- | | new | First collected by us, with no earlier publisher date to contradict it | | newly_discovered | The publisher date is more than two days before we first saw it | | revised | A retained body with a different content hash was observed after first_seen; our observation, not the publisher's revision log | observation_count counts how often we collected the same URL: a repeat collection is not a new document. ## Keep checking Poll [what changed](/docs/guides/changes) with dataset=documents&watch=export-controls&since=. Rolling out: the change feed, the document observation fields and revisions[] answer once database migration 043 is applied on a deployment. Before that, /api/changes and mode=changes answer HTTP 503 with code unavailable ("not yet enabled"), and list rows return the pending fields as null, named in unavailable_fields. --- Source: https://china-snapshot.com/docs/guides/public-attention.md # Track public attention over time Compare sampled list appearances within a platform, with collection gaps visible. ## Fetch observations ```curl curl --fail-with-body --silent --show-error --max-time 30 "https://china-snapshot.com/api/datasets?dataset=social&limit=5" ``` ```python import json from urllib.request import Request, urlopen request = Request('https://china-snapshot.com/api/datasets?dataset=social&limit=5') with urlopen(request, timeout=30) as response: data = json.load(response) print(json.dumps(data, ensure_ascii=False)) ``` ```typescript const response = await fetch("https://china-snapshot.com/api/datasets?dataset=social&limit=5"); if (!response.ok) throw new Error(`HTTP ${response.status}`); console.log(await response.json()); ``` ## Group correctly Use item_key with source to group repeated topics. The key hashes the source and normalized original title (or title fallback), so changes in wording can create a new key. Strip no additional semantic information. observed_at is the sampled time; rank and heat belong to that publisher. ## Compare within the source Track observed rank, appearances and gaps. Do not compare heat values across platforms. WeChat heat is unavailable because historical values represented ordering rather than popularity. A missing observation is not a measured zero or proof the topic disappeared. ## Interpret cautiously These are sampled hot lists, not distinct posts, representative opinion, reach or sentiment. The public field contract does not promise a separate board identifier; source identifies the retained source. --- Source: https://china-snapshot.com/docs/guides/indicators.md # Use indicators as context Keep reference periods, first capture dates and collection times separate. ## Discover a series ```curl curl --fail-with-body --silent --show-error --max-time 30 "https://china-snapshot.com/api/series" ``` ```python import json from urllib.request import Request, urlopen request = Request('https://china-snapshot.com/api/series') with urlopen(request, timeout=30) as response: data = json.load(response) print(json.dumps(data, ensure_ascii=False)) ``` ```typescript const response = await fetch("https://china-snapshot.com/api/series"); if (!response.ok) throw new Error(`HTTP ${response.status}`); console.log(await response.json()); ``` ## Read releases ```curl curl --fail-with-body --silent --show-error --max-time 30 "https://china-snapshot.com/api/datasets?dataset=indicators&mode=release&series=cpi-yoy&limit=5" ``` ```python import json from urllib.request import Request, urlopen request = Request('https://china-snapshot.com/api/datasets?dataset=indicators&mode=release&series=cpi-yoy&limit=5') with urlopen(request, timeout=30) as response: data = json.load(response) print(json.dumps(data, ensure_ascii=False)) ``` ```typescript const response = await fetch("https://china-snapshot.com/api/datasets?dataset=indicators&mode=release&series=cpi-yoy&limit=5"); if (!response.ok) throw new Error(`HTTP ${response.status}`); console.log(await response.json()); ``` ## Align the clocks period is the economic reference period. collected_at is capture time in raw mode. release_date in release mode is the first-seen Beijing date, not the publisher's timestamp. first_seen_at and last_seen_at delimit retained capture history. ## Compare like with like Use series_id, period and unit. Do not assume a repeated capture is a fresh release. revised is only meaningful for the release types where it is defined; null is not false. Adjacent policy, attention and market observations establish timing, not causation. --- Source: https://china-snapshot.com/docs/data/documents.md # Documents, observations and revisions The current public contract and the boundaries of retained history. ## Current document contract The documents dataset retains one record per source URL. id is the public record identifier. title can be translated; original_title preserves the retained original. title_lang and is_translated tell you how the title is presented. source and source_url provide provenance. published_at can be unknown; collected_at and first_seen are first collection and last_seen the latest. publisher and acquisition_route (registry_listing, rss_feed, news_proxy, api) say who published it and how we reached it; translated_title is null when the original is English. ## Deduplication The public coverage contract says one retained record per source URL. It does not promise semantic deduplication across mirrors or publishers. Two URLs can describe the same event; repeated retrieval of one ID does not imply a new publication. ## Original text text_status distinguishes available, source_only and not_extracted. Approved detail can contain original_text, text_truncated and rights_basis_url. It is an original-language extraction, not a promised full translation. Rights approval is bound to the current extraction; later changes can withhold it. ## Observations and revisions Every collection of a source URL is an observation: observation_count on the list row, observations[] (observed_at, acquisition_route; capped, with observations_total) on the detail. revisions[] lists retained content versions by version_id, content_hash and observed_at, and version_count counts them; the private body of each version is never served. change_type is revised when a different content hash was observed after first_seen, else newly_discovered when the publisher date is more than two days before first_seen (Beijing date), else new. Rolling out: the change feed, the document observation fields and revisions[] answer once database migration 043 is applied on a deployment. Before that, /api/changes and mode=changes answer HTTP 503 with code unavailable ("not yet enabled"), and list rows return the pending fields as null, named in unavailable_fields. Coming soon: the text of each historical version. [Indicator releases](/docs/data/indicators) and editorial topic versions are different objects. ## Fields | Field | Meaning | | --- | --- | | id | Record ID | | title | Title | | title_lang | Title language | | is_translated | Machine translation flag | | source | Publisher | | source_url | Original source | | original_title | Original title | | published_at | Publication date | | collected_at | First collected | | language | Original language | | topic | Topic | | text_status | Text availability | | publisher | Publisher | | acquisition_route | Acquisition route | | translated_title | Translated title | | first_seen | First seen | | last_seen | Last seen | | observation_count | Observations | | change_type | Change type | | version_count | Retained versions | | last_revised_at | Last changed body observed | | content_hash | Hash of the current extracted text | --- Source: https://china-snapshot.com/docs/data/public-attention.md # Public attention Sampled hot-list observations, with source-specific ranks and heat. ## Unit and identity Each row is one list appearance. id identifies that retained row; item_key groups source plus normalized topic wording across observations. Rank prefixes are removed before that key is computed. Translation changes do not define a new topic when the original title is retained. ## Coverage limits The retained hot lists include Baidu, Weibo and Tencent WeChat. Collection gaps and changing source coverage affect comparisons. Source URLs may point to platform search results. Heat is publisher-specific and WeChat heat is unavailable. ## Fields | Field | Meaning | | --- | --- | | id | Record ID | | title | Title | | title_lang | Title language | | is_translated | Machine translation flag | | source | Publisher | | source_url | Original source | | original_title | Original title | | observed_at | Observed at | | rank | Observed rank | | heat | Publisher heat score | | platform | Platform | | board | Board | | collected_at | Collected at | | original_topic | Original topic | | translated_topic | Translated topic | | item_key | Stable source and normalized topic key | --- Source: https://china-snapshot.com/docs/data/indicators.md # Indicators Raw captures and deduplicated release views answer different questions. ## Raw mode The default is one retained observation per capture. A flat sequence may be repeated collection of the same release. Some legacy observations lack a source URL. Blank legacy rows and explicitly stale observations are excluded by the public view. | Field | Meaning | | --- | --- | | id | Record ID | | title | Title | | title_lang | Title language | | is_translated | Machine translation flag | | source | Publisher | | source_url | Original source | | period | Reference period | | value | Published value | | numeric_value | Numeric value | | unit | Unit | | collected_at | Collected at | | series_id | Series ID | | frequency | Frequency | | release_date | Release date (Beijing) | | revision_status | Revision status | ## Release mode Request dataset=indicators&mode=release for one row per series_id and period. Latest value and first_value let you inspect a captured revision. revised is true for changed monthly or policy prints and null for daily quotes. release_date is the first-seen Beijing date. | Field | Meaning | | --- | --- | | id | Record ID | | title | Title | | title_lang | Title language | | is_translated | Machine translation flag | | source | Publisher | | source_url | Original source | | series_id | Series ID | | frequency | Frequency | | period | Reference period | | value | Latest value | | numeric_value | Latest numeric value | | unit | Unit | | previous_period | Previous period | | previous_value | Previous value | | previous_numeric_value | Previous numeric value | | first_value | First captured value | | revised | Changed after first capture | | release_date | First seen (Beijing date) | | first_seen_at | First captured at | | last_seen_at | Last captured at | | observations | Captures of this release | | collected_at | Collected at | | revision_status | Revision status | --- Source: https://china-snapshot.com/docs/data/coverage.md # Coverage and gaps Distinguish the advertised collection schedule from what was actually retained. ## Check the live coverage Use the [live coverage table](/coverage) and each response's coverage notes. The [endpoint catalog](/data/endpoints.json) describes feeds and access. Earliest retained dates are source-specific; do not infer them from the tier's maximum history window. ## Frequency A scheduled collection is an attempt, not proof of a successful observation. Compare actual collection timestamps and visible freshness information. A source that did not publish and a collector that failed are different situations. ## Missing intervals Preserve gaps rather than filling them with zeros or forward-filled claims of new observations. Document which sources and date ranges your query covered. Publication date can differ from the first date the archive saw a document. ## Historical completeness The archive contains what was retained. A longer paid history allowance cannot recover a publication that was never collected. Use source-specific earliest timestamps and observed intervals when estimating completeness. --- Source: https://china-snapshot.com/docs/data/glossary.md # Field glossary Clocks, identifiers, nulls and the meaning of a page response. ## Shared terms | Field | Meaning | | --- | --- | | id | Public retained-record identity; use the value returned | | source_url | Retained provenance link, possibly null or a platform search URL | | published_at | Publisher date where retained; not first collection | | collected_at | Collection clock, with dataset-specific semantics | | observed_at | Time a social list appearance was observed | | period | Economic reference period | | as_of | Query snapshot timestamp, not a query parameter | | row_count | Count in this page, not a matching total | | next_cursor | Opaque continuation for the same query; on /api/changes it equals next_since | | next_since | Signed change-feed checkpoint from /api/changes or mode=changes; pass it back as since | | first_seen / last_seen | First and latest collection of a document's source URL | | observation_count | How many times that URL was collected; a repeat is not a new document | | change_type / change_at | new, newly_discovered or revised, and when (UTC) on the change feed | | unavailable_fields | Fields this deployment cannot fill yet; they are null, not empty | | title_lang / is_translated | Presentation language and machine-translation flag | ## Missing values Null means the field is not available or not applicable under that dataset's contract. It is not zero, an empty measurement, no revision, or permission to infer a fact. An omitted field may be excluded by your fields projection. Read the field list and coverage notes before normalizing. ## Original and translated titles Preserve both when present. Use original wording for source verification and the translation for discovery. Translations are not publisher-authored English unless the source itself is English. --- Source: https://china-snapshot.com/docs/data/schema-versioning.md # Schema versioning Payload schemas and API transport versions are separate contracts. ## Payload versions Read schema_version where the dataset or feed includes it. The standard collector feed convention bumps schema_version on breaking shape changes. Do not assume every endpoint has the same envelope; the OpenAPI operation describes that endpoint. ## Research transport Research and MCP expose X-Api-Version. Their deprecation policy uses Deprecation and Sunset headers when a retirement is announced. See [changelog and deprecation](/docs/resources/changelog). ## Client behavior Ignore additive unknown fields, validate required fields, preserve null and distinguish an empty successful page from an error. Re-check the OpenAPI document when a schema or API version changes. --- Source: https://china-snapshot.com/docs/api/overview.md # API reference Generated from the repository OpenAPI contract. Every documented operation is included. ## Contract and coverage The operation pages below are generated from [OpenAPI](/openapi.json), including parameters, request bodies, response schemas, documented examples and errors. The spec is the generation boundary, not a claim that every internal application route is public API. GET /api/usage is described in the plans guide but is not yet in this spec. ## Operations | Method | Path | Operation | | --- | --- | --- | | GET | /api/datasets | [browsePublicDatasets](/docs/api/browse-public-datasets) | | GET | /api/changes | [getChanges](/docs/api/get-changes) | | GET | /api/series | [listSeries](/docs/api/list-series) | | GET | /api/series/{series_id} | [getSeries](/docs/api/get-series) | | GET | /api/indicators/{slug}.json | [getIndicator](/docs/api/get-indicator) | | GET | /data/topics/index.json | [listPublicTopics](/docs/api/list-public-topics) | | GET | /data/topics/{slug}.json | [getPublicTopic](/docs/api/get-public-topic) | | GET | /data/topics/{slug}.md | [getPublicTopicMarkdown](/docs/api/get-public-topic-markdown) | | GET | /data/topics/{slug}/v{version}.json | [getPublicTopicRevision](/docs/api/get-public-topic-revision) | | GET | /data/topics/{slug}/v{version}.md | [getPublicTopicRevisionMarkdown](/docs/api/get-public-topic-revision-markdown) | | GET | /data/endpoints.json | [getEndpointsCatalog](/docs/api/get-endpoints-catalog) | | GET | /data/daily_digest.json | [getDailyDigest](/docs/api/get-daily-digest) | | GET | /data/baselines.json | [getBaselines](/docs/api/get-baselines) | | GET | /data/forecasts/index.json | [getForecastBoard](/docs/api/get-forecast-board) | | GET | /data/forecasts/track_record.json | [getForecastTrackRecord](/docs/api/get-forecast-track-record) | | GET | /data/signal-packet.json | [getSignalPacket](/docs/api/get-signal-packet) | | GET | /data/digest.md | [getDailyDigestMarkdown](/docs/api/get-daily-digest-markdown) | | GET | /.well-known/mcp.json | [getMcpManifest](/docs/api/get-mcp-manifest) | | GET | /api/{path} | [apiNotFound](/docs/api/api-not-found) | | POST | /api/{path} | [apiNotFoundPost](/docs/api/api-not-found-post) | | GET | /api/research | [researchDiscovery](/docs/api/research-discovery) | | POST | /api/research | [researchInvoke](/docs/api/research-invoke) | | GET | /api/mcp | [mcpDiscovery](/docs/api/mcp-discovery) | | POST | /api/mcp | [mcpJsonRpc](/docs/api/mcp-json-rpc) | | POST | /api/key_signup | [requestApiKey](/docs/api/request-api-key) | | POST | /api/keys/replace | [replaceApiKey](/docs/api/replace-api-key) | | POST | /api/keys/revoke | [revokeApiKey](/docs/api/revoke-api-key) | | POST | /api/checkout/pro | [startProCheckout](/docs/api/start-pro-checkout) | | GET | /developers | [developerPortal](/docs/api/developer-portal) | | GET | /openapi.json | [getOpenApi](/docs/api/get-open-api) | ## Errors and retries Read the operation-specific status and body. Public datasets may use a string error; signup uses a structured error.code; MCP uses JSON-RPC errors. Do not assume one envelope. For read-only requests, honor Retry-After on 429, and use bounded backoff with jitter for transient 503 or transport failures. Fix invalid parameters and expired cursors before retrying 400. Resolve credentials or entitlement for 401/403. A missing record is not a retry loop. Do not automatically repeat signup or checkout after an ambiguous response. ## Examples versus schemas JSON marked as a schema or operation contract is not an executable request. Response examples are copied only when supplied by OpenAPI; where it provides none, the page says so rather than inventing a production response. --- Source: https://china-snapshot.com/docs/api/browse-public-datasets.md # Browse and export retained China records browsePublicDatasets · GET /api/datasets ## Endpoint `GET /api/datasets` Public filtered read-only archive access, without sign-in or inference calls. Ordered by collection timestamp descending then stable ID descending. Cursor pages keep a fixed query cutoff and insertion high-water mark; document metadata remains a current projection, not an immutable historical edition. Social rows are repeated list observations, not distinct posts or public-opinion measures. Economic collection dates differ from reference periods. Original extraction is released only under reviewed per-document rights tied to the exact original text; other records link to the publisher. WeChat heat is null because historical raw_score values were ordering positions. Unknown and repeated query parameters are rejected. GET and OPTIONS permit cross-origin access. next_cursor is null on the last page. Order is collection timestamp descending, then id descending, so a page is stable for the lifetime of its cursor. row_count is the rows in this response, not a total. ## Authentication ```json [] ``` An empty security requirement means this operation does not declare authentication. Read operation notes for entitlement and protocol-method requirements. ## Parameters | Name | In | Required | Description | | --- | --- | --- | --- | | dataset | query | No | Choose the retained source documents, sampled social hot-list observations, or indicators. | | mode | query | No | raw: one row per record (documents) or per collection run (social, indicators). release (indicators): one row per (series_id, period), with previous value, first and latest capture, and a revised flag. changes (documents): new and revised events after since=, oldest first; GET /api/changes is the contract route for this feed. topics (social): one row per (platform, topic) within the from/to window (default 30 days, max 92). | | watch | query | No | Alias of topic (the older name). Passing both with different values is invalid_parameter. | | topic | query | No | Reviewed bilingual topic filter (documents, social): a case-insensitive title match on one of the listed terms, nothing inferred. export-controls is conservative: every term names export control or dual-use items itself. Terms and review dates: list_watchlists (MCP) or api/_watchlists.py. | | since | query | No | mode=changes only, and required there: an ISO-8601 instant with a timezone (UTC recommended, e.g. 2026-10-29T00:00:00Z), inclusive, compared with the event time (change_at / event_at, our collection clock). To continue, pass the previous next_since back as since. A malformed or altered next_since token is invalid_cursor. | | series | query | No | Indicators only. Exact series_id, e.g. cpi-yoy. | | q | query | No | Literal case-insensitive substring of displayed/original title or publisher. No semantic search, query translation, or full-body search. | | source | query | No | Case-insensitive exact publisher label; use the response sources list. | | from | query | No | Inclusive start day, YYYY-MM-DD, read as 00:00:00 UTC. It filters the COLLECTION timestamp, not the publication date: documents use first_seen (when we first collected the record), social observations use the captured_at time of the list read, indicators use captured_at (mode=release: first capture). published_at and the economic reference period `period` are returned but cannot be filtered. Years 2000 through 2099. Must not be after `to`. A tier's history window (x-api-tiers history_days) raises the effective start; a `from` before it is rejected with invalid_parameter. | | to | query | No | Inclusive end day, YYYY-MM-DD, UTC: records collected before 00:00:00 UTC of the following day are included. Same timestamp as `from` (first_seen / captured_at, not publication date or reference period). | | limit | query | No | Rows per page, 1 to the caller's tier ceiling, default 50. Ceilings come from api/tiers.py: Anonymous 100; Free key 500; Pro 5,000; Team 5,000. A larger value returns invalid_parameter, never a silently smaller page. max_page_size in the response repeats the ceiling. | | cursor | query | No | Pass the previous page's next_cursor unchanged, with the same dataset, q, source, from, to, text, mode and series; limit, fields and format may change. The cursor fixes the query cutoff (as_of) and a row high-water mark taken on the first page, so rows collected later never appear mid-walk and no row repeats or is skipped. It is signed, opaque and expires 24 hours after the FIRST page (not after each page): an expired, altered or mismatched cursor returns invalid_cursor, and the fix is to restart without a cursor. Document metadata is a current projection, so a row can change between a walk's pages even though the set of rows cannot. | | change_type | query | No | Documents only (mode=raw): new, newly_discovered (published more than 2 days, Beijing date, before first_seen) or revised (a changed body observed after first_seen). Field meanings: /data/data-contract.json. | | text | query | No | Documents only: select records with individually approved original text. Omit to browse all document metadata. | | fields | query | No | Comma-separated unique field keys from fields in this dataset response. Defaults to all available fields. original_text/text_truncated/rights_basis_url require a documents id request. | | format | query | No | CSV downloads one bounded page as UTF-8 with BOM and formula-safe string cells, never the entire matching archive. The selected fields are followed by a computed citation column. | | id | query | No | Stable record ID returned by this dataset. Cannot combine with q/source/from/to/text/cursor. Document detail includes only individually approved original text, capped at 120,000 characters, otherwise null. | ## Responses and errors | HTTP status | Meaning | | --- | --- | | 200 | A real retained-archive page or single record; no fabricated totals or static fallback. | | 400 | Invalid query, field, date or cursor. Body carries `code`: see x-error-codes. | | 404 | Requested record is absent. Body carries `code`: see x-error-codes. | | 405 | Use GET. Body carries `code`: see x-error-codes. | | 429 | Rate limit exceeded (60 requests per minute). Body carries `code`: see x-error-codes. | | 503 | Retained archive or request protection unavailable; retry later. Body carries `code`: see x-error-codes. | ## Response examples No response example is supplied by OpenAPI for this operation. The complete response schemas are preserved below; use an actual response for observed values. ## Runnable request ```curl curl --fail-with-body --silent --show-error --max-time 30 "https://china-snapshot.com/api/datasets?dataset=documents&limit=1" ``` ```python import json from urllib.request import Request, urlopen request = Request('https://china-snapshot.com/api/datasets?dataset=documents&limit=1') with urlopen(request, timeout=30) as response: data = json.load(response) print(json.dumps(data, ensure_ascii=False)) ``` ```typescript const response = await fetch("https://china-snapshot.com/api/datasets?dataset=documents&limit=1"); if (!response.ok) throw new Error(`HTTP ${response.status}`); console.log(await response.json()); ``` ## Retry guidance See [errors and retries](/docs/api/overview#errors-and-retries). Honor operation-specific response headers. A POST may create state; do not replay it automatically. ## Complete operation contract The expanded contract below includes all parameter constraints, response headers, schemas and examples from OpenAPI. ```json { "path": "/api/datasets", "method": "GET", "parameters": [ { "name": "dataset", "in": "query", "required": false, "schema": { "type": "string", "enum": [ "documents", "social", "indicators" ], "default": "documents" }, "description": "Choose the retained source documents, sampled social hot-list observations, or indicators." }, { "name": "mode", "in": "query", "required": false, "schema": { "type": "string", "enum": [ "raw", "release", "changes", "topics" ], "default": "raw" }, "description": "raw: one row per record (documents) or per collection run (social, indicators). release (indicators): one row per (series_id, period), with previous value, first and latest capture, and a revised flag. changes (documents): new and revised events after since=, oldest first; GET /api/changes is the contract route for this feed. topics (social): one row per (platform, topic) within the from/to window (default 30 days, max 92)." }, { "name": "watch", "in": "query", "required": false, "schema": { "type": "string", "enum": [ "data-security", "export-controls", "property-support", "semiconductors" ] }, "description": "Alias of topic (the older name). Passing both with different values is invalid_parameter." }, { "name": "topic", "in": "query", "required": false, "schema": { "type": "string", "enum": [ "data-security", "export-controls", "property-support", "semiconductors" ] }, "description": "Reviewed bilingual topic filter (documents, social): a case-insensitive title match on one of the listed terms, nothing inferred. export-controls is conservative: every term names export control or dual-use items itself. Terms and review dates: list_watchlists (MCP) or api/_watchlists.py." }, { "name": "since", "in": "query", "required": false, "schema": { "type": "string" }, "description": "mode=changes only, and required there: an ISO-8601 instant with a timezone (UTC recommended, e.g. 2026-10-29T00:00:00Z), inclusive, compared with the event time (change_at / event_at, our collection clock). To continue, pass the previous next_since back as since. A malformed or altered next_since token is invalid_cursor." }, { "name": "series", "in": "query", "required": false, "schema": { "type": "string", "pattern": "^[a-z0-9]+(-[a-z0-9]+)*$", "maxLength": 120 }, "description": "Indicators only. Exact series_id, e.g. cpi-yoy." }, { "name": "q", "in": "query", "required": false, "schema": { "type": "string", "maxLength": 180 }, "description": "Literal case-insensitive substring of displayed/original title or publisher. No semantic search, query translation, or full-body search." }, { "name": "source", "in": "query", "required": false, "schema": { "type": "string", "maxLength": 180 }, "description": "Case-insensitive exact publisher label; use the response sources list." }, { "name": "from", "in": "query", "required": false, "schema": { "type": "string", "format": "date" }, "description": "Inclusive start day, YYYY-MM-DD, read as 00:00:00 UTC. It filters the COLLECTION timestamp, not the publication date: documents use first_seen (when we first collected the record), social observations use the captured_at time of the list read, indicators use captured_at (mode=release: first capture). published_at and the economic reference period `period` are returned but cannot be filtered. Years 2000 through 2099. Must not be after `to`. A tier's history window (x-api-tiers history_days) raises the effective start; a `from` before it is rejected with invalid_parameter." }, { "name": "to", "in": "query", "required": false, "schema": { "type": "string", "format": "date" }, "description": "Inclusive end day, YYYY-MM-DD, UTC: records collected before 00:00:00 UTC of the following day are included. Same timestamp as `from` (first_seen / captured_at, not publication date or reference period)." }, { "name": "limit", "in": "query", "required": false, "schema": { "type": "integer", "minimum": 1, "maximum": 5000, "default": 50 }, "description": "Rows per page, 1 to the caller's tier ceiling, default 50. Ceilings come from api/tiers.py: Anonymous 100; Free key 500; Pro 5,000; Team 5,000. A larger value returns invalid_parameter, never a silently smaller page. max_page_size in the response repeats the ceiling." }, { "name": "cursor", "in": "query", "required": false, "schema": { "type": "string" }, "description": "Pass the previous page's next_cursor unchanged, with the same dataset, q, source, from, to, text, mode and series; limit, fields and format may change. The cursor fixes the query cutoff (as_of) and a row high-water mark taken on the first page, so rows collected later never appear mid-walk and no row repeats or is skipped. It is signed, opaque and expires 24 hours after the FIRST page (not after each page): an expired, altered or mismatched cursor returns invalid_cursor, and the fix is to restart without a cursor. Document metadata is a current projection, so a row can change between a walk's pages even though the set of rows cannot." }, { "name": "change_type", "in": "query", "required": false, "schema": { "type": "string", "enum": [ "new", "newly_discovered", "revised" ] }, "description": "Documents only (mode=raw): new, newly_discovered (published more than 2 days, Beijing date, before first_seen) or revised (a changed body observed after first_seen). Field meanings: /data/data-contract.json." }, { "name": "text", "in": "query", "required": false, "schema": { "type": "string", "enum": [ "available" ] }, "description": "Documents only: select records with individually approved original text. Omit to browse all document metadata." }, { "name": "fields", "in": "query", "required": false, "schema": { "type": "string" }, "description": "Comma-separated unique field keys from fields in this dataset response. Defaults to all available fields. original_text/text_truncated/rights_basis_url require a documents id request." }, { "name": "format", "in": "query", "required": false, "schema": { "type": "string", "enum": [ "json", "csv" ], "default": "json" }, "description": "CSV downloads one bounded page as UTF-8 with BOM and formula-safe string cells, never the entire matching archive. The selected fields are followed by a computed citation column." }, { "name": "id", "in": "query", "required": false, "schema": { "type": "string", "pattern": "^(documents|social|indicators):[1-9][0-9]*$" }, "description": "Stable record ID returned by this dataset. Cannot combine with q/source/from/to/text/cursor. Document detail includes only individually approved original text, capped at 120,000 characters, otherwise null." } ], "operationId": "browsePublicDatasets", "summary": "Browse and export retained China records", "description": "Public filtered read-only archive access, without sign-in or inference calls. Ordered by collection timestamp descending then stable ID descending. Cursor pages keep a fixed query cutoff and insertion high-water mark; document metadata remains a current projection, not an immutable historical edition. Social rows are repeated list observations, not distinct posts or public-opinion measures. Economic collection dates differ from reference periods. Original extraction is released only under reviewed per-document rights tied to the exact original text; other records link to the publisher. WeChat heat is null because historical raw_score values were ordering positions. Unknown and repeated query parameters are rejected. GET and OPTIONS permit cross-origin access. next_cursor is null on the last page. Order is collection timestamp descending, then id descending, so a page is stable for the lifetime of its cursor. row_count is the rows in this response, not a total.", "tags": [ "data", "Find records", "Retrieve a record", "Inspect observations", "Export" ], "security": [], "responses": { "200": { "description": "A real retained-archive page or single record; no fabricated totals or static fallback.", "headers": { "X-Dataset-Row-Count": { "schema": { "type": "integer" }, "description": "Rows in this response." }, "X-Dataset-Next-Cursor": { "schema": { "type": "string" }, "description": "Continuation cursor; empty at the end. Also exposed to cross-origin browsers." }, "X-Dataset-Next-Since": { "schema": { "type": "string" }, "description": "mode=changes and /api/changes: the next_since token; empty otherwise." }, "X-Dataset-Export-Scope": { "schema": { "enum": [ "page", "record" ] }, "description": "Explicit export scope." } }, "content": { "application/json": { "schema": { "type": "object", "required": [ "schema_version", "dataset", "mode", "as_of", "items", "row_count", "next_cursor", "sources", "sources_truncated", "coverage", "fields", "export_scope", "max_page_size", "links" ], "properties": { "schema_version": { "const": 1 }, "dataset": { "type": "string", "enum": [ "documents", "social", "indicators" ] }, "mode": { "type": "string", "enum": [ "raw", "release", "changes", "topics" ] }, "as_of": { "type": "string", "format": "date-time", "description": "Query cutoff fixed on the first page; not a claim about source freshness." }, "items": { "type": "array", "maxItems": 5000, "items": { "anyOf": [ { "type": "object", "additionalProperties": false, "properties": { "id": { "type": "string", "pattern": "^documents:[1-9][0-9]*$", "description": "Record ID" }, "title": { "type": [ "string", "null" ], "description": "Title" }, "title_lang": { "type": [ "string", "null" ], "description": "Title language" }, "is_translated": { "type": [ "boolean", "null" ], "description": "Title is a machine translation" }, "source": { "type": [ "string", "null" ], "description": "Publisher" }, "source_url": { "type": [ "string", "null" ], "description": "Original source" }, "original_title": { "type": [ "string", "null" ], "description": "Original title" }, "published_at": { "type": [ "string", "null" ], "description": "Publication date", "format": "date" }, "collected_at": { "type": [ "string", "null" ], "description": "First collected", "format": "date-time" }, "language": { "type": [ "string", "null" ], "description": "Original language" }, "topic": { "type": [ "string", "null" ], "description": "Topic" }, "text_status": { "type": [ "string", "null" ], "description": "Text availability", "enum": [ "available", "source_only", "not_extracted" ] }, "publisher": { "type": [ "string", "null" ], "description": "Publisher" }, "acquisition_route": { "type": [ "string", "null" ], "description": "Acquisition route" }, "translated_title": { "type": [ "string", "null" ], "description": "Translated title" }, "first_seen": { "type": [ "string", "null" ], "description": "First seen", "format": "date-time" }, "last_seen": { "type": [ "string", "null" ], "description": "Last seen", "format": "date-time" }, "observation_count": { "type": [ "integer", "null" ], "description": "Observations" }, "change_type": { "type": [ "string", "null" ], "description": "Change type", "enum": [ "new", "newly_discovered", "revised", null ] }, "version_count": { "type": [ "integer", "null" ], "description": "Retained versions" }, "last_revised_at": { "type": [ "string", "null" ], "description": "Last changed body observed", "format": "date-time" }, "content_hash": { "type": [ "string", "null" ], "description": "Hash of the current extracted text" }, "original_text": { "type": [ "string", "null" ], "description": "Original-language extraction" }, "text_truncated": { "type": [ "boolean", "null" ], "description": "Text truncated" }, "rights_basis_url": { "type": [ "string", "null" ], "description": "Republication basis" }, "observations": { "type": [ "array", "null" ], "description": "Collections of this document" }, "observations_total": { "type": [ "integer", "null" ], "description": "Total collections" }, "revisions": { "type": [ "array", "null" ], "description": "Retained content versions" } }, "description": "documents: selected fields only; null means not retained or not available." }, { "type": "object", "additionalProperties": false, "properties": { "id": { "type": "string", "pattern": "^documents:[1-9][0-9]*$", "description": "Record ID" }, "title": { "type": [ "string", "null" ], "description": "Title" }, "title_lang": { "type": [ "string", "null" ], "description": "Title language" }, "is_translated": { "type": [ "boolean", "null" ], "description": "Title is a machine translation" }, "source": { "type": [ "string", "null" ], "description": "Publisher" }, "source_url": { "type": [ "string", "null" ], "description": "Original source" }, "change_type": { "type": [ "string", "null" ], "description": "Change type", "enum": [ "new", "newly_discovered", "revised", null ] }, "change_at": { "type": [ "string", "null" ], "description": "Change time (UTC)", "format": "date-time" }, "event": { "type": [ "string", "null" ], "description": "Event" }, "event_at": { "type": [ "string", "null" ], "description": "Event time", "format": "date-time" }, "original_title": { "type": [ "string", "null" ], "description": "Original title" }, "published_at": { "type": [ "string", "null" ], "description": "Publication date", "format": "date" }, "first_seen_at": { "type": [ "string", "null" ], "description": "First collected", "format": "date-time" }, "content_hash": { "type": [ "string", "null" ], "description": "Hash of the extracted text at this event" }, "language": { "type": [ "string", "null" ], "description": "Original language" } }, "description": "document_changes: selected fields only; null means not retained or not available." }, { "type": "object", "additionalProperties": false, "properties": { "id": { "type": "string", "pattern": "^social_topics:[1-9][0-9]*$", "description": "Record ID" }, "title": { "type": [ "string", "null" ], "description": "Title" }, "title_lang": { "type": [ "string", "null" ], "description": "Title language" }, "is_translated": { "type": [ "boolean", "null" ], "description": "Title is a machine translation" }, "source": { "type": [ "string", "null" ], "description": "Publisher" }, "source_url": { "type": [ "string", "null" ], "description": "Original source" }, "topic": { "type": [ "string", "null" ], "description": "Topic" }, "original_title": { "type": [ "string", "null" ], "description": "Original title" }, "first_seen_at": { "type": [ "string", "null" ], "description": "First seen in window", "format": "date-time" }, "last_seen_at": { "type": [ "string", "null" ], "description": "Last seen in window", "format": "date-time" }, "observations": { "type": [ "integer", "null" ], "description": "List appearances in window" }, "best_rank": { "type": [ "integer", "null" ], "description": "Best rank in window" }, "item_key": { "type": [ "string", "null" ], "description": "Stable item key" } }, "description": "social_topics: selected fields only; null means not retained or not available." }, { "type": "object", "additionalProperties": false, "properties": { "id": { "type": "string", "pattern": "^social:[1-9][0-9]*$", "description": "Record ID" }, "title": { "type": [ "string", "null" ], "description": "Title" }, "title_lang": { "type": [ "string", "null" ], "description": "Title language" }, "is_translated": { "type": [ "boolean", "null" ], "description": "Title is a machine translation" }, "source": { "type": [ "string", "null" ], "description": "Publisher" }, "source_url": { "type": [ "string", "null" ], "description": "Original source" }, "original_title": { "type": [ "string", "null" ], "description": "Original title" }, "observed_at": { "type": [ "string", "null" ], "description": "Observed at", "format": "date-time" }, "rank": { "type": [ "integer", "null" ], "description": "Observed rank" }, "heat": { "type": [ "string", "null" ], "description": "Publisher heat score" }, "platform": { "type": [ "string", "null" ], "description": "Platform" }, "board": { "type": [ "string", "null" ], "description": "Board" }, "collected_at": { "type": [ "string", "null" ], "description": "Collected at", "format": "date-time" }, "original_topic": { "type": [ "string", "null" ], "description": "Original topic" }, "translated_topic": { "type": [ "string", "null" ], "description": "Translated topic" }, "item_key": { "type": [ "string", "null" ], "description": "Stable item key" } }, "description": "social: selected fields only; null means not retained or not available." }, { "type": "object", "additionalProperties": false, "properties": { "id": { "type": "string", "pattern": "^indicators:[1-9][0-9]*$", "description": "Record ID" }, "title": { "type": [ "string", "null" ], "description": "Title" }, "title_lang": { "type": [ "string", "null" ], "description": "Title language" }, "is_translated": { "type": [ "boolean", "null" ], "description": "Title is a machine translation" }, "source": { "type": [ "string", "null" ], "description": "Publisher" }, "source_url": { "type": [ "string", "null" ], "description": "Original source" }, "period": { "type": [ "string", "null" ], "description": "Reference period" }, "value": { "type": [ "string", "null" ], "description": "Published value" }, "numeric_value": { "type": [ "number", "null" ], "description": "Numeric value" }, "unit": { "type": [ "string", "null" ], "description": "Unit" }, "collected_at": { "type": [ "string", "null" ], "description": "Collected at", "format": "date-time" }, "series_id": { "type": [ "string", "null" ], "description": "Series ID" }, "frequency": { "type": [ "string", "null" ], "description": "Frequency" }, "release_date": { "type": [ "string", "null" ], "description": "Release date (Beijing)", "format": "date" }, "revision_status": { "type": [ "string", "null" ], "description": "Revision status", "enum": [ "first_release", "repeat_collection", "revised", "unknown", null ] } }, "description": "indicators: selected fields only; null means not retained or not available." }, { "type": "object", "additionalProperties": false, "properties": { "id": { "type": "string", "pattern": "^indicator_releases:[1-9][0-9]*$", "description": "Record ID" }, "title": { "type": [ "string", "null" ], "description": "Title" }, "title_lang": { "type": [ "string", "null" ], "description": "Title language" }, "is_translated": { "type": [ "boolean", "null" ], "description": "Title is a machine translation" }, "source": { "type": [ "string", "null" ], "description": "Publisher" }, "source_url": { "type": [ "string", "null" ], "description": "Original source" }, "series_id": { "type": [ "string", "null" ], "description": "Series ID" }, "frequency": { "type": [ "string", "null" ], "description": "Frequency" }, "period": { "type": [ "string", "null" ], "description": "Reference period" }, "value": { "type": [ "string", "null" ], "description": "Latest value" }, "numeric_value": { "type": [ "number", "null" ], "description": "Latest numeric value" }, "unit": { "type": [ "string", "null" ], "description": "Unit" }, "previous_period": { "type": [ "string", "null" ], "description": "Previous period" }, "previous_value": { "type": [ "string", "null" ], "description": "Previous value" }, "previous_numeric_value": { "type": [ "number", "null" ], "description": "Previous numeric value" }, "first_value": { "type": [ "string", "null" ], "description": "First captured value" }, "revised": { "type": [ "boolean", "null" ], "description": "Changed after first capture" }, "release_date": { "type": [ "string", "null" ], "description": "First seen (Beijing date)", "format": "date" }, "first_seen_at": { "type": [ "string", "null" ], "description": "First captured at", "format": "date-time" }, "last_seen_at": { "type": [ "string", "null" ], "description": "Last captured at", "format": "date-time" }, "observations": { "type": [ "integer", "null" ], "description": "Captures of this release" }, "collected_at": { "type": [ "string", "null" ], "description": "Collected at", "format": "date-time" }, "revision_status": { "type": [ "string", "null" ], "description": "Revision status", "enum": [ "first_release", "repeat_collection", "revised", "unknown", null ] } }, "description": "indicator_releases: selected fields only; null means not retained or not available." } ] } }, "row_count": { "type": "integer", "minimum": 0, "maximum": 5000, "description": "Rows in this response, not total matching rows." }, "next_cursor": { "type": [ "string", "null" ], "description": "Opaque signed continuation, or null at the end. Valid for 24 hours and tied to dataset/q/source/from/to/text." }, "sources": { "type": "array", "maxItems": 500, "items": { "type": "string" }, "description": "Observed publisher labels in this public dataset, not monitored-channel configuration." }, "sources_truncated": { "type": "boolean" }, "coverage": { "type": "object", "required": [ "kind", "unit", "date_field", "date_timezone", "order", "notes" ], "properties": { "kind": { "const": "retained_archive" }, "unit": { "enum": [ "documents", "document_events", "hot_list_observations", "platform_topics", "indicator_observations", "indicator_releases" ] }, "date_field": { "enum": [ "collected_at", "observed_at", "first_seen_at", "last_seen_at", "event_at" ] }, "date_timezone": { "const": "UTC" }, "order": { "enum": [ "date_desc_id_desc", "event_at_asc" ] }, "notes": { "type": "array", "items": { "type": "string" } } } }, "fields": { "type": "array", "items": { "type": "object", "required": [ "key", "label" ], "properties": { "key": { "type": "string" }, "label": { "type": "string" } } } }, "export_scope": { "enum": [ "page", "record" ] }, "max_page_size": { "type": "integer", "minimum": 1, "maximum": 5000, "description": "The caller's tier ceiling for limit (see x-api-tiers)." }, "links": { "type": "object", "required": [ "share_url", "csv_url", "curl" ], "description": "List responses only (absent on an id request). The same query, first page, no cursor.", "properties": { "share_url": { "type": "string", "format": "uri", "description": "An /explore URL that reopens this query (topic= names the topic filter)." }, "csv_url": { "type": "string", "format": "uri", "description": "This request as format=csv. The CSV adds a computed citation column (publisher, original title, publication date, source_url, record id and permalink)." }, "api_url": { "type": "string", "format": "uri", "description": "This request as JSON." }, "curl": { "type": "string", "description": "The equivalent curl command." } } } } } }, "text/csv": { "schema": { "type": "string" } } } }, "400": { "description": "Invalid query, field, date or cursor. Body carries `code`: see x-error-codes.", "content": { "application/json": { "schema": { "type": "object", "required": [ "error", "code" ], "properties": { "error": { "type": "string" }, "retry_after": { "type": "integer" }, "code": { "type": "string", "enum": [ "invalid_parameter", "invalid_cursor", "unauthorized", "forbidden", "not_found", "method_not_allowed", "rate_limited", "server_error", "unavailable" ], "description": "Stable machine code; see x-error-codes." } } } } } }, "404": { "description": "Requested record is absent. Body carries `code`: see x-error-codes.", "content": { "application/json": { "schema": { "type": "object", "required": [ "error", "code" ], "properties": { "error": { "type": "string" }, "retry_after": { "type": "integer" }, "code": { "type": "string", "enum": [ "invalid_parameter", "invalid_cursor", "unauthorized", "forbidden", "not_found", "method_not_allowed", "rate_limited", "server_error", "unavailable" ], "description": "Stable machine code; see x-error-codes." } } } } } }, "405": { "description": "Use GET. Body carries `code`: see x-error-codes.", "content": { "application/json": { "schema": { "type": "object", "required": [ "error", "code" ], "properties": { "error": { "type": "string" }, "retry_after": { "type": "integer" }, "code": { "type": "string", "enum": [ "invalid_parameter", "invalid_cursor", "unauthorized", "forbidden", "not_found", "method_not_allowed", "rate_limited", "server_error", "unavailable" ], "description": "Stable machine code; see x-error-codes." } } } } } }, "429": { "description": "Rate limit exceeded (60 requests per minute). Body carries `code`: see x-error-codes.", "content": { "application/json": { "schema": { "type": "object", "required": [ "error", "code" ], "properties": { "error": { "type": "string" }, "retry_after": { "type": "integer" }, "code": { "type": "string", "enum": [ "invalid_parameter", "invalid_cursor", "unauthorized", "forbidden", "not_found", "method_not_allowed", "rate_limited", "server_error", "unavailable" ], "description": "Stable machine code; see x-error-codes." } } } } }, "headers": { "Retry-After": { "schema": { "type": "integer" }, "description": "Seconds until retry; also sends RateLimit-* and X-RateLimit-* headers." } } }, "503": { "description": "Retained archive or request protection unavailable; retry later. Body carries `code`: see x-error-codes.", "content": { "application/json": { "schema": { "type": "object", "required": [ "error", "code" ], "properties": { "error": { "type": "string" }, "retry_after": { "type": "integer" }, "code": { "type": "string", "enum": [ "invalid_parameter", "invalid_cursor", "unauthorized", "forbidden", "not_found", "method_not_allowed", "rate_limited", "server_error", "unavailable" ], "description": "Stable machine code; see x-error-codes." } } } } } } }, "x-codeSamples": [ { "lang": "curl", "label": "curl", "source": "curl -s 'https://china-snapshot.com/api/datasets?dataset=documents&q=export&from=2026-10-01&limit=3'" }, { "lang": "curl", "label": "curl", "source": "curl -s 'https://china-snapshot.com/api/datasets?dataset=indicators&mode=release&series=cpi-yoy&limit=3'" }, { "lang": "curl", "label": "curl", "source": "curl -s -o social-page.csv 'https://china-snapshot.com/api/datasets?dataset=social&format=csv&limit=3'" } ], "x-pagination": { "cursor_lifetime_hours": 24, "cursor_expires": "24 hours after the first page", "end_of_results": "next_cursor is null" } } ``` --- Source: https://china-snapshot.com/docs/api/get-changes.md # Documents added or revised since a time getChanges · GET /api/changes ## Endpoint `GET /api/changes` Polling feed over the retained documents: what we newly collected (new, newly_discovered) or saw change (revised) after since, oldest first, with change_at in UTC. Changes from the last 10 minutes are held back so a slow collector commit is never skipped. Same rows as /api/datasets?dataset=documents&mode=changes, collapsed to one item per document. Anonymous, no key; tiers bound the page size and history window. ## Authentication ```json [] ``` An empty security requirement means this operation does not declare authentication. Read operation notes for entitlement and protocol-method requirements. ## Parameters | Name | In | Required | Description | | --- | --- | --- | --- | | dataset | query | No | Only documents has a change feed. | | since | query | No | Required on the first call (or pass cursor instead): an ISO-8601 instant with a timezone, UTC recommended (2026-10-29T00:00:00Z). Inclusive. Compared with change_at, the time of the change on our collection clock (first collection for new and newly_discovered, the observation of the changed body for revised), not publication time. Must lie inside the tier history window (x-api-tiers history_days). A next_since token is also accepted here. | | cursor | query | No | next_cursor (== next_since) from the previous page; do not combine with since. The walk resumes strictly after the last item returned, so no document repeats and none is skipped. A document that changes again later reappears once, at its new change. The token is signed; an altered or malformed one returns invalid_cursor. | | limit | query | No | Maximum rows in this page. Anonymous callers may ask for up to 100; keyed tiers up to their rows_per_page (x-api-tiers). | | source | query | No | Case-insensitive exact publisher label; use the response sources list. | | q | query | No | Literal case-insensitive substring of displayed/original title or publisher. No semantic search, query translation, or full-body search. | | watch | query | No | Alias of topic (the older name). Passing both with different values is invalid_parameter. | | topic | query | No | Reviewed bilingual topic filter (documents, social): a case-insensitive title match on one of the listed terms, nothing inferred. export-controls is conservative: every term names export control or dual-use items itself. Terms and review dates: list_watchlists (MCP) or api/_watchlists.py. | | fields | query | No | Comma-separated unique field keys of the change item; defaults to all. | | format | query | No | CSV downloads one bounded page as UTF-8 with BOM and formula-safe string cells, never the entire matching archive. The selected fields are followed by a computed citation column. | ## Responses and errors | HTTP status | Meaning | | --- | --- | | 200 | One page of changes, oldest first. Each document appears once, at its latest settled change; change_type is new, newly_discovered (published more than 2 days, Beijing dates, before we first collected it) or revised. | | 400 | Invalid query, field, date or cursor. Body carries `code`: see x-error-codes. | | 405 | Use GET. Body carries `code`: see x-error-codes. | | 429 | Rate limit exceeded (60 requests per minute). Body carries `code`: see x-error-codes. | | 503 | The change feed is not yet enabled on this deployment (database migration 043 pending: code unavailable, error "not yet enabled"), or the archive is unavailable; retry later. Body carries `code`: see x-error-codes. | ## Response examples No response example is supplied by OpenAPI for this operation. The complete response schemas are preserved below; use an actual response for observed values. ## Retry guidance See [errors and retries](/docs/api/overview#errors-and-retries). Honor operation-specific response headers. A POST may create state; do not replay it automatically. ## Complete operation contract The expanded contract below includes all parameter constraints, response headers, schemas and examples from OpenAPI. ```json { "path": "/api/changes", "method": "GET", "parameters": [ { "name": "dataset", "in": "query", "required": false, "schema": { "type": "string", "enum": [ "documents" ], "default": "documents" }, "description": "Only documents has a change feed." }, { "name": "since", "in": "query", "required": false, "schema": { "type": "string", "format": "date-time" }, "description": "Required on the first call (or pass cursor instead): an ISO-8601 instant with a timezone, UTC recommended (2026-10-29T00:00:00Z). Inclusive. Compared with change_at, the time of the change on our collection clock (first collection for new and newly_discovered, the observation of the changed body for revised), not publication time. Must lie inside the tier history window (x-api-tiers history_days). A next_since token is also accepted here." }, { "name": "cursor", "in": "query", "required": false, "schema": { "type": "string" }, "description": "next_cursor (== next_since) from the previous page; do not combine with since. The walk resumes strictly after the last item returned, so no document repeats and none is skipped. A document that changes again later reappears once, at its new change. The token is signed; an altered or malformed one returns invalid_cursor." }, { "name": "limit", "in": "query", "required": false, "schema": { "type": "integer", "minimum": 1, "maximum": 5000, "default": 50 }, "description": "Maximum rows in this page. Anonymous callers may ask for up to 100; keyed tiers up to their rows_per_page (x-api-tiers)." }, { "name": "source", "in": "query", "required": false, "schema": { "type": "string", "maxLength": 180 }, "description": "Case-insensitive exact publisher label; use the response sources list." }, { "name": "q", "in": "query", "required": false, "schema": { "type": "string", "maxLength": 180 }, "description": "Literal case-insensitive substring of displayed/original title or publisher. No semantic search, query translation, or full-body search." }, { "name": "watch", "in": "query", "required": false, "schema": { "type": "string", "enum": [ "data-security", "export-controls", "property-support", "semiconductors" ] }, "description": "Alias of topic (the older name). Passing both with different values is invalid_parameter." }, { "name": "topic", "in": "query", "required": false, "schema": { "type": "string", "enum": [ "data-security", "export-controls", "property-support", "semiconductors" ] }, "description": "Reviewed bilingual topic filter (documents, social): a case-insensitive title match on one of the listed terms, nothing inferred. export-controls is conservative: every term names export control or dual-use items itself. Terms and review dates: list_watchlists (MCP) or api/_watchlists.py." }, { "name": "fields", "in": "query", "required": false, "schema": { "type": "string" }, "description": "Comma-separated unique field keys of the change item; defaults to all." }, { "name": "format", "in": "query", "required": false, "schema": { "type": "string", "enum": [ "json", "csv" ], "default": "json" }, "description": "CSV downloads one bounded page as UTF-8 with BOM and formula-safe string cells, never the entire matching archive. The selected fields are followed by a computed citation column." } ], "operationId": "getChanges", "summary": "Documents added or revised since a time", "description": "Polling feed over the retained documents: what we newly collected (new, newly_discovered) or saw change (revised) after since, oldest first, with change_at in UTC. Changes from the last 10 minutes are held back so a slow collector commit is never skipped. Same rows as /api/datasets?dataset=documents&mode=changes, collapsed to one item per document. Anonymous, no key; tiers bound the page size and history window.", "tags": [ "data", "Retrieve changes", "Export" ], "security": [], "responses": { "200": { "description": "One page of changes, oldest first. Each document appears once, at its latest settled change; change_type is new, newly_discovered (published more than 2 days, Beijing dates, before we first collected it) or revised.", "headers": { "X-Dataset-Row-Count": { "schema": { "type": "integer" }, "description": "Rows in this response." }, "X-Dataset-Next-Cursor": { "schema": { "type": "string" }, "description": "Continuation cursor; empty at the end. Also exposed to cross-origin browsers." }, "X-Dataset-Next-Since": { "schema": { "type": "string" }, "description": "mode=changes and /api/changes: the next_since token; empty otherwise." }, "X-Dataset-Export-Scope": { "schema": { "enum": [ "page", "record" ] }, "description": "Explicit export scope." } }, "content": { "application/json": { "schema": { "type": "object", "required": [ "items", "row_count", "next_since", "next_cursor" ], "properties": { "schema_version": { "const": 1 }, "dataset": { "const": "documents" }, "mode": { "const": "changes" }, "endpoint": { "const": "/api/changes" }, "as_of": { "type": "string", "format": "date-time" }, "since": { "type": "string", "format": "date-time", "description": "The resolved start of this page (UTC)." }, "items": { "type": "array", "maxItems": 5000, "items": { "type": "object", "additionalProperties": false, "properties": { "id": { "type": "string", "pattern": "^documents:[1-9][0-9]*$", "description": "The document the change is about. Appears at most once per walk." }, "title": { "type": [ "string", "null" ], "description": "Title" }, "title_lang": { "type": [ "string", "null" ], "description": "Title language" }, "is_translated": { "type": [ "boolean", "null" ], "description": "Title is a machine translation" }, "source": { "type": [ "string", "null" ], "description": "Publisher" }, "source_url": { "type": [ "string", "null" ], "description": "Original source" }, "change_type": { "type": [ "string", "null" ], "description": "Change type", "enum": [ "new", "newly_discovered", "revised", null ] }, "change_at": { "type": [ "string", "null" ], "description": "Change time (UTC)", "format": "date-time" }, "event": { "type": [ "string", "null" ], "description": "Event" }, "event_at": { "type": [ "string", "null" ], "description": "Event time", "format": "date-time" }, "original_title": { "type": [ "string", "null" ], "description": "Original title" }, "published_at": { "type": [ "string", "null" ], "description": "Publication date", "format": "date" }, "first_seen_at": { "type": [ "string", "null" ], "description": "First collected", "format": "date-time" }, "content_hash": { "type": [ "string", "null" ], "description": "Hash of the extracted text at this event" }, "language": { "type": [ "string", "null" ], "description": "Original language" } }, "description": "One document at its latest settled event in the walk." } }, "row_count": { "type": "integer", "minimum": 0, "maximum": 5000 }, "next_since": { "type": "string", "description": "Signed resume position after the last item; never null. Pass back as since (or as cursor)." }, "next_cursor": { "type": "string", "description": "Same token as next_since, for clients that page with cursor=." }, "has_more": { "type": "boolean", "description": "More settled changes exist after this page." }, "change_types": { "type": "object", "description": "Counts of change_type in this page only.", "properties": { "new": { "type": "integer", "minimum": 0 }, "newly_discovered": { "type": "integer", "minimum": 0 }, "revised": { "type": "integer", "minimum": 0 } } }, "settle_minutes": { "type": "integer", "description": "Events newer than this are held back until settled." }, "coverage": { "type": "object", "required": [ "kind", "unit", "date_field", "date_timezone", "order", "notes" ], "properties": { "kind": { "const": "retained_archive" }, "unit": { "enum": [ "documents", "document_events", "hot_list_observations", "platform_topics", "indicator_observations", "indicator_releases" ] }, "date_field": { "enum": [ "collected_at", "observed_at", "first_seen_at", "last_seen_at", "event_at" ] }, "date_timezone": { "const": "UTC" }, "order": { "enum": [ "date_desc_id_desc", "event_at_asc" ] }, "notes": { "type": "array", "items": { "type": "string" } } } } } } }, "text/csv": { "schema": { "type": "string" } } } }, "400": { "description": "Invalid query, field, date or cursor. Body carries `code`: see x-error-codes.", "content": { "application/json": { "schema": { "type": "object", "required": [ "error", "code" ], "properties": { "error": { "type": "string" }, "retry_after": { "type": "integer" }, "code": { "type": "string", "enum": [ "invalid_parameter", "invalid_cursor", "unauthorized", "forbidden", "not_found", "method_not_allowed", "rate_limited", "server_error", "unavailable" ], "description": "Stable machine code; see x-error-codes." } } } } } }, "405": { "description": "Use GET. Body carries `code`: see x-error-codes.", "content": { "application/json": { "schema": { "type": "object", "required": [ "error" ], "properties": { "error": { "type": "string" }, "retry_after": { "type": "integer" } } } } } }, "429": { "description": "Rate limit exceeded (60 requests per minute). Body carries `code`: see x-error-codes.", "content": { "application/json": { "schema": { "type": "object", "required": [ "error" ], "properties": { "error": { "type": "string" }, "retry_after": { "type": "integer" } } } } }, "headers": { "Retry-After": { "schema": { "type": "integer" }, "description": "Seconds until retry; also sends RateLimit-* and X-RateLimit-* headers." } } }, "503": { "description": "The change feed is not yet enabled on this deployment (database migration 043 pending: code unavailable, error \"not yet enabled\"), or the archive is unavailable; retry later. Body carries `code`: see x-error-codes.", "content": { "application/json": { "schema": { "type": "object", "required": [ "error" ], "properties": { "error": { "type": "string" }, "retry_after": { "type": "integer" } } } } } } }, "x-codeSamples": [ { "lang": "curl", "label": "curl", "source": "curl -s 'https://china-snapshot.com/api/changes?dataset=documents&since=2026-10-29T00:00:00Z&limit=3'" }, { "lang": "curl", "label": "curl", "source": "curl -s -o changes.csv 'https://china-snapshot.com/api/changes?dataset=documents&since=2026-10-29T00:00:00Z&watch=export-controls&format=csv&limit=20'" } ], "x-pagination": { "first_call": "since=, inclusive", "continue": "cursor= or since= (the same signed token)", "repeats": "none within a walk: each document appears once, at its latest settled change", "end_of_results": "has_more is false; next_since stays valid for polling later", "settle_minutes": 10 } } ``` --- Source: https://china-snapshot.com/docs/api/list-series.md # Catalog of indicator time series listSeries · GET /api/series ## Endpoint `GET /api/series` Every series_id with title, frequency, unit, first and last period, observation count and how many rows are CSV backfill rather than archive releases. Free, no key. ## Authentication ```json [] ``` An empty security requirement means this operation does not declare authentication. Read operation notes for entitlement and protocol-method requirements. ## Parameters No parameters declared by the specification. ## Responses and errors | HTTP status | Meaning | | --- | --- | | 200 | The series catalog. | | 503 | Archive unavailable; retry later. | ## Response examples No response example is supplied by OpenAPI for this operation. The complete response schemas are preserved below; use an actual response for observed values. ## Retry guidance See [errors and retries](/docs/api/overview#errors-and-retries). Honor operation-specific response headers. A POST may create state; do not replay it automatically. ## Complete operation contract The expanded contract below includes all parameter constraints, response headers, schemas and examples from OpenAPI. ```json { "path": "/api/series", "method": "GET", "parameters": [], "operationId": "listSeries", "summary": "Catalog of indicator time series", "description": "Every series_id with title, frequency, unit, first and last period, observation count and how many rows are CSV backfill rather than archive releases. Free, no key.", "tags": [ "data", "Find records" ], "security": [], "responses": { "200": { "description": "The series catalog.", "content": { "application/json": { "schema": { "type": "object", "required": [ "series", "count" ] } } } }, "503": { "description": "Archive unavailable; retry later." } }, "x-codeSamples": [ { "lang": "curl", "label": "curl", "source": "curl -s 'https://china-snapshot.com/api/series'" } ] } ``` --- Source: https://china-snapshot.com/docs/api/get-series.md # One indicator as a clean time series getSeries · GET /api/series/{series_id} ## Endpoint `GET /api/series/{series_id}` One row per period, oldest first, with previous_value. origin=archive is a release we watched appear (release_date, revised); origin=backfill is older history from the published CSVs (provenance). Where both exist and disagree, both values are published (archive_value, backfill_value) and backfill_disagrees is true; a monthly or policy release keeps the archive, a daily quote takes the publisher-dated backfill, and an archive monthly row first seen before its month ended is rejected (archive_rejected). The tier history window bounds the earliest period (x-api-tiers): Pro reads the full series, e.g. CPI YoY from 2008-01. Over the tier row ceiling the newest rows are kept and truncated is true. ## Authentication ```json [] ``` An empty security requirement means this operation does not declare authentication. Read operation notes for entitlement and protocol-method requirements. ## Parameters | Name | In | Required | Description | | --- | --- | --- | --- | | series_id | path | Yes | From /api/series, e.g. cpi-yoy. | | from | query | No | Earliest reference period, YYYY-MM or YYYY-MM-DD. | | to | query | No | Latest reference period, inclusive. | | format | query | No | CSV returns the same observations as UTF-8 with BOM. | ## Responses and errors | HTTP status | Meaning | | --- | --- | | 200 | The series. | | 400 | Invalid series_id, period or format. | | 404 | Unknown series, or no observations in the caller history window. | | 429 | Rate limit exceeded. | | 503 | Archive unavailable; retry later. | ## Response examples No response example is supplied by OpenAPI for this operation. The complete response schemas are preserved below; use an actual response for observed values. ## Retry guidance See [errors and retries](/docs/api/overview#errors-and-retries). Honor operation-specific response headers. A POST may create state; do not replay it automatically. ## Complete operation contract The expanded contract below includes all parameter constraints, response headers, schemas and examples from OpenAPI. ```json { "path": "/api/series/{series_id}", "method": "GET", "parameters": [ { "name": "series_id", "in": "path", "required": true, "schema": { "type": "string", "pattern": "^[a-z0-9]+(-[a-z0-9]+)*$" }, "description": "From /api/series, e.g. cpi-yoy." }, { "name": "from", "in": "query", "required": false, "schema": { "type": "string", "pattern": "^\\d{4}-\\d{2}(-\\d{2})?$" }, "description": "Earliest reference period, YYYY-MM or YYYY-MM-DD." }, { "name": "to", "in": "query", "required": false, "schema": { "type": "string", "pattern": "^\\d{4}-\\d{2}(-\\d{2})?$" }, "description": "Latest reference period, inclusive." }, { "name": "format", "in": "query", "required": false, "schema": { "type": "string", "enum": [ "json", "csv" ], "default": "json" }, "description": "CSV returns the same observations as UTF-8 with BOM." } ], "operationId": "getSeries", "summary": "One indicator as a clean time series", "description": "One row per period, oldest first, with previous_value. origin=archive is a release we watched appear (release_date, revised); origin=backfill is older history from the published CSVs (provenance). Where both exist and disagree, both values are published (archive_value, backfill_value) and backfill_disagrees is true; a monthly or policy release keeps the archive, a daily quote takes the publisher-dated backfill, and an archive monthly row first seen before its month ended is rejected (archive_rejected). The tier history window bounds the earliest period (x-api-tiers): Pro reads the full series, e.g. CPI YoY from 2008-01. Over the tier row ceiling the newest rows are kept and truncated is true.", "tags": [ "data", "Inspect observations", "Export" ], "security": [], "responses": { "200": { "description": "The series.", "content": { "application/json": { "schema": { "type": "object", "required": [ "series_id", "observations", "truncated" ] } }, "text/csv": { "schema": { "type": "string" } } } }, "400": { "description": "Invalid series_id, period or format." }, "404": { "description": "Unknown series, or no observations in the caller history window." }, "429": { "description": "Rate limit exceeded." }, "503": { "description": "Archive unavailable; retry later." } }, "x-codeSamples": [ { "lang": "curl", "label": "curl", "source": "curl -s 'https://china-snapshot.com/api/series/pmi-manufacturing'" }, { "lang": "curl", "label": "curl", "source": "curl -s -o pmi.csv 'https://china-snapshot.com/api/series/pmi-manufacturing?from=2025-01&format=csv'" } ] } ``` --- Source: https://china-snapshot.com/docs/api/get-indicator.md # One official indicator, free getIndicator · GET /api/indicators/{slug}.json ## Endpoint `GET /api/indicators/{slug}.json` Latest value, source, next release, a recent window, and the history charted on /indicators/{slug}. This is one series (or the sub-series that share that page). It is not a bulk download of every indicator. No key. ## Authentication ```json [] ``` An empty security requirement means this operation does not declare authentication. Read operation notes for entitlement and protocol-method requirements. ## Parameters | Name | In | Required | Description | | --- | --- | --- | --- | | slug | path | Yes | Indicator slug, for example china-cpi. | ## Responses and errors | HTTP status | Meaning | | --- | --- | | 200 | The public indicator record. | | 404 | No such indicator. | ## Response examples No response example is supplied by OpenAPI for this operation. The complete response schemas are preserved below; use an actual response for observed values. ## Retry guidance See [errors and retries](/docs/api/overview#errors-and-retries). Honor operation-specific response headers. A POST may create state; do not replay it automatically. ## Complete operation contract The expanded contract below includes all parameter constraints, response headers, schemas and examples from OpenAPI. ```json { "path": "/api/indicators/{slug}.json", "method": "GET", "parameters": [ { "name": "slug", "in": "path", "required": true, "schema": { "type": "string", "pattern": "^[a-z0-9-]{1,64}$" }, "description": "Indicator slug, for example china-cpi." } ], "operationId": "getIndicator", "summary": "One official indicator, free", "description": "Latest value, source, next release, a recent window, and the history charted on /indicators/{slug}. This is one series (or the sub-series that share that page). It is not a bulk download of every indicator. No key.", "tags": [ "data" ], "security": [], "responses": { "200": { "description": "The public indicator record.", "content": { "application/json": { "schema": { "type": "object" } } } }, "404": { "description": "No such indicator." } }, "x-codeSamples": [ { "lang": "curl", "label": "curl", "source": "curl -s 'https://china-snapshot.com/api/indicators/china-cpi.json'" } ] } ``` --- Source: https://china-snapshot.com/docs/api/list-public-topics.md # Current topics and shared cited-document edges. Connections do not establish causation. listPublicTopics · GET /data/topics/index.json ## Endpoint `GET /data/topics/index.json` No account required. Reading dates mark analysis revisions, not policy effective dates. Preserve source URLs, coverage notes and the AI-assisted interpretation label. Citation additions/removals do not mean publications/retractions. ## Authentication ```json [] ``` An empty security requirement means this operation does not declare authentication. Read operation notes for entitlement and protocol-method requirements. ## Parameters No parameters declared by the specification. ## Responses and errors | HTTP status | Meaning | | --- | --- | | 200 | Current topics and shared cited-document edges. Connections do not establish causation. | | 404 | No published topic or revision at this URL. | ## Response examples No response example is supplied by OpenAPI for this operation. The complete response schemas are preserved below; use an actual response for observed values. ## Retry guidance See [errors and retries](/docs/api/overview#errors-and-retries). Honor operation-specific response headers. A POST may create state; do not replay it automatically. ## Complete operation contract The expanded contract below includes all parameter constraints, response headers, schemas and examples from OpenAPI. ```json { "path": "/data/topics/index.json", "method": "GET", "parameters": [], "operationId": "listPublicTopics", "summary": "Current topics and shared cited-document edges. Connections do not establish causation.", "tags": [ "data" ], "security": [], "description": "No account required. Reading dates mark analysis revisions, not policy effective dates. Preserve source URLs, coverage notes and the AI-assisted interpretation label. Citation additions/removals do not mean publications/retractions.", "responses": { "200": { "description": "Current topics and shared cited-document edges. Connections do not establish causation.", "content": { "application/json": { "schema": { "type": "object" } } } }, "404": { "description": "No published topic or revision at this URL." } }, "x-codeSamples": [ { "lang": "curl", "label": "curl", "source": "curl -s 'https://china-snapshot.com/data/topics/index.json'" } ] } ``` --- Source: https://china-snapshot.com/docs/api/get-public-topic.md # Current reading, cited evidence and available history. Revision numbers may have gaps. getPublicTopic · GET /data/topics/{slug}.json ## Endpoint `GET /data/topics/{slug}.json` No account required. Reading dates mark analysis revisions, not policy effective dates. Preserve source URLs, coverage notes and the AI-assisted interpretation label. Citation additions/removals do not mean publications/retractions. ## Authentication ```json [] ``` An empty security requirement means this operation does not declare authentication. Read operation notes for entitlement and protocol-method requirements. ## Parameters | Name | In | Required | Description | | --- | --- | --- | --- | | slug | path | Yes | | ## Responses and errors | HTTP status | Meaning | | --- | --- | | 200 | Current reading, cited evidence and available history. Revision numbers may have gaps. | | 404 | No published topic or revision at this URL. | ## Response examples No response example is supplied by OpenAPI for this operation. The complete response schemas are preserved below; use an actual response for observed values. ## Retry guidance See [errors and retries](/docs/api/overview#errors-and-retries). Honor operation-specific response headers. A POST may create state; do not replay it automatically. ## Complete operation contract The expanded contract below includes all parameter constraints, response headers, schemas and examples from OpenAPI. ```json { "path": "/data/topics/{slug}.json", "method": "GET", "parameters": [ { "name": "slug", "in": "path", "required": true, "schema": { "type": "string", "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$" } } ], "operationId": "getPublicTopic", "summary": "Current reading, cited evidence and available history. Revision numbers may have gaps.", "tags": [ "data" ], "security": [], "description": "No account required. Reading dates mark analysis revisions, not policy effective dates. Preserve source URLs, coverage notes and the AI-assisted interpretation label. Citation additions/removals do not mean publications/retractions.", "responses": { "200": { "description": "Current reading, cited evidence and available history. Revision numbers may have gaps.", "content": { "application/json": { "schema": { "type": "object" } } } }, "404": { "description": "No published topic or revision at this URL." } } } ``` --- Source: https://china-snapshot.com/docs/api/get-public-topic-markdown.md # Current reading as Markdown with dates, original citations and creator attribution. getPublicTopicMarkdown · GET /data/topics/{slug}.md ## Endpoint `GET /data/topics/{slug}.md` No account required. Reading dates mark analysis revisions, not policy effective dates. Preserve source URLs, coverage notes and the AI-assisted interpretation label. Citation additions/removals do not mean publications/retractions. ## Authentication ```json [] ``` An empty security requirement means this operation does not declare authentication. Read operation notes for entitlement and protocol-method requirements. ## Parameters | Name | In | Required | Description | | --- | --- | --- | --- | | slug | path | Yes | | ## Responses and errors | HTTP status | Meaning | | --- | --- | | 200 | Current reading as Markdown with dates, original citations and creator attribution. | | 404 | No published topic or revision at this URL. | ## Response examples No response example is supplied by OpenAPI for this operation. The complete response schemas are preserved below; use an actual response for observed values. ## Retry guidance See [errors and retries](/docs/api/overview#errors-and-retries). Honor operation-specific response headers. A POST may create state; do not replay it automatically. ## Complete operation contract The expanded contract below includes all parameter constraints, response headers, schemas and examples from OpenAPI. ```json { "path": "/data/topics/{slug}.md", "method": "GET", "parameters": [ { "name": "slug", "in": "path", "required": true, "schema": { "type": "string", "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$" } } ], "operationId": "getPublicTopicMarkdown", "summary": "Current reading as Markdown with dates, original citations and creator attribution.", "tags": [ "data" ], "security": [], "description": "No account required. Reading dates mark analysis revisions, not policy effective dates. Preserve source URLs, coverage notes and the AI-assisted interpretation label. Citation additions/removals do not mean publications/retractions.", "responses": { "200": { "description": "Current reading as Markdown with dates, original citations and creator attribution.", "content": { "text/markdown": { "schema": { "type": "string" } } } }, "404": { "description": "No published topic or revision at this URL." } } } ``` --- Source: https://china-snapshot.com/docs/api/get-public-topic-revision.md # A retained publishable reading with its own citations and comparison to the previous available reading. getPublicTopicRevision · GET /data/topics/{slug}/v{version}.json ## Endpoint `GET /data/topics/{slug}/v{version}.json` No account required. Reading dates mark analysis revisions, not policy effective dates. Preserve source URLs, coverage notes and the AI-assisted interpretation label. Citation additions/removals do not mean publications/retractions. ## Authentication ```json [] ``` An empty security requirement means this operation does not declare authentication. Read operation notes for entitlement and protocol-method requirements. ## Parameters | Name | In | Required | Description | | --- | --- | --- | --- | | slug | path | Yes | | | version | path | Yes | | ## Responses and errors | HTTP status | Meaning | | --- | --- | | 200 | A retained publishable reading with its own citations and comparison to the previous available reading. | | 404 | No published topic or revision at this URL. | ## Response examples No response example is supplied by OpenAPI for this operation. The complete response schemas are preserved below; use an actual response for observed values. ## Retry guidance See [errors and retries](/docs/api/overview#errors-and-retries). Honor operation-specific response headers. A POST may create state; do not replay it automatically. ## Complete operation contract The expanded contract below includes all parameter constraints, response headers, schemas and examples from OpenAPI. ```json { "path": "/data/topics/{slug}/v{version}.json", "method": "GET", "parameters": [ { "name": "slug", "in": "path", "required": true, "schema": { "type": "string", "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$" } }, { "name": "version", "in": "path", "required": true, "schema": { "type": "integer", "minimum": 1 } } ], "operationId": "getPublicTopicRevision", "summary": "A retained publishable reading with its own citations and comparison to the previous available reading.", "tags": [ "data" ], "security": [], "description": "No account required. Reading dates mark analysis revisions, not policy effective dates. Preserve source URLs, coverage notes and the AI-assisted interpretation label. Citation additions/removals do not mean publications/retractions.", "responses": { "200": { "description": "A retained publishable reading with its own citations and comparison to the previous available reading.", "content": { "application/json": { "schema": { "type": "object" } } } }, "404": { "description": "No published topic or revision at this URL." } } } ``` --- Source: https://china-snapshot.com/docs/api/get-public-topic-revision-markdown.md # A retained publishable reading as Markdown, with original citations. getPublicTopicRevisionMarkdown · GET /data/topics/{slug}/v{version}.md ## Endpoint `GET /data/topics/{slug}/v{version}.md` No account required. Reading dates mark analysis revisions, not policy effective dates. Preserve source URLs, coverage notes and the AI-assisted interpretation label. Citation additions/removals do not mean publications/retractions. ## Authentication ```json [] ``` An empty security requirement means this operation does not declare authentication. Read operation notes for entitlement and protocol-method requirements. ## Parameters | Name | In | Required | Description | | --- | --- | --- | --- | | slug | path | Yes | | | version | path | Yes | | ## Responses and errors | HTTP status | Meaning | | --- | --- | | 200 | A retained publishable reading as Markdown, with original citations. | | 404 | No published topic or revision at this URL. | ## Response examples No response example is supplied by OpenAPI for this operation. The complete response schemas are preserved below; use an actual response for observed values. ## Retry guidance See [errors and retries](/docs/api/overview#errors-and-retries). Honor operation-specific response headers. A POST may create state; do not replay it automatically. ## Complete operation contract The expanded contract below includes all parameter constraints, response headers, schemas and examples from OpenAPI. ```json { "path": "/data/topics/{slug}/v{version}.md", "method": "GET", "parameters": [ { "name": "slug", "in": "path", "required": true, "schema": { "type": "string", "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$" } }, { "name": "version", "in": "path", "required": true, "schema": { "type": "integer", "minimum": 1 } } ], "operationId": "getPublicTopicRevisionMarkdown", "summary": "A retained publishable reading as Markdown, with original citations.", "tags": [ "data" ], "security": [], "description": "No account required. Reading dates mark analysis revisions, not policy effective dates. Preserve source URLs, coverage notes and the AI-assisted interpretation label. Citation additions/removals do not mean publications/retractions.", "responses": { "200": { "description": "A retained publishable reading as Markdown, with original citations.", "content": { "text/markdown": { "schema": { "type": "string" } } } }, "404": { "description": "No published topic or revision at this URL." } } } ``` --- Source: https://china-snapshot.com/docs/api/get-endpoints-catalog.md # Fetch endpoints getEndpointsCatalog · GET /data/endpoints.json ## Endpoint `GET /data/endpoints.json` The self-describing catalog of every public China Snapshot data feed, regenerated after each collection run. Category, schema, access level, series, and item counts. ## Authentication ```json [] ``` An empty security requirement means this operation does not declare authentication. Read operation notes for entitlement and protocol-method requirements. ## Parameters No parameters declared by the specification. ## Responses and errors | HTTP status | Meaning | | --- | --- | | 200 | The requested feed. | | 404 | Feed not published for this run. | ## Response examples No response example is supplied by OpenAPI for this operation. The complete response schemas are preserved below; use an actual response for observed values. ## Retry guidance See [errors and retries](/docs/api/overview#errors-and-retries). Honor operation-specific response headers. A POST may create state; do not replay it automatically. ## Complete operation contract The expanded contract below includes all parameter constraints, response headers, schemas and examples from OpenAPI. ```json { "path": "/data/endpoints.json", "method": "GET", "parameters": [], "operationId": "getEndpointsCatalog", "summary": "Fetch endpoints", "description": "The self-describing catalog of every public China Snapshot data feed, regenerated after each collection run. Category, schema, access level, series, and item counts.", "tags": [ "data" ], "security": [], "responses": { "200": { "description": "The requested feed.", "content": { "application/json": { "schema": { "type": "object", "properties": { "name": { "type": "string" }, "description": { "type": "string" }, "generated_at": { "type": "string" }, "license": { "type": "string" }, "llms_txt": { "type": "string" }, "agents_txt": { "type": "string" }, "developer_portal": { "type": "string" }, "developer_portal_markdown": { "type": "string" }, "openapi": { "type": "string" }, "schema": { "type": "object", "properties": { "version": { "type": "integer" }, "feed_schema_url": { "type": "string" }, "digest_schema_url": { "type": "string" }, "synthesis_schema_url": { "type": "string" }, "geo_catalog_schema_url": { "type": "string" }, "signal_packet_schema_url": { "type": "string" }, "feeds": { "type": "string" }, "digest": { "type": "string" }, "history": { "type": "string" }, "series": { "type": "string" }, "access": { "type": "string" } } }, "access": { "type": "object", "properties": { "default": { "type": "string" }, "levels": { "type": "object", "properties": { "public": { "type": "string" }, "professional": { "type": "string" } } }, "professional_url_prefixes": { "type": "array", "items": { "type": "string" } }, "professional_endpoints": { "type": "object", "properties": { "https://china-snapshot.com/api/research": { "type": "string" }, "https://china-snapshot.com/api/mcp": { "type": "string" } } }, "how_to_access": { "type": "string" } } }, "archive": { "type": "object", "properties": { "description": { "type": "string" }, "access": { "type": "string" }, "how_to_access": { "type": "string" }, "index_url": { "type": "string" }, "url_template": { "type": "string" }, "slots": { "type": "array", "items": { "type": "string" } }, "earliest_date": { "type": "string" }, "latest_date": { "type": "string" }, "indexed_snapshots": { "type": "integer" }, "note": { "type": "string" } } }, "synthesis": { "type": "object", "properties": { "description": { "type": "string" }, "access": { "type": "string" }, "how_to_access": { "type": "string" }, "index_url": { "type": "string" }, "entities_url": { "type": "string" }, "topic_url_template": { "type": "string" }, "version_permalink_template": { "type": "string" }, "topic_count": { "type": "integer" } } }, "topics": { "type": "object", "properties": { "access": { "type": "string" }, "description": { "type": "string" }, "index_url": { "type": "string" }, "current_url_template": { "type": "string" }, "reading_url_template": { "type": "string" }, "markdown_url_template": { "type": "string" }, "human_url": { "type": "string" }, "note": { "type": "string" } } }, "special": { "type": "object", "properties": { "agents_txt": { "type": "string" }, "daily_digest": { "type": "string" }, "daily_digest_markdown": { "type": "string" }, "payload_budget": { "type": "string" }, "health": { "type": "string" }, "source_reliability": { "type": "string" }, "geo_catalog": { "type": "string" }, "professional_api": { "type": "string" }, "professional_mcp": { "type": "string" }, "openapi": { "type": "string" }, "developer_portal": { "type": "string" }, "professional_signal_packet": { "type": "object", "properties": { "json": { "type": "string" }, "markdown": { "type": "string" }, "access": { "type": "string" } } }, "gov_registry_health": { "type": "string" }, "public_signal_packet": { "type": "string" }, "public_signal_packet_markdown": { "type": "string" } } }, "feeds": { "type": "array", "items": { "type": "object", "properties": { "name": { "type": "string" }, "category": { "type": "string" }, "description": { "type": "string" }, "url": { "type": "string" }, "access": { "type": "string" }, "as_of": { "type": "string" }, "schema_id": { "type": "string" }, "schema_url": { "type": "string" }, "item_count": { "type": "integer" }, "series": { "type": "array", "items": { "type": "string" } }, "history_url": { "type": "string" }, "history_access": { "type": "string" } } } }, "forecasts": { "type": "object", "properties": { "description": { "type": "string" }, "access": { "type": "string" }, "index_url": { "type": "string" }, "track_record_url": { "type": "string" }, "question_url_template": { "type": "string" }, "human_url": { "type": "string" }, "as_of": { "type": "string" }, "counts": { "type": "object", "properties": { "total": { "type": "integer" }, "public": { "type": "integer" }, "member": { "type": "integer" }, "open": { "type": "integer" }, "resolved": { "type": "integer" }, "void": { "type": "integer" } } }, "note": { "type": "string" } } } }, "additionalProperties": true, "description": "Inferred from the live feed at build time. The field inventory reflects this collection run; optional fields may be absent and the shape is refreshed on every deploy." } } } }, "404": { "description": "Feed not published for this run.", "content": { "application/json": { "schema": { "type": "object", "required": [ "error" ], "properties": { "error": { "type": "object", "required": [ "code", "message" ], "properties": { "code": { "type": "string", "description": "A stable machine-readable code, e.g. unauthorized or not_found." }, "message": { "type": "string", "description": "A human-readable explanation." }, "hint": { "type": "string", "description": "Optional resolution pointer." } } } } } } } } }, "x-codeSamples": [ { "lang": "curl", "label": "curl", "source": "curl -s 'https://china-snapshot.com/data/endpoints.json'" } ] } ``` --- Source: https://china-snapshot.com/docs/api/get-daily-digest.md # Fetch daily_digest getDailyDigest · GET /data/daily_digest.json ## Endpoint `GET /data/daily_digest.json` The current cross-source synthesis: headline, narrative, market snapshot, deviations, themes, entities, pillars, and ranked top stories. ## Authentication ```json [] ``` An empty security requirement means this operation does not declare authentication. Read operation notes for entitlement and protocol-method requirements. ## Parameters No parameters declared by the specification. ## Responses and errors | HTTP status | Meaning | | --- | --- | | 200 | The requested feed. | | 404 | Feed not published for this run. | ## Response examples No response example is supplied by OpenAPI for this operation. The complete response schemas are preserved below; use an actual response for observed values. ## Retry guidance See [errors and retries](/docs/api/overview#errors-and-retries). Honor operation-specific response headers. A POST may create state; do not replay it automatically. ## Complete operation contract The expanded contract below includes all parameter constraints, response headers, schemas and examples from OpenAPI. ```json { "path": "/data/daily_digest.json", "method": "GET", "parameters": [], "operationId": "getDailyDigest", "summary": "Fetch daily_digest", "description": "The current cross-source synthesis: headline, narrative, market snapshot, deviations, themes, entities, pillars, and ranked top stories.", "tags": [ "data" ], "security": [], "responses": { "200": { "description": "The requested feed.", "content": { "application/json": { "schema": { "type": "object", "properties": { "schema_version": { "type": "integer" }, "digest_type": { "type": "string" }, "as_of": { "type": "string" }, "date": { "type": "string" }, "time_label": { "type": "string" }, "beijing_time": { "type": "string" }, "generated_by": { "type": "string" }, "editorial_review": { "type": "object", "properties": { "model": { "type": "string" }, "passes": { "type": "integer" }, "score": { "type": "integer" }, "reviewer_score": { "type": "integer" }, "dimensions": { "type": "object", "properties": { "evidence_integrity": { "type": "integer" }, "executive_clarity": { "type": "integer" }, "decision_utility": { "type": "integer" }, "selectivity": { "type": "integer" }, "voice": { "type": "integer" } } }, "hard_failures": { "type": "array", "items": {} }, "deterministic_issues": { "type": "array", "items": { "type": "string" } }, "deterministic_penalties": { "type": "array", "items": {} }, "deterministic_repairs": { "type": "array", "items": { "type": "string" } }, "revision_notes": { "type": "array", "items": { "type": "string" } }, "passed": { "type": "boolean" }, "ungrounded_overrides": { "type": "integer" }, "ungrounded_story_indices": { "type": "array", "items": {} }, "model_overrides_offered": { "type": "integer" }, "model_overrides_applied": { "type": "integer" }, "override_alignment_failed": { "type": "boolean" }, "sanitized_stories": { "type": "array", "items": {} }, "stories_dropped_by_sanitizer": { "type": "array", "items": {} }, "story_annotations": { "type": "object", "properties": { "kept": { "type": "integer" }, "kept_indices": { "type": "array", "items": { "type": "integer" } }, "dropped_by_model": { "type": "array", "items": { "type": "integer" } }, "withheld": { "type": "array", "items": {} }, "withheld_reasons": { "type": "object", "properties": {} } } }, "llm_usage": { "type": "object", "properties": { "calls": { "type": "integer" }, "prompt_tokens": { "type": "integer" }, "completion_tokens": { "type": "integer" } } }, "current_read_deterministic_issues": { "type": "array", "items": {} }, "current_read_passed": { "type": "boolean" }, "current_read_focused_recovery": { "type": "object", "properties": { "score": { "type": "integer" }, "dimensions": { "type": "object", "properties": { "evidence_integrity": { "type": "integer" }, "executive_clarity": { "type": "integer" }, "decision_utility": { "type": "integer" }, "selectivity": { "type": "integer" }, "voice": { "type": "integer" } } }, "hard_failures": { "type": "array", "items": {} }, "deterministic_issues": { "type": "array", "items": {} }, "llm_usage": { "type": "object", "properties": { "calls": { "type": "integer" }, "prompt_tokens": { "type": "integer" }, "completion_tokens": { "type": "integer" } } } } }, "current_read_failed_issues": { "type": "array", "items": { "type": "string" } }, "fallback": { "type": "string" } } }, "review_status": { "type": "string" }, "review_status_reason": { "type": "string" }, "headline": { "type": "string" }, "headline_zh": { "type": "string" }, "narrative": { "type": "string" }, "narrative_zh": { "type": "string" }, "market_snapshot": { "type": "string" }, "market_snapshot_stale": { "type": "boolean" }, "market_snapshot_session_date": { "type": "string" }, "deviations": { "type": "array", "items": { "type": "object", "properties": { "title": { "type": "string" }, "note": { "type": "string" }, "note_zh": { "type": "string" }, "kind": { "type": "string" }, "flags": { "type": "array", "items": { "type": "string" } }, "z30": { "type": "number" }, "severity": { "type": "number" }, "days_flagged": { "type": "integer" } } } }, "changes_since_prior": { "type": "object", "properties": { "prior_as_of": { "type": "string" }, "prior_label": { "type": "string" }, "new_count": { "type": "integer" }, "carried_count": { "type": "integer" }, "new_signals": { "type": "array", "items": { "type": "string" } } } }, "themes": { "type": "array", "items": { "type": "string" } }, "theme_trends": { "type": "object", "properties": { "export controls": { "type": "object", "properties": { "status": { "type": "string" }, "days_seen": { "type": "integer" } } }, "employment": { "type": "object", "properties": { "status": { "type": "string" }, "days_seen": { "type": "integer" } } }, "party building": { "type": "object", "properties": { "status": { "type": "string" }, "days_seen": { "type": "integer" } } }, "public satisfaction": { "type": "object", "properties": { "status": { "type": "string" }, "days_seen": { "type": "integer" } } } } }, "entities": { "type": "array", "items": { "type": "string" } }, "pillars": { "type": "array", "items": { "type": "object", "properties": { "key": { "type": "string" }, "label": { "type": "string" }, "label_zh": { "type": "string" } } } }, "top_stories": { "type": "array", "items": { "type": "object", "properties": { "rank": { "type": "integer" }, "first_seen": { "type": "string" }, "days_on_board": { "type": "integer" }, "story_status": { "type": "string" }, "continuity_basis": { "type": "string" }, "narrative_eligible": { "type": "boolean" }, "narrative_reason": { "type": "string" }, "weight": { "type": "number" }, "weight_components": { "type": "object", "properties": { "base": { "type": "integer" }, "repeat_editions_excluded": { "type": "integer" }, "cross_platform_bonus": { "type": "number" }, "significance_class": { "type": "string" }, "source_class_factor": { "type": "number" }, "deviation_linked": { "type": "boolean" }, "deviation_factor": { "type": "number" }, "age_hours": { "type": "number" }, "recency_factor": { "type": "number" }, "stale": { "type": "boolean" }, "continuity_factor": { "type": "number" }, "weight": { "type": "number" } } }, "platforms": { "type": "array", "items": { "type": "string" } }, "platform_count": { "type": "integer" }, "source_class": { "type": "string" }, "source_class_label": { "type": "string" }, "confidence": { "type": "number" }, "primary_title": { "type": "string" }, "english_title": { "type": "string" }, "untranslated": { "type": "boolean" }, "observed_signal": { "type": "string" }, "why_it_matters": { "type": "string" }, "why_it_matters_zh": { "type": "string" }, "what_to_watch": { "type": "string" }, "what_to_watch_zh": { "type": "string" }, "pull_quote": { "type": "string" }, "pull_quote_zh": { "type": "string" }, "analysis_basis": { "type": "object", "properties": { "type": { "type": "string" }, "implication_source": { "type": "string" }, "watchpoint_source": { "type": "string" }, "evidence_ids": { "type": "array", "items": {} }, "note": { "type": "string" } } }, "watcher_lenses": { "type": "array", "items": { "type": "string" } }, "pillar": { "type": "string" }, "pillar_label": { "type": "string" }, "pillar_label_zh": { "type": "string" }, "source": { "type": "string" }, "category": { "type": "string" }, "url": { "type": "string" }, "appearances": { "type": "array", "items": { "type": "object", "properties": { "platform": {}, "rank": {} } } }, "evidence": { "type": "array", "items": { "type": "object", "properties": { "id": {}, "source": {}, "source_label": {}, "rank": {}, "title": {}, "url": {}, "source_class": {}, "description": {}, "published": {} } } } } } }, "excluded": { "type": "array", "items": { "type": "object", "properties": { "reason": { "type": "string" }, "title": { "type": "string" }, "english_title": { "type": "string" }, "source": { "type": "string" }, "url": { "type": "string" }, "weight": { "type": "number" }, "age_hours": { "type": "number" }, "mention": { "type": "string" } } } }, "excluded_summary": { "type": "object", "properties": { "boilerplate:information_disclosure_guide": { "type": "integer" }, "boilerplate:regular_press_conference": { "type": "integer" }, "stale:older_than_72h": { "type": "integer" }, "pillar_cap_full:society": { "type": "integer" }, "pillar_cap_full:economy": { "type": "integer" }, "below_weight_floor": { "type": "integer" } } }, "routine_mentions": { "type": "array", "items": { "type": "string" } }, "board_translation": { "type": "object", "properties": { "requested": { "type": "integer" }, "translated": { "type": "integer" }, "untranslated": { "type": "array", "items": {} } } }, "body_reads": { "type": "object", "properties": { "corpus": { "type": "string" }, "status": { "type": "string" }, "queried": { "type": "boolean" }, "matched_documents": { "type": "integer" }, "selected_docs": { "type": "integer" }, "refused_bodies": { "type": "integer" }, "total_excerpt_chars": { "type": "integer" }, "max_docs": { "type": "integer" }, "max_total_chars": { "type": "integer" } } }, "history_reads": { "type": "object", "properties": { "corpus": { "type": "string" }, "status": { "type": "string" }, "queried": { "type": "boolean" }, "stories_examined": { "type": "integer" }, "stories_with_prior_day_matches": { "type": "integer" }, "stories_with_same_day_only_matches": { "type": "integer" }, "truncated": { "type": "boolean" }, "max_stories": { "type": "integer" }, "max_urls": { "type": "integer" }, "claim_boundary": { "type": "string" } } }, "evidence_summary": { "type": "object", "properties": { "mode": { "type": "string" }, "claim_boundary": { "type": "string" }, "evidence_count": { "type": "integer" }, "source_classes": { "type": "object", "properties": { "official": { "type": "string" }, "state_media": { "type": "string" }, "commercial_press": { "type": "string" }, "social": { "type": "string" }, "market": { "type": "string" }, "calculation": { "type": "string" } } } } }, "sources_summary": { "type": "object", "properties": { "baidu_top": { "type": "integer" }, "weibo_hot": { "type": "integer" }, "tencent_wechat_hot": { "type": "integer" }, "xinhua_news": { "type": "integer" }, "thepaper_news": { "type": "integer" }, "ladymax_news": { "type": "integer" }, "gov_registry": { "type": "integer" }, "elite_press": { "type": "integer" }, "indices": { "type": "integer" }, "fx": { "type": "integer" } } }, "disclaimer": { "type": "string" }, "edition_lead": { "type": "object", "properties": { "url": { "type": "string" }, "title": { "type": "string" } } }, "art": { "type": "object", "properties": { "key": { "type": "string" }, "src": { "type": "string" }, "subject": { "type": "string" }, "location": { "type": "string" }, "alt": { "type": "string" }, "position": { "type": "string" } } } }, "additionalProperties": true, "description": "Inferred from the live feed at build time. The field inventory reflects this collection run; optional fields may be absent and the shape is refreshed on every deploy." } } } }, "404": { "description": "Feed not published for this run.", "content": { "application/json": { "schema": { "type": "object", "required": [ "error" ], "properties": { "error": { "type": "object", "required": [ "code", "message" ], "properties": { "code": { "type": "string", "description": "A stable machine-readable code, e.g. unauthorized or not_found." }, "message": { "type": "string", "description": "A human-readable explanation." }, "hint": { "type": "string", "description": "Optional resolution pointer." } } } } } } } } }, "x-codeSamples": [ { "lang": "curl", "label": "curl", "source": "curl -s 'https://china-snapshot.com/data/daily_digest.json'" } ] } ``` --- Source: https://china-snapshot.com/docs/api/get-baselines.md # Fetch baselines getBaselines · GET /data/baselines.json ## Endpoint `GET /data/baselines.json` 30/90-day archive baselines per numeric series plus a ranked deviations list (z-spikes, range breaks, new prints). ## Authentication ```json [] ``` An empty security requirement means this operation does not declare authentication. Read operation notes for entitlement and protocol-method requirements. ## Parameters No parameters declared by the specification. ## Responses and errors | HTTP status | Meaning | | --- | --- | | 200 | The requested feed. | | 404 | Feed not published for this run. | ## Response examples No response example is supplied by OpenAPI for this operation. The complete response schemas are preserved below; use an actual response for observed values. ## Retry guidance See [errors and retries](/docs/api/overview#errors-and-retries). Honor operation-specific response headers. A POST may create state; do not replay it automatically. ## Complete operation contract The expanded contract below includes all parameter constraints, response headers, schemas and examples from OpenAPI. ```json { "path": "/data/baselines.json", "method": "GET", "parameters": [], "operationId": "getBaselines", "summary": "Fetch baselines", "description": "30/90-day archive baselines per numeric series plus a ranked deviations list (z-spikes, range breaks, new prints).", "tags": [ "data" ], "security": [], "responses": { "200": { "description": "The requested feed.", "content": { "application/json": { "schema": { "type": "object", "properties": { "schema_version": { "type": "integer" }, "as_of": { "type": "string" }, "source": { "type": "string" }, "items": { "type": "array", "items": { "type": "object", "properties": { "title": { "type": "string" }, "value": { "type": "number" }, "url": { "type": "string" }, "extra": { "type": "object", "properties": { "feed": { "type": "string" }, "feed_label": { "type": "string" }, "unit": { "type": "string" }, "kind": { "type": "string" }, "latest_source": { "type": "string" }, "baseline_30d": { "type": "object", "properties": { "days": {}, "mean": {}, "std": {}, "min": {}, "max": {} } }, "baseline_90d": { "type": "object", "properties": { "days": {}, "mean": {}, "std": {}, "min": {}, "max": {} } }, "z30": { "type": "number" }, "flags": { "type": "array", "items": {} }, "note": { "type": "string" }, "note_zh": { "type": "string" }, "days_flagged": { "type": "integer" }, "archive_corrected_days": { "type": "integer" }, "archive_stale_days_excluded": { "type": "integer" } } } } } }, "window_days": { "type": "integer" }, "short_window_days": { "type": "integer" }, "deviations": { "type": "array", "items": { "type": "object", "properties": { "title": { "type": "string" }, "feed": { "type": "string" }, "feed_label": { "type": "string" }, "kind": { "type": "string" }, "flags": { "type": "array", "items": { "type": "string" } }, "z30": { "type": "number" }, "severity": { "type": "number" }, "severity_raw": { "type": "number" }, "first_flagged": { "type": "string" }, "last_flagged": { "type": "string" }, "days_flagged": { "type": "integer" }, "note": { "type": "string" }, "note_zh": { "type": "string" }, "measure": { "type": "string" }, "unit": { "type": "string" }, "subject": { "type": "string" } } } }, "suppressed": { "type": "object", "properties": { "archive_corrections": { "type": "array", "items": {} }, "archive_correction_pending": { "type": "array", "items": {} }, "stale_archive_days": { "type": "object", "properties": {} }, "suspect_values": { "type": "array", "items": {} }, "baseline_building": { "type": "array", "items": { "type": "string" } } } }, "notes": { "type": "string" } }, "additionalProperties": true, "description": "Inferred from the live feed at build time. The field inventory reflects this collection run; optional fields may be absent and the shape is refreshed on every deploy." } } } }, "404": { "description": "Feed not published for this run.", "content": { "application/json": { "schema": { "type": "object", "required": [ "error" ], "properties": { "error": { "type": "object", "required": [ "code", "message" ], "properties": { "code": { "type": "string", "description": "A stable machine-readable code, e.g. unauthorized or not_found." }, "message": { "type": "string", "description": "A human-readable explanation." }, "hint": { "type": "string", "description": "Optional resolution pointer." } } } } } } } } }, "x-codeSamples": [ { "lang": "curl", "label": "curl", "source": "curl -s 'https://china-snapshot.com/data/baselines.json'" } ] } ``` --- Source: https://china-snapshot.com/docs/api/get-forecast-board.md # Fetch forecasts/index getForecastBoard · GET /data/forecasts/index.json ## Endpoint `GET /data/forecasts/index.json` The Forecast Desk board: resolvable questions on Chinese policy and data prints with their resolution criteria and probability history. ## Authentication ```json [] ``` An empty security requirement means this operation does not declare authentication. Read operation notes for entitlement and protocol-method requirements. ## Parameters No parameters declared by the specification. ## Responses and errors | HTTP status | Meaning | | --- | --- | | 200 | The requested feed. | | 404 | Feed not published for this run. | ## Response examples No response example is supplied by OpenAPI for this operation. The complete response schemas are preserved below; use an actual response for observed values. ## Retry guidance See [errors and retries](/docs/api/overview#errors-and-retries). Honor operation-specific response headers. A POST may create state; do not replay it automatically. ## Complete operation contract The expanded contract below includes all parameter constraints, response headers, schemas and examples from OpenAPI. ```json { "path": "/data/forecasts/index.json", "method": "GET", "parameters": [], "operationId": "getForecastBoard", "summary": "Fetch forecasts/index", "description": "The Forecast Desk board: resolvable questions on Chinese policy and data prints with their resolution criteria and probability history.", "tags": [ "data" ], "security": [], "responses": { "200": { "description": "The requested feed.", "content": { "application/json": { "schema": { "type": "object", "properties": { "schema_version": { "type": "integer" }, "as_of": { "type": "string" }, "source": { "type": "string" }, "counts": { "type": "object", "properties": { "total": { "type": "integer" }, "public": { "type": "integer" }, "member": { "type": "integer" }, "open": { "type": "integer" }, "resolved": { "type": "integer" }, "void": { "type": "integer" } } }, "track_record_url": { "type": "string" }, "questions": { "type": "array", "items": { "type": "object", "properties": { "id": { "type": "string" }, "title": { "type": "string" }, "criterion": { "type": "string" }, "pillar": { "type": "string" }, "question_class": { "type": "string" }, "access": { "type": "string" }, "status": { "type": "string" }, "opens": { "type": "string" }, "resolve_by": { "type": "string" }, "resolver_summary": { "type": "string" }, "probability": { "type": "number" }, "spread": { "type": "number" }, "members": { "type": "integer" }, "method": { "type": "string" }, "model": { "type": "string" }, "provisional": { "type": "boolean" }, "base_rate": { "type": "object", "properties": { "available": { "type": "boolean" }, "kind": { "type": "string" }, "probability": { "type": "number" }, "observed_days": { "type": "integer" }, "note": { "type": "string" }, "reason": { "type": "string" }, "observed_prints": { "type": "integer" }, "detail": { "type": "string" }, "window": { "type": "object", "properties": { "first_day": {}, "last_day": {} } } } }, "evidence_quality": { "type": "object", "properties": { "grade": { "type": "string" }, "documents": { "type": "integer" }, "term_matched": { "type": "integer" }, "dated": { "type": "integer" } } }, "updated_at": { "type": "string" }, "resolution": { "type": "object", "properties": { "outcome": { "type": "string" }, "reason": { "type": "string" }, "detail": { "type": "string" }, "observed_value": { "type": "number" }, "observed_on": { "type": "string" }, "observations": { "type": "integer" }, "question_id": { "type": "string" }, "question_class": { "type": "string" }, "resolved_on": { "type": "string" }, "probability_at_resolution": { "type": "number" }, "base_rate_at_resolution": { "type": "number" }, "brier": { "type": "number" }, "log_score": { "type": "number" }, "print_id": { "type": "string" }, "carryover_check": { "type": "string" }, "base_rate_brier": { "type": "number" } } }, "note": { "type": "string" } } } } }, "additionalProperties": true, "description": "Inferred from the live feed at build time. The field inventory reflects this collection run; optional fields may be absent and the shape is refreshed on every deploy." } } } }, "404": { "description": "Feed not published for this run.", "content": { "application/json": { "schema": { "type": "object", "required": [ "error" ], "properties": { "error": { "type": "object", "required": [ "code", "message" ], "properties": { "code": { "type": "string", "description": "A stable machine-readable code, e.g. unauthorized or not_found." }, "message": { "type": "string", "description": "A human-readable explanation." }, "hint": { "type": "string", "description": "Optional resolution pointer." } } } } } } } } } } ``` --- Source: https://china-snapshot.com/docs/api/get-forecast-track-record.md # Fetch forecasts/track_record getForecastTrackRecord · GET /data/forecasts/track_record.json ## Endpoint `GET /data/forecasts/track_record.json` The Forecast Desk track record in full, including misses and voids, so the score can be independently checked. ## Authentication ```json [] ``` An empty security requirement means this operation does not declare authentication. Read operation notes for entitlement and protocol-method requirements. ## Parameters No parameters declared by the specification. ## Responses and errors | HTTP status | Meaning | | --- | --- | | 200 | The requested feed. | | 404 | Feed not published for this run. | ## Response examples No response example is supplied by OpenAPI for this operation. The complete response schemas are preserved below; use an actual response for observed values. ## Retry guidance See [errors and retries](/docs/api/overview#errors-and-retries). Honor operation-specific response headers. A POST may create state; do not replay it automatically. ## Complete operation contract The expanded contract below includes all parameter constraints, response headers, schemas and examples from OpenAPI. ```json { "path": "/data/forecasts/track_record.json", "method": "GET", "parameters": [], "operationId": "getForecastTrackRecord", "summary": "Fetch forecasts/track_record", "description": "The Forecast Desk track record in full, including misses and voids, so the score can be independently checked.", "tags": [ "data" ], "security": [], "responses": { "200": { "description": "The requested feed.", "content": { "application/json": { "schema": { "type": "object", "properties": { "schema_version": { "type": "integer" }, "as_of": { "type": "string" }, "resolved": { "type": "integer" }, "void": { "type": "integer" }, "yes_rate": { "type": "number" }, "mean_brier": { "type": "number" }, "mean_log_score": { "type": "number" }, "base_rate_brier": { "type": "number" }, "base_rate_comparable_n": { "type": "integer" }, "brier_skill_vs_base_rate": { "type": "number" }, "calibration": { "type": "array", "items": { "type": "object", "properties": { "bucket": { "type": "string" }, "low": { "type": "number" }, "high": { "type": "number" }, "n": { "type": "integer" }, "observed_yes": { "type": "integer" }, "observed_rate": { "type": "number" }, "mean_probability": { "type": "number" } } } }, "calibration_building": { "type": "boolean" }, "min_resolved_for_calibration": { "type": "integer" }, "note": { "type": "string" }, "resolved_questions": { "type": "array", "items": { "type": "object", "properties": { "question_id": { "type": "string" }, "probability": { "type": "number" }, "base_rate": { "type": "number" }, "outcome": { "type": "string" }, "resolved_on": { "type": "string" }, "title": { "type": "string" }, "access": { "type": "string" } } } } }, "additionalProperties": true, "description": "Inferred from the live feed at build time. The field inventory reflects this collection run; optional fields may be absent and the shape is refreshed on every deploy." } } } }, "404": { "description": "Feed not published for this run.", "content": { "application/json": { "schema": { "type": "object", "required": [ "error" ], "properties": { "error": { "type": "object", "required": [ "code", "message" ], "properties": { "code": { "type": "string", "description": "A stable machine-readable code, e.g. unauthorized or not_found." }, "message": { "type": "string", "description": "A human-readable explanation." }, "hint": { "type": "string", "description": "Optional resolution pointer." } } } } } } } } } } ``` --- Source: https://china-snapshot.com/docs/api/get-signal-packet.md # Fetch signal-packet getSignalPacket · GET /data/signal-packet.json ## Endpoint `GET /data/signal-packet.json` The current validated public Signal Packet (export controls) as JSON. ## Authentication ```json [] ``` An empty security requirement means this operation does not declare authentication. Read operation notes for entitlement and protocol-method requirements. ## Parameters No parameters declared by the specification. ## Responses and errors | HTTP status | Meaning | | --- | --- | | 200 | The requested feed. | | 404 | Feed not published for this run. | ## Response examples No response example is supplied by OpenAPI for this operation. The complete response schemas are preserved below; use an actual response for observed values. ## Retry guidance See [errors and retries](/docs/api/overview#errors-and-retries). Honor operation-specific response headers. A POST may create state; do not replay it automatically. ## Complete operation contract The expanded contract below includes all parameter constraints, response headers, schemas and examples from OpenAPI. ```json { "path": "/data/signal-packet.json", "method": "GET", "parameters": [], "operationId": "getSignalPacket", "summary": "Fetch signal-packet", "description": "The current validated public Signal Packet (export controls) as JSON.", "tags": [ "data" ], "security": [], "responses": { "200": { "description": "The requested feed.", "content": { "application/json": { "schema": { "type": "object", "properties": { "schema_version": { "type": "integer" }, "packet_version": { "type": "integer" }, "packet_id": { "type": "string" }, "edition_id": { "type": "string" }, "topic_ids": { "type": "array", "items": { "type": "string" } }, "entity_ids": { "type": "array", "items": {} }, "institution_ids": { "type": "array", "items": { "type": "string" } }, "place_ids": { "type": "array", "items": {} }, "change": { "type": "object", "properties": { "change_type": { "type": "string" }, "measure_type": { "type": "string" }, "status": { "type": "string" }, "prior_state": { "type": "object", "properties": { "claim_id": { "type": "string" }, "claim_type": { "type": "string" }, "statement": { "type": "string" }, "material": { "type": "boolean" }, "support_status": { "type": "string" }, "evidence_ids": { "type": "array", "items": {} }, "calculation_ids": { "type": "array", "items": {} }, "basis_claim_ids": { "type": "array", "items": {} }, "provenance_kind": { "type": "string" }, "generated": { "type": "boolean" } } }, "materiality": { "type": "object", "properties": { "claim_id": { "type": "string" }, "claim_type": { "type": "string" }, "statement": { "type": "string" }, "material": { "type": "boolean" }, "support_status": { "type": "string" }, "evidence_ids": { "type": "array", "items": {} }, "calculation_ids": { "type": "array", "items": {} }, "basis_claim_ids": { "type": "array", "items": { "type": "string" } }, "provenance_kind": { "type": "string" }, "generated": { "type": "boolean" } } } } }, "observations": { "type": "array", "items": { "type": "object", "properties": { "claim_id": { "type": "string" }, "claim_type": { "type": "string" }, "statement": { "type": "string" }, "material": { "type": "boolean" }, "support_status": { "type": "string" }, "evidence_ids": { "type": "array", "items": { "type": "string" } }, "calculation_ids": { "type": "array", "items": {} }, "basis_claim_ids": { "type": "array", "items": {} }, "provenance_kind": { "type": "string" }, "generated": { "type": "boolean" }, "value": { "type": "object", "properties": { "type": { "type": "string" }, "value": { "type": [ "integer", "string" ] }, "unit": { "type": "string" } } } } } }, "significance": { "type": "array", "items": { "type": "object", "properties": { "claim_id": { "type": "string" }, "claim_type": { "type": "string" }, "statement": { "type": "string" }, "material": { "type": "boolean" }, "support_status": { "type": "string" }, "evidence_ids": { "type": "array", "items": {} }, "calculation_ids": { "type": "array", "items": {} }, "basis_claim_ids": { "type": "array", "items": { "type": "string" } }, "provenance_kind": { "type": "string" }, "generated": { "type": "boolean" } } } }, "interpretation": { "type": "array", "items": {} }, "evidence": { "type": "array", "items": { "type": "object", "properties": { "evidence_id": { "type": "string" }, "source_class": { "type": "string" }, "publisher_id": { "type": "string" }, "publisher_name": { "type": "string" }, "url": { "type": "string" }, "language": { "type": "string" }, "original_title": { "type": "string" }, "translated_title": { "type": "string" }, "translation_id": { "type": "string" }, "published_at": {}, "collected_at": { "type": "string" }, "supports_claim_ids": { "type": "array", "items": { "type": "string" } } } } }, "historical_comparisons": { "type": "array", "items": {} }, "uncertainties": { "type": "array", "items": { "type": "object", "properties": { "claim_id": { "type": "string" }, "claim_type": { "type": "string" }, "statement": { "type": "string" }, "material": { "type": "boolean" }, "support_status": { "type": "string" }, "evidence_ids": { "type": "array", "items": {} }, "calculation_ids": { "type": "array", "items": {} }, "basis_claim_ids": { "type": "array", "items": {} }, "provenance_kind": { "type": "string" }, "generated": { "type": "boolean" } } } }, "watch_next": { "type": "array", "items": { "type": "object", "properties": { "watch_id": { "type": "string" }, "institution_ids": { "type": "array", "items": { "type": "string" } }, "condition": { "type": "object", "properties": { "claim_id": { "type": "string" }, "claim_type": { "type": "string" }, "statement": { "type": "string" }, "material": { "type": "boolean" }, "support_status": { "type": "string" }, "evidence_ids": { "type": "array", "items": {} }, "calculation_ids": { "type": "array", "items": {} }, "basis_claim_ids": { "type": "array", "items": {} }, "provenance_kind": { "type": "string" }, "generated": { "type": "boolean" } } }, "expected_at": {}, "would_confirm_claim_ids": { "type": "array", "items": { "type": "string" } }, "would_invalidate_claim_ids": { "type": "array", "items": {} } } } }, "times": { "type": "object", "properties": { "event_at": {}, "published_at": {}, "effective_at": {}, "observed_at": { "type": "string" }, "collected_at": { "type": "string" }, "generated_at": { "type": "string" }, "updated_at": { "type": "string" } } }, "translations": { "type": "array", "items": { "type": "object", "properties": { "translation_id": { "type": "string" }, "source_evidence_id": { "type": "string" }, "source_language": { "type": "string" }, "target_language": { "type": "string" }, "field": { "type": "string" }, "model_provider": { "type": "string" }, "model": { "type": "string" }, "generated_at": {} } } }, "calculations": { "type": "array", "items": {} }, "confidence": { "type": "object", "properties": { "evidence": { "type": "object", "properties": { "level": { "type": "string" }, "score": { "type": "number" }, "rationale": { "type": "string" }, "claim_type": { "type": "string" } } }, "change_detection": { "type": "object", "properties": { "level": { "type": "string" }, "score": { "type": "number" }, "rationale": { "type": "string" }, "claim_type": { "type": "string" } } }, "interpretation": { "type": "object", "properties": { "level": { "type": "string" }, "score": {}, "rationale": { "type": "string" }, "claim_type": { "type": "string" } } }, "overall": { "type": "object", "properties": { "level": { "type": "string" }, "score": { "type": "number" }, "rationale": { "type": "string" }, "claim_type": { "type": "string" } } } } }, "lifecycle": { "type": "object", "properties": { "state": { "type": "string" }, "freshness": { "type": "string" }, "prior_packet_id": {}, "replacement_packet_id": {}, "reason": {} } }, "access": { "type": "object", "properties": { "classification": { "type": "string" }, "public_sample": { "type": "object", "properties": { "eligible": { "type": "boolean" }, "edition_packet_limit": { "type": "integer" }, "evidence_limit": { "type": "integer" }, "selected_evidence_ids": { "type": "array", "items": { "type": "string" } }, "projection_of_packet_id": { "type": "string" }, "selection_method": { "type": "string" } } } } } }, "additionalProperties": true, "description": "Inferred from the live feed at build time. The field inventory reflects this collection run; optional fields may be absent and the shape is refreshed on every deploy." } } } }, "404": { "description": "Feed not published for this run.", "content": { "application/json": { "schema": { "type": "object", "required": [ "error" ], "properties": { "error": { "type": "object", "required": [ "code", "message" ], "properties": { "code": { "type": "string", "description": "A stable machine-readable code, e.g. unauthorized or not_found." }, "message": { "type": "string", "description": "A human-readable explanation." }, "hint": { "type": "string", "description": "Optional resolution pointer." } } } } } } } } }, "x-codeSamples": [ { "lang": "curl", "label": "curl", "source": "curl -s 'https://china-snapshot.com/data/signal-packet.json'" } ] } ``` --- Source: https://china-snapshot.com/docs/api/get-daily-digest-markdown.md # Fetch the current briefing as Markdown getDailyDigestMarkdown · GET /data/digest.md ## Endpoint `GET /data/digest.md` The current cross-source synthesis as Markdown, served with Vary: Accept so Accept: text/markdown negotiation is cache-safe. The same document the human reads on the Snapshot page. A client that requests an unsupported representation receives 406. ## Authentication ```json [] ``` An empty security requirement means this operation does not declare authentication. Read operation notes for entitlement and protocol-method requirements. ## Parameters No parameters declared by the specification. ## Responses and errors | HTTP status | Meaning | | --- | --- | | 200 | The briefing in Markdown. | | 406 | No supported representation matches the Accept header. Retry with Accept: text/html or Accept: text/markdown. | ## Response examples No response example is supplied by OpenAPI for this operation. The complete response schemas are preserved below; use an actual response for observed values. ## Retry guidance See [errors and retries](/docs/api/overview#errors-and-retries). Honor operation-specific response headers. A POST may create state; do not replay it automatically. ## Complete operation contract The expanded contract below includes all parameter constraints, response headers, schemas and examples from OpenAPI. ```json { "path": "/data/digest.md", "method": "GET", "parameters": [], "operationId": "getDailyDigestMarkdown", "summary": "Fetch the current briefing as Markdown", "description": "The current cross-source synthesis as Markdown, served with Vary: Accept so Accept: text/markdown negotiation is cache-safe. The same document the human reads on the Snapshot page. A client that requests an unsupported representation receives 406.", "tags": [ "data" ], "security": [], "responses": { "200": { "description": "The briefing in Markdown.", "content": { "text/markdown": { "schema": { "type": "string" } } } }, "406": { "description": "No supported representation matches the Accept header. Retry with Accept: text/html or Accept: text/markdown.", "content": { "application/problem+json": { "schema": { "type": "object", "required": [ "error" ], "properties": { "error": { "type": "object", "required": [ "code", "message" ], "properties": { "code": { "type": "string", "description": "A stable machine-readable code, e.g. unauthorized or not_found." }, "message": { "type": "string", "description": "A human-readable explanation." }, "hint": { "type": "string", "description": "Optional resolution pointer." } } } } } } } } }, "x-codeSamples": [ { "lang": "curl", "label": "curl", "source": "curl -s 'https://china-snapshot.com/data/digest.md'" } ] } ``` --- Source: https://china-snapshot.com/docs/api/get-mcp-manifest.md # The MCP server manifest getMcpManifest · GET /.well-known/mcp.json ## Endpoint `GET /.well-known/mcp.json` A standards-based MCP manifest declaring the Streamable HTTP endpoint, its eleven tools, capabilities, rate limits, and authentication, so clients that discover MCP servers by manifest can find this one. ## Authentication ```json [] ``` An empty security requirement means this operation does not declare authentication. Read operation notes for entitlement and protocol-method requirements. ## Parameters No parameters declared by the specification. ## Responses and errors | HTTP status | Meaning | | --- | --- | | 200 | The MCP manifest. | ## Response examples No response example is supplied by OpenAPI for this operation. The complete response schemas are preserved below; use an actual response for observed values. ## Retry guidance See [errors and retries](/docs/api/overview#errors-and-retries). Honor operation-specific response headers. A POST may create state; do not replay it automatically. ## Complete operation contract The expanded contract below includes all parameter constraints, response headers, schemas and examples from OpenAPI. ```json { "path": "/.well-known/mcp.json", "method": "GET", "parameters": [], "operationId": "getMcpManifest", "summary": "The MCP server manifest", "description": "A standards-based MCP manifest declaring the Streamable HTTP endpoint, its eleven tools, capabilities, rate limits, and authentication, so clients that discover MCP servers by manifest can find this one.", "tags": [ "mcp" ], "security": [], "responses": { "200": { "description": "The MCP manifest.", "content": { "application/json": { "schema": { "type": "object", "required": [ "mcpServers" ], "properties": { "mcpServers": { "type": "object", "additionalProperties": { "type": "object", "required": [ "url" ], "properties": { "url": { "type": "string", "format": "uri" }, "transport": { "type": "string" }, "description": { "type": "string" }, "capabilities": { "type": "object", "additionalProperties": true }, "rate_limits": { "type": "object", "additionalProperties": true }, "auth": { "type": "object", "additionalProperties": true } } } } } } } } } }, "x-codeSamples": [ { "lang": "curl", "label": "curl", "source": "curl -s 'https://china-snapshot.com/.well-known/mcp.json'" } ] } ``` --- Source: https://china-snapshot.com/docs/api/api-not-found.md # Catch-all for undocumented API routes apiNotFound · GET /api/{path} ## Endpoint `GET /api/{path}` Any /api/* route no other handler owns returns this structured JSON 404 with an X-Api-Version header, so an agent probing an undocumented route gets a parseable envelope, never the HTML app shell. ## Authentication ```json [] ``` An empty security requirement means this operation does not declare authentication. Read operation notes for entitlement and protocol-method requirements. ## Parameters | Name | In | Required | Description | | --- | --- | --- | --- | | path | path | Yes | | ## Responses and errors | HTTP status | Meaning | | --- | --- | | 404 | The requested resource or operation does not exist. JSON-RPC maps 404 to -32002. | ## Response examples No response example is supplied by OpenAPI for this operation. The complete response schemas are preserved below; use an actual response for observed values. ## Retry guidance See [errors and retries](/docs/api/overview#errors-and-retries). Honor operation-specific response headers. A POST may create state; do not replay it automatically. ## Complete operation contract The expanded contract below includes all parameter constraints, response headers, schemas and examples from OpenAPI. ```json { "path": "/api/{path}", "method": "GET", "parameters": [ { "name": "path", "in": "path", "required": true, "schema": { "type": "string" } } ], "operationId": "apiNotFound", "summary": "Catch-all for undocumented API routes", "description": "Any /api/* route no other handler owns returns this structured JSON 404 with an X-Api-Version header, so an agent probing an undocumented route gets a parseable envelope, never the HTML app shell.", "tags": [ "research" ], "security": [], "responses": { "404": { "description": "The requested resource or operation does not exist. JSON-RPC maps 404 to -32002.", "content": { "application/json": { "schema": { "type": "object", "required": [ "error" ], "properties": { "error": { "type": "object", "required": [ "code", "message" ], "properties": { "code": { "type": "string", "description": "A stable machine-readable code, e.g. unauthorized or not_found." }, "message": { "type": "string", "description": "A human-readable explanation." }, "hint": { "type": "string", "description": "Optional resolution pointer." } } } } } } } } } } ``` --- Source: https://china-snapshot.com/docs/api/api-not-found-post.md # Catch-all for undocumented API routes (POST) apiNotFoundPost · POST /api/{path} ## Endpoint `POST /api/{path}` Structured JSON 404 for POST to an undocumented /api/* route. ## Authentication ```json [] ``` An empty security requirement means this operation does not declare authentication. Read operation notes for entitlement and protocol-method requirements. ## Parameters | Name | In | Required | Description | | --- | --- | --- | --- | | path | path | Yes | | ## Responses and errors | HTTP status | Meaning | | --- | --- | | 404 | The requested resource or operation does not exist. JSON-RPC maps 404 to -32002. | ## Response examples No response example is supplied by OpenAPI for this operation. The complete response schemas are preserved below; use an actual response for observed values. ## Retry guidance See [errors and retries](/docs/api/overview#errors-and-retries). Honor operation-specific response headers. A POST may create state; do not replay it automatically. ## Complete operation contract The expanded contract below includes all parameter constraints, response headers, schemas and examples from OpenAPI. ```json { "path": "/api/{path}", "method": "POST", "parameters": [ { "name": "path", "in": "path", "required": true, "schema": { "type": "string" } } ], "operationId": "apiNotFoundPost", "summary": "Catch-all for undocumented API routes (POST)", "description": "Structured JSON 404 for POST to an undocumented /api/* route.", "tags": [ "research" ], "security": [], "responses": { "404": { "description": "The requested resource or operation does not exist. JSON-RPC maps 404 to -32002.", "content": { "application/json": { "schema": { "type": "object", "required": [ "error" ], "properties": { "error": { "type": "object", "required": [ "code", "message" ], "properties": { "code": { "type": "string", "description": "A stable machine-readable code, e.g. unauthorized or not_found." }, "message": { "type": "string", "description": "A human-readable explanation." }, "hint": { "type": "string", "description": "Optional resolution pointer." } } } } } } } } } } ``` --- Source: https://china-snapshot.com/docs/api/research-discovery.md # Describe the Professional research API researchDiscovery · GET /api/research ## Endpoint `GET /api/research` Unauthenticated discovery response. Describes the endpoint, the eleven operations, authentication, quotas, and links to pricing and account. Actual research calls are POST-only. ## Authentication ```json [] ``` An empty security requirement means this operation does not declare authentication. Read operation notes for entitlement and protocol-method requirements. ## Parameters No parameters declared by the specification. ## Responses and errors | HTTP status | Meaning | | --- | --- | | 200 | The self-describing discovery body. | ## Response examples No response example is supplied by OpenAPI for this operation. The complete response schemas are preserved below; use an actual response for observed values. ## Retry guidance See [errors and retries](/docs/api/overview#errors-and-retries). Honor operation-specific response headers. A POST may create state; do not replay it automatically. ## Complete operation contract The expanded contract below includes all parameter constraints, response headers, schemas and examples from OpenAPI. ```json { "path": "/api/research", "method": "GET", "parameters": [], "operationId": "researchDiscovery", "summary": "Describe the Professional research API", "description": "Unauthenticated discovery response. Describes the endpoint, the eleven operations, authentication, quotas, and links to pricing and account. Actual research calls are POST-only.", "tags": [ "research" ], "security": [], "responses": { "200": { "description": "The self-describing discovery body.", "content": { "application/json": { "schema": { "type": "object" } } } } }, "x-codeSamples": [ { "lang": "curl", "label": "curl", "source": "curl -s 'https://china-snapshot.com/api/research'" } ] } ``` --- Source: https://china-snapshot.com/docs/api/research-invoke.md # Run one Professional research operation researchInvoke · POST /api/research ## Endpoint `POST /api/research` Runs one of the eleven operations: get_digest, get_signal_packet, export_signal_packet, search_stories, get_entity_timeline, find_records, get_record, get_observations, get_changes, export_records, list_watchlists. Requires a Professional credential with the api_access entitlement. Metered against the monthly allowance; an exhausted allowance returns 429 with Retry-After and X-RateLimit-* headers and never charges an overage. ## Authentication ```json [] ``` An empty security requirement means this operation does not declare authentication. Read operation notes for entitlement and protocol-method requirements. ## Parameters No parameters declared by the specification. ## Request body ```json { "required": true, "content": { "application/json": { "schema": { "type": "object", "required": [ "operation", "arguments" ], "properties": { "operation": { "type": "string", "enum": [ "get_digest", "get_signal_packet", "export_signal_packet", "search_stories", "get_entity_timeline", "find_records", "get_record", "get_observations", "get_changes", "export_records", "list_watchlists" ], "description": "The research operation to run." }, "arguments": { "type": "object", "description": "Operation arguments, validated against the operation's typed input schema.", "oneOf": [ { "type": "object", "properties": {}, "required": [], "additionalProperties": false }, { "type": "object", "properties": { "packet_id": { "type": "string", "maxLength": 180 } }, "required": [], "additionalProperties": false }, { "type": "object", "properties": { "packet_id": { "type": "string", "maxLength": 180 }, "format": { "type": "string", "enum": [ "markdown", "json" ] } }, "required": [ "format" ], "additionalProperties": false }, { "type": "object", "properties": { "query": { "type": "string", "maxLength": 180 }, "limit": { "type": "integer", "minimum": 1, "maximum": 20 } }, "required": [ "query" ], "additionalProperties": false }, { "type": "object", "properties": { "entity": { "type": "string", "maxLength": 120 }, "limit": { "type": "integer", "minimum": 1, "maximum": 20 } }, "required": [ "entity" ], "additionalProperties": false }, { "type": "object", "properties": { "dataset": { "type": "string", "enum": [ "documents", "social", "indicators" ], "description": "Default documents." }, "q": { "type": "string", "maxLength": 180, "description": "Literal case-insensitive substring of title or publisher." }, "source": { "type": "string", "maxLength": 180, "description": "Exact publisher label." }, "from": { "type": "string", "description": "Inclusive UTC collection day, YYYY-MM-DD (first_seen for documents, captured_at otherwise)." }, "to": { "type": "string", "description": "Inclusive UTC collection day, YYYY-MM-DD." }, "text": { "type": "string", "enum": [ "available" ], "description": "Documents only: records with approved original text." }, "mode": { "type": "string", "enum": [ "raw", "release", "topics" ], "description": "raw (default) = one row per record or collection run; release = indicators only, one row per series and period; topics = social only, one row per (platform, topic) within the window (default 30 days, max 92)." }, "watch": { "type": "string", "enum": [ "data-security", "export-controls", "property-support", "semiconductors" ], "description": "Reviewed topic filter (see list_watchlists): a title match on a listed bilingual term, nothing inferred." }, "topic": { "type": "string", "enum": [ "data-security", "export-controls", "property-support", "semiconductors" ], "description": "Alias of watch." }, "change_type": { "type": "string", "enum": [ "new", "newly_discovered", "revised" ], "description": "Documents only: new, newly_discovered (published more than 2 days before we first saw it) or revised (a changed body was observed)." }, "series": { "type": "string", "description": "Indicators only: exact series_id such as cpi-yoy." }, "cursor": { "type": "string", "description": "next_cursor from the previous page, with the same filters. Valid 24 hours." }, "fields": { "type": "string", "description": "Comma-separated field keys; default all." }, "limit": { "type": "integer", "minimum": 1, "maximum": 20 } }, "required": [], "additionalProperties": false }, { "type": "object", "properties": { "id": { "type": "string", "pattern": "^(documents|social|indicators):[1-9][0-9]*$" } }, "required": [ "id" ], "additionalProperties": false }, { "type": "object", "properties": { "series_id": { "type": "string", "pattern": "^[a-z0-9]+(-[a-z0-9]+)*$", "maxLength": 120 }, "from": { "type": "string", "description": "Earliest reference period, YYYY-MM or YYYY-MM-DD." }, "to": { "type": "string", "description": "Latest reference period, inclusive." } }, "required": [ "series_id" ], "additionalProperties": false }, { "type": "object", "properties": { "since": { "type": "string", "maxLength": 400, "description": "ISO-8601 instant with timezone (inclusive), or a next_since token." }, "cursor": { "type": "string", "maxLength": 400, "description": "next_cursor from the previous call." }, "watch": { "type": "string", "enum": [ "data-security", "export-controls", "property-support", "semiconductors" ] }, "topic": { "type": "string", "enum": [ "data-security", "export-controls", "property-support", "semiconductors" ], "description": "Alias of watch." }, "source": { "type": "string", "maxLength": 180 }, "q": { "type": "string", "maxLength": 180 }, "limit": { "type": "integer", "minimum": 1, "maximum": 20 } }, "required": [], "additionalProperties": false }, { "type": "object", "properties": { "dataset": { "type": "string", "enum": [ "documents", "social", "indicators" ], "description": "Default documents." }, "q": { "type": "string", "maxLength": 180, "description": "Literal case-insensitive substring of title or publisher." }, "source": { "type": "string", "maxLength": 180, "description": "Exact publisher label." }, "from": { "type": "string", "description": "Inclusive UTC collection day, YYYY-MM-DD (first_seen for documents, captured_at otherwise)." }, "to": { "type": "string", "description": "Inclusive UTC collection day, YYYY-MM-DD." }, "text": { "type": "string", "enum": [ "available" ], "description": "Documents only: records with approved original text." }, "mode": { "type": "string", "enum": [ "raw", "release", "topics" ], "description": "raw (default) = one row per record or collection run; release = indicators only, one row per series and period; topics = social only, one row per (platform, topic) within the window (default 30 days, max 92)." }, "watch": { "type": "string", "enum": [ "data-security", "export-controls", "property-support", "semiconductors" ], "description": "Reviewed topic filter (see list_watchlists): a title match on a listed bilingual term, nothing inferred." }, "topic": { "type": "string", "enum": [ "data-security", "export-controls", "property-support", "semiconductors" ], "description": "Alias of watch." }, "change_type": { "type": "string", "enum": [ "new", "newly_discovered", "revised" ], "description": "Documents only: new, newly_discovered (published more than 2 days before we first saw it) or revised (a changed body was observed)." }, "series": { "type": "string", "description": "Indicators only: exact series_id such as cpi-yoy." }, "cursor": { "type": "string", "description": "next_cursor from the previous page, with the same filters. Valid 24 hours." }, "fields": { "type": "string", "description": "Comma-separated field keys; default all." }, "format": { "type": "string", "enum": [ "csv", "json" ] }, "limit": { "type": "integer", "minimum": 1, "maximum": 100 } }, "required": [ "format" ], "additionalProperties": false }, { "type": "object", "properties": {}, "required": [], "additionalProperties": false } ] } } } } } } ``` ## Responses and errors | HTTP status | Meaning | | --- | --- | | 200 | The operation result with usage metering. | | 400 | Invalid request. JSON-RPC maps 400 to -32602. | | 401 | Missing or invalid credential. JSON-RPC maps 401 to -32001. | | 403 | Authenticated but not entitled. JSON-RPC maps 403 to -32003. | | 404 | The requested resource or operation does not exist. JSON-RPC maps 404 to -32002. | | 429 | Monthly allowance reached; no overage is charged. Retry after the calendar-month reset. Response carries RateLimit-* and X-RateLimit-* headers plus Retry-After. | ## Response examples No response example is supplied by OpenAPI for this operation. The complete response schemas are preserved below; use an actual response for observed values. ## Retry guidance See [errors and retries](/docs/api/overview#errors-and-retries). Honor operation-specific response headers. A POST may create state; do not replay it automatically. ## Complete operation contract The expanded contract below includes all parameter constraints, response headers, schemas and examples from OpenAPI. ```json { "path": "/api/research", "method": "POST", "parameters": [], "operationId": "researchInvoke", "summary": "Run one Professional research operation", "description": "Runs one of the eleven operations: get_digest, get_signal_packet, export_signal_packet, search_stories, get_entity_timeline, find_records, get_record, get_observations, get_changes, export_records, list_watchlists. Requires a Professional credential with the api_access entitlement. Metered against the monthly allowance; an exhausted allowance returns 429 with Retry-After and X-RateLimit-* headers and never charges an overage.", "tags": [ "research" ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "required": [ "operation", "arguments" ], "properties": { "operation": { "type": "string", "enum": [ "get_digest", "get_signal_packet", "export_signal_packet", "search_stories", "get_entity_timeline", "find_records", "get_record", "get_observations", "get_changes", "export_records", "list_watchlists" ], "description": "The research operation to run." }, "arguments": { "type": "object", "description": "Operation arguments, validated against the operation's typed input schema.", "oneOf": [ { "type": "object", "properties": {}, "required": [], "additionalProperties": false }, { "type": "object", "properties": { "packet_id": { "type": "string", "maxLength": 180 } }, "required": [], "additionalProperties": false }, { "type": "object", "properties": { "packet_id": { "type": "string", "maxLength": 180 }, "format": { "type": "string", "enum": [ "markdown", "json" ] } }, "required": [ "format" ], "additionalProperties": false }, { "type": "object", "properties": { "query": { "type": "string", "maxLength": 180 }, "limit": { "type": "integer", "minimum": 1, "maximum": 20 } }, "required": [ "query" ], "additionalProperties": false }, { "type": "object", "properties": { "entity": { "type": "string", "maxLength": 120 }, "limit": { "type": "integer", "minimum": 1, "maximum": 20 } }, "required": [ "entity" ], "additionalProperties": false }, { "type": "object", "properties": { "dataset": { "type": "string", "enum": [ "documents", "social", "indicators" ], "description": "Default documents." }, "q": { "type": "string", "maxLength": 180, "description": "Literal case-insensitive substring of title or publisher." }, "source": { "type": "string", "maxLength": 180, "description": "Exact publisher label." }, "from": { "type": "string", "description": "Inclusive UTC collection day, YYYY-MM-DD (first_seen for documents, captured_at otherwise)." }, "to": { "type": "string", "description": "Inclusive UTC collection day, YYYY-MM-DD." }, "text": { "type": "string", "enum": [ "available" ], "description": "Documents only: records with approved original text." }, "mode": { "type": "string", "enum": [ "raw", "release", "topics" ], "description": "raw (default) = one row per record or collection run; release = indicators only, one row per series and period; topics = social only, one row per (platform, topic) within the window (default 30 days, max 92)." }, "watch": { "type": "string", "enum": [ "data-security", "export-controls", "property-support", "semiconductors" ], "description": "Reviewed topic filter (see list_watchlists): a title match on a listed bilingual term, nothing inferred." }, "topic": { "type": "string", "enum": [ "data-security", "export-controls", "property-support", "semiconductors" ], "description": "Alias of watch." }, "change_type": { "type": "string", "enum": [ "new", "newly_discovered", "revised" ], "description": "Documents only: new, newly_discovered (published more than 2 days before we first saw it) or revised (a changed body was observed)." }, "series": { "type": "string", "description": "Indicators only: exact series_id such as cpi-yoy." }, "cursor": { "type": "string", "description": "next_cursor from the previous page, with the same filters. Valid 24 hours." }, "fields": { "type": "string", "description": "Comma-separated field keys; default all." }, "limit": { "type": "integer", "minimum": 1, "maximum": 20 } }, "required": [], "additionalProperties": false }, { "type": "object", "properties": { "id": { "type": "string", "pattern": "^(documents|social|indicators):[1-9][0-9]*$" } }, "required": [ "id" ], "additionalProperties": false }, { "type": "object", "properties": { "series_id": { "type": "string", "pattern": "^[a-z0-9]+(-[a-z0-9]+)*$", "maxLength": 120 }, "from": { "type": "string", "description": "Earliest reference period, YYYY-MM or YYYY-MM-DD." }, "to": { "type": "string", "description": "Latest reference period, inclusive." } }, "required": [ "series_id" ], "additionalProperties": false }, { "type": "object", "properties": { "since": { "type": "string", "maxLength": 400, "description": "ISO-8601 instant with timezone (inclusive), or a next_since token." }, "cursor": { "type": "string", "maxLength": 400, "description": "next_cursor from the previous call." }, "watch": { "type": "string", "enum": [ "data-security", "export-controls", "property-support", "semiconductors" ] }, "topic": { "type": "string", "enum": [ "data-security", "export-controls", "property-support", "semiconductors" ], "description": "Alias of watch." }, "source": { "type": "string", "maxLength": 180 }, "q": { "type": "string", "maxLength": 180 }, "limit": { "type": "integer", "minimum": 1, "maximum": 20 } }, "required": [], "additionalProperties": false }, { "type": "object", "properties": { "dataset": { "type": "string", "enum": [ "documents", "social", "indicators" ], "description": "Default documents." }, "q": { "type": "string", "maxLength": 180, "description": "Literal case-insensitive substring of title or publisher." }, "source": { "type": "string", "maxLength": 180, "description": "Exact publisher label." }, "from": { "type": "string", "description": "Inclusive UTC collection day, YYYY-MM-DD (first_seen for documents, captured_at otherwise)." }, "to": { "type": "string", "description": "Inclusive UTC collection day, YYYY-MM-DD." }, "text": { "type": "string", "enum": [ "available" ], "description": "Documents only: records with approved original text." }, "mode": { "type": "string", "enum": [ "raw", "release", "topics" ], "description": "raw (default) = one row per record or collection run; release = indicators only, one row per series and period; topics = social only, one row per (platform, topic) within the window (default 30 days, max 92)." }, "watch": { "type": "string", "enum": [ "data-security", "export-controls", "property-support", "semiconductors" ], "description": "Reviewed topic filter (see list_watchlists): a title match on a listed bilingual term, nothing inferred." }, "topic": { "type": "string", "enum": [ "data-security", "export-controls", "property-support", "semiconductors" ], "description": "Alias of watch." }, "change_type": { "type": "string", "enum": [ "new", "newly_discovered", "revised" ], "description": "Documents only: new, newly_discovered (published more than 2 days before we first saw it) or revised (a changed body was observed)." }, "series": { "type": "string", "description": "Indicators only: exact series_id such as cpi-yoy." }, "cursor": { "type": "string", "description": "next_cursor from the previous page, with the same filters. Valid 24 hours." }, "fields": { "type": "string", "description": "Comma-separated field keys; default all." }, "format": { "type": "string", "enum": [ "csv", "json" ] }, "limit": { "type": "integer", "minimum": 1, "maximum": 100 } }, "required": [ "format" ], "additionalProperties": false }, { "type": "object", "properties": {}, "required": [], "additionalProperties": false } ] } } } } } }, "responses": { "200": { "description": "The operation result with usage metering.", "content": { "application/json": { "schema": { "type": "object", "properties": { "result": { "type": "object", "additionalProperties": true, "description": "The operation's result object. Field shape varies by operation; each operation carries the honesty fields its tool description names (coverage, low_confidence, preview, belongs_to_current_edition, retained_last_known_good, and the limits thresholds for the search tools)." }, "usage": { "type": "object", "properties": { "used": { "type": "integer" }, "limit": { "type": "integer" } } } } } } } }, "400": { "description": "Invalid request. JSON-RPC maps 400 to -32602.", "content": { "application/json": { "schema": { "type": "object", "required": [ "error" ], "properties": { "error": { "type": "object", "required": [ "code", "message" ], "properties": { "code": { "type": "string", "description": "A stable machine-readable code, e.g. unauthorized or not_found." }, "message": { "type": "string", "description": "A human-readable explanation." }, "hint": { "type": "string", "description": "Optional resolution pointer." } } } } } } } }, "401": { "description": "Missing or invalid credential. JSON-RPC maps 401 to -32001.", "content": { "application/json": { "schema": { "type": "object", "required": [ "error" ], "properties": { "error": { "type": "object", "required": [ "code", "message" ], "properties": { "code": { "type": "string", "description": "A stable machine-readable code, e.g. unauthorized or not_found." }, "message": { "type": "string", "description": "A human-readable explanation." }, "hint": { "type": "string", "description": "Optional resolution pointer." } } } } } } } }, "403": { "description": "Authenticated but not entitled. JSON-RPC maps 403 to -32003.", "content": { "application/json": { "schema": { "type": "object", "required": [ "error" ], "properties": { "error": { "type": "object", "required": [ "code", "message" ], "properties": { "code": { "type": "string", "description": "A stable machine-readable code, e.g. unauthorized or not_found." }, "message": { "type": "string", "description": "A human-readable explanation." }, "hint": { "type": "string", "description": "Optional resolution pointer." } } } } } } } }, "404": { "description": "The requested resource or operation does not exist. JSON-RPC maps 404 to -32002.", "content": { "application/json": { "schema": { "type": "object", "required": [ "error" ], "properties": { "error": { "type": "object", "required": [ "code", "message" ], "properties": { "code": { "type": "string", "description": "A stable machine-readable code, e.g. unauthorized or not_found." }, "message": { "type": "string", "description": "A human-readable explanation." }, "hint": { "type": "string", "description": "Optional resolution pointer." } } } } } } } }, "429": { "description": "Monthly allowance reached; no overage is charged. Retry after the calendar-month reset. Response carries RateLimit-* and X-RateLimit-* headers plus Retry-After.", "headers": { "Retry-After": { "schema": { "type": "integer" } }, "RateLimit-Limit": { "schema": { "type": "integer" } }, "RateLimit-Remaining": { "schema": { "type": "integer" } }, "RateLimit-Reset": { "schema": { "type": "integer" } } }, "content": { "application/json": { "schema": { "type": "object", "required": [ "error" ], "properties": { "error": { "type": "object", "required": [ "code", "message" ], "properties": { "code": { "type": "string", "description": "A stable machine-readable code, e.g. unauthorized or not_found." }, "message": { "type": "string", "description": "A human-readable explanation." }, "hint": { "type": "string", "description": "Optional resolution pointer." } } } } } } } } }, "x-codeSamples": [ { "lang": "curl", "label": "curl (Pro key)", "source": "curl -s 'https://china-snapshot.com/api/research' \\\n -H \"Authorization: Bearer $CS_API_KEY\" \\\n -H 'Content-Type: application/json' \\\n -d '{\"operation\":\"find_records\",\"arguments\":{\"q\":\"export\",\"limit\":3}}'" } ] } ``` --- Source: https://china-snapshot.com/docs/api/mcp-discovery.md # Describe the MCP Streamable HTTP endpoint mcpDiscovery · GET /api/mcp ## Endpoint `GET /api/mcp` Unauthenticated discovery response. The endpoint implements JSON-RPC 2.0 over POST (MCP Streamable HTTP). initialize, notifications/initialized, and ping are unauthenticated protocol methods; tools/list and tools/call need an API key (a free key or Pro). The tool names are the operation names shared with the REST surface. ## Authentication ```json [] ``` An empty security requirement means this operation does not declare authentication. Read operation notes for entitlement and protocol-method requirements. ## Parameters No parameters declared by the specification. ## Responses and errors | HTTP status | Meaning | | --- | --- | | 200 | The self-describing MCP discovery body. | ## Response examples No response example is supplied by OpenAPI for this operation. The complete response schemas are preserved below; use an actual response for observed values. ## Retry guidance See [errors and retries](/docs/api/overview#errors-and-retries). Honor operation-specific response headers. A POST may create state; do not replay it automatically. ## Complete operation contract The expanded contract below includes all parameter constraints, response headers, schemas and examples from OpenAPI. ```json { "path": "/api/mcp", "method": "GET", "parameters": [], "operationId": "mcpDiscovery", "summary": "Describe the MCP Streamable HTTP endpoint", "description": "Unauthenticated discovery response. The endpoint implements JSON-RPC 2.0 over POST (MCP Streamable HTTP). initialize, notifications/initialized, and ping are unauthenticated protocol methods; tools/list and tools/call need an API key (a free key or Pro). The tool names are the operation names shared with the REST surface.", "tags": [ "mcp" ], "security": [], "responses": { "200": { "description": "The self-describing MCP discovery body.", "content": { "application/json": { "schema": { "type": "object" } } } } }, "x-codeSamples": [ { "lang": "curl", "label": "curl", "source": "curl -s 'https://china-snapshot.com/api/mcp'" } ] } ``` --- Source: https://china-snapshot.com/docs/api/mcp-json-rpc.md # Send a JSON-RPC 2.0 message to the MCP endpoint mcpJsonRpc · POST /api/mcp ## Endpoint `POST /api/mcp` MCP Streamable HTTP JSON-RPC transport. Accepts initialize, notifications/initialized, ping, tools/list, and tools/call. tools/call requires an API key (a free key or Pro) and is metered against the key's monthly allowance. ## Authentication ```json [] ``` An empty security requirement means this operation does not declare authentication. Read operation notes for entitlement and protocol-method requirements. ## Parameters No parameters declared by the specification. ## Request body ```json { "required": true, "content": { "application/json": { "schema": { "type": "object", "properties": { "jsonrpc": { "type": "string", "const": "2.0" }, "id": { "type": [ "integer", "string", "null" ] }, "method": { "type": "string", "enum": [ "initialize", "ping", "tools/list", "tools/call" ] }, "params": { "type": "object" } } } } } } ``` ## Responses and errors | HTTP status | Meaning | | --- | --- | | 200 | The JSON-RPC result object. | | 202 | Accepted for a notification (no body). | | 400 | Invalid request. JSON-RPC maps 400 to -32602. | | 401 | Missing or invalid credential. JSON-RPC maps 401 to -32001. | | 403 | Authenticated but not entitled. JSON-RPC maps 403 to -32003. | | 404 | The requested resource or operation does not exist. JSON-RPC maps 404 to -32002. | | 429 | Monthly allowance reached; no overage is charged. Retry after the calendar-month reset. Response carries RateLimit-* and X-RateLimit-* headers plus Retry-After. | ## Response examples No response example is supplied by OpenAPI for this operation. The complete response schemas are preserved below; use an actual response for observed values. ## Retry guidance See [errors and retries](/docs/api/overview#errors-and-retries). Honor operation-specific response headers. A POST may create state; do not replay it automatically. ## Complete operation contract The expanded contract below includes all parameter constraints, response headers, schemas and examples from OpenAPI. ```json { "path": "/api/mcp", "method": "POST", "parameters": [], "operationId": "mcpJsonRpc", "summary": "Send a JSON-RPC 2.0 message to the MCP endpoint", "description": "MCP Streamable HTTP JSON-RPC transport. Accepts initialize, notifications/initialized, ping, tools/list, and tools/call. tools/call requires an API key (a free key or Pro) and is metered against the key's monthly allowance.", "tags": [ "mcp" ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "properties": { "jsonrpc": { "type": "string", "const": "2.0" }, "id": { "type": [ "integer", "string", "null" ] }, "method": { "type": "string", "enum": [ "initialize", "ping", "tools/list", "tools/call" ] }, "params": { "type": "object" } } } } } }, "responses": { "200": { "description": "The JSON-RPC result object.", "content": { "application/json": { "schema": { "type": "object" } } } }, "202": { "description": "Accepted for a notification (no body)." }, "400": { "description": "Invalid request. JSON-RPC maps 400 to -32602.", "content": { "application/json": { "schema": { "type": "object", "required": [ "error" ], "properties": { "error": { "type": "object", "required": [ "code", "message" ], "properties": { "code": { "type": "string", "description": "A stable machine-readable code, e.g. unauthorized or not_found." }, "message": { "type": "string", "description": "A human-readable explanation." }, "hint": { "type": "string", "description": "Optional resolution pointer." } } } } } } } }, "401": { "description": "Missing or invalid credential. JSON-RPC maps 401 to -32001.", "content": { "application/json": { "schema": { "type": "object", "required": [ "error" ], "properties": { "error": { "type": "object", "required": [ "code", "message" ], "properties": { "code": { "type": "string", "description": "A stable machine-readable code, e.g. unauthorized or not_found." }, "message": { "type": "string", "description": "A human-readable explanation." }, "hint": { "type": "string", "description": "Optional resolution pointer." } } } } } } } }, "403": { "description": "Authenticated but not entitled. JSON-RPC maps 403 to -32003.", "content": { "application/json": { "schema": { "type": "object", "required": [ "error" ], "properties": { "error": { "type": "object", "required": [ "code", "message" ], "properties": { "code": { "type": "string", "description": "A stable machine-readable code, e.g. unauthorized or not_found." }, "message": { "type": "string", "description": "A human-readable explanation." }, "hint": { "type": "string", "description": "Optional resolution pointer." } } } } } } } }, "404": { "description": "The requested resource or operation does not exist. JSON-RPC maps 404 to -32002.", "content": { "application/json": { "schema": { "type": "object", "required": [ "error" ], "properties": { "error": { "type": "object", "required": [ "code", "message" ], "properties": { "code": { "type": "string", "description": "A stable machine-readable code, e.g. unauthorized or not_found." }, "message": { "type": "string", "description": "A human-readable explanation." }, "hint": { "type": "string", "description": "Optional resolution pointer." } } } } } } } }, "429": { "description": "Monthly allowance reached; no overage is charged. Retry after the calendar-month reset. Response carries RateLimit-* and X-RateLimit-* headers plus Retry-After.", "headers": { "Retry-After": { "schema": { "type": "integer" } }, "RateLimit-Limit": { "schema": { "type": "integer" } }, "RateLimit-Remaining": { "schema": { "type": "integer" } }, "RateLimit-Reset": { "schema": { "type": "integer" } } }, "content": { "application/json": { "schema": { "type": "object", "required": [ "error" ], "properties": { "error": { "type": "object", "required": [ "code", "message" ], "properties": { "code": { "type": "string", "description": "A stable machine-readable code, e.g. unauthorized or not_found." }, "message": { "type": "string", "description": "A human-readable explanation." }, "hint": { "type": "string", "description": "Optional resolution pointer." } } } } } } } } }, "x-codeSamples": [ { "lang": "curl", "label": "curl (Pro key)", "source": "curl -s 'https://china-snapshot.com/api/mcp' \\\n -H \"Authorization: Bearer $CS_API_KEY\" \\\n -H 'Content-Type: application/json' \\\n -d '{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"tools/call\",\"params\":{\"name\":\"find_records\",\"arguments\":{\"q\":\"export\",\"limit\":3}}}'" } ] } ``` --- Source: https://china-snapshot.com/docs/api/request-api-key.md # Request a Free API key by email requestApiKey · POST /api/key_signup ## Endpoint `POST /api/key_signup` Self-serve, no account. Issues one Free key per email address and emails it with a quickstart (subject 'Your China Snapshot API key (and a 30-second start)', from contact@china-snapshot.com). The key is NOT in the response: read it from the email. Only when mail delivery fails is a Free key returned once as `key` with status `key_shown_once`. Every answer carries docs_url, openapi_url, llms_url, upgrade_url and, on success or 409, a next_steps array. Errors use one envelope with a stable error.code. Leave the `website` honeypot out. Send no browser Origin header, or https://china-snapshot.com. Limited to 5 requests per 10 minutes per IP. ## Authentication ```json [] ``` An empty security requirement means this operation does not declare authentication. Read operation notes for entitlement and protocol-method requirements. ## Parameters No parameters declared by the specification. ## Request body ```json { "required": true, "content": { "application/json": { "schema": { "type": "object", "required": [ "email" ], "properties": { "email": { "type": "string", "format": "email", "maxLength": 254, "description": "Where the key is emailed. One live key per address." } } }, "example": { "email": "you@example.com" } }, "application/x-www-form-urlencoded": { "schema": { "type": "object", "required": [ "email" ], "properties": { "email": { "type": "string", "format": "email", "maxLength": 254, "description": "Where the key is emailed. One live key per address." } } } } } } ``` ## Responses and errors | HTTP status | Meaning | | --- | --- | | 201 | Key issued. status=key_emailed: check the inbox. status=key_shown_once: delivery failed and the Free key is in `key`, once. | | 400 | error.code invalid_email, email_required or invalid_request (body not JSON or form, or over 4 KB). | | 403 | error.code forbidden_origin: a browser Origin other than https://china-snapshot.com. | | 409 | error.code key_exists: this address already has a live key. next_steps explains how to find it in the inbox, replace it, or upgrade. | | 429 | error.code rate_limited: too many requests from this network. Honour Retry-After; retry_after is in seconds. | | 503 | error.code unavailable or email_failed: nothing was issued; retry later. | ## Response examples No response example is supplied by OpenAPI for this operation. The complete response schemas are preserved below; use an actual response for observed values. ## Request example (creates a key) Supply your own authorized address. This JSON illustrates the request body; the docs test runner never submits signup or checkout. ```json {"email": "YOUR_AUTHORIZED_EMAIL"} ``` Use Content-Type: application/json and Accept: application/json. Follow next_steps; normally the key arrives by email. ## Retry guidance See [errors and retries](/docs/api/overview#errors-and-retries). Honor operation-specific response headers. A POST may create state; do not replay it automatically. ## Complete operation contract The expanded contract below includes all parameter constraints, response headers, schemas and examples from OpenAPI. ```json { "path": "/api/key_signup", "method": "POST", "parameters": [], "operationId": "requestApiKey", "summary": "Request a Free API key by email", "description": "Self-serve, no account. Issues one Free key per email address and emails it with a quickstart (subject 'Your China Snapshot API key (and a 30-second start)', from contact@china-snapshot.com). The key is NOT in the response: read it from the email. Only when mail delivery fails is a Free key returned once as `key` with status `key_shown_once`. Every answer carries docs_url, openapi_url, llms_url, upgrade_url and, on success or 409, a next_steps array. Errors use one envelope with a stable error.code. Leave the `website` honeypot out. Send no browser Origin header, or https://china-snapshot.com. Limited to 5 requests per 10 minutes per IP.", "tags": [ "keys", "Manage access" ], "security": [], "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "required": [ "email" ], "properties": { "email": { "type": "string", "format": "email", "maxLength": 254, "description": "Where the key is emailed. One live key per address." } } }, "example": { "email": "you@example.com" } }, "application/x-www-form-urlencoded": { "schema": { "type": "object", "required": [ "email" ], "properties": { "email": { "type": "string", "format": "email", "maxLength": 254, "description": "Where the key is emailed. One live key per address." } } } } } }, "responses": { "201": { "description": "Key issued. status=key_emailed: check the inbox. status=key_shown_once: delivery failed and the Free key is in `key`, once.", "content": { "application/json": { "schema": { "type": "object", "required": [ "ok", "status", "tier", "emailed", "limits", "docs_url", "openapi_url", "upgrade_url", "next_steps" ], "properties": { "ok": { "type": "boolean", "const": true }, "status": { "type": "string", "enum": [ "key_emailed", "key_shown_once" ] }, "message": { "type": "string" }, "tier": { "type": "string", "enum": [ "free", "pro" ], "description": "pro when the address already has a paid subscription." }, "prefix": { "type": "string", "description": "The key's first 18 characters, to recognise it later." }, "emailed": { "type": "boolean" }, "limits": { "type": "object", "additionalProperties": true }, "docs_url": { "type": "string", "format": "uri" }, "openapi_url": { "type": "string", "format": "uri" }, "llms_url": { "type": "string", "format": "uri" }, "upgrade_url": { "type": "string", "format": "uri" }, "support_email": { "type": "string", "format": "email" }, "pro": { "type": "object", "additionalProperties": true, "description": "What Pro adds, from x-api-tiers. Present for a Free key." }, "next_steps": { "type": "array", "items": { "type": "object", "required": [ "id", "text" ], "properties": { "id": { "type": "string", "description": "Stable step id, e.g. check_inbox, first_request, read_docs, upgrade, replace_key, store_key." }, "text": { "type": "string" }, "url": { "type": "string" }, "example": { "type": "string", "description": "A copy-paste command, with $CHINA_SNAPSHOT_KEY in place of the key." }, "api": { "type": "object", "additionalProperties": true } } } }, "key": { "type": "string", "description": "Only with status key_shown_once. Shown once; never stored in plain text." } } } } } }, "400": { "description": "error.code invalid_email, email_required or invalid_request (body not JSON or form, or over 4 KB).", "content": { "application/json": { "schema": { "type": "object", "required": [ "ok", "status", "error" ], "properties": { "ok": { "type": "boolean", "const": false }, "status": { "type": "string", "const": "error" }, "error": { "type": "object", "required": [ "code", "message" ], "properties": { "code": { "type": "string", "enum": [ "invalid_request", "invalid_email", "email_required", "forbidden_origin", "method_not_allowed", "key_exists", "rate_limited", "unavailable", "email_failed" ] }, "message": { "type": "string" }, "hint": { "type": "string" } } }, "retry_after": { "type": "integer", "description": "Seconds, on 429." }, "docs_url": { "type": "string", "format": "uri" }, "openapi_url": { "type": "string", "format": "uri" }, "llms_url": { "type": "string", "format": "uri" }, "upgrade_url": { "type": "string", "format": "uri" }, "next_steps": { "type": "array", "items": { "type": "object", "required": [ "id", "text" ], "properties": { "id": { "type": "string", "description": "Stable step id, e.g. check_inbox, first_request, read_docs, upgrade, replace_key, store_key." }, "text": { "type": "string" }, "url": { "type": "string" }, "example": { "type": "string", "description": "A copy-paste command, with $CHINA_SNAPSHOT_KEY in place of the key." }, "api": { "type": "object", "additionalProperties": true } } } } } } } } }, "403": { "description": "error.code forbidden_origin: a browser Origin other than https://china-snapshot.com.", "content": { "application/json": { "schema": { "type": "object", "required": [ "ok", "status", "error" ], "properties": { "ok": { "type": "boolean", "const": false }, "status": { "type": "string", "const": "error" }, "error": { "type": "object", "required": [ "code", "message" ], "properties": { "code": { "type": "string", "enum": [ "invalid_request", "invalid_email", "email_required", "forbidden_origin", "method_not_allowed", "key_exists", "rate_limited", "unavailable", "email_failed" ] }, "message": { "type": "string" }, "hint": { "type": "string" } } }, "retry_after": { "type": "integer", "description": "Seconds, on 429." }, "docs_url": { "type": "string", "format": "uri" }, "openapi_url": { "type": "string", "format": "uri" }, "llms_url": { "type": "string", "format": "uri" }, "upgrade_url": { "type": "string", "format": "uri" }, "next_steps": { "type": "array", "items": { "type": "object", "required": [ "id", "text" ], "properties": { "id": { "type": "string", "description": "Stable step id, e.g. check_inbox, first_request, read_docs, upgrade, replace_key, store_key." }, "text": { "type": "string" }, "url": { "type": "string" }, "example": { "type": "string", "description": "A copy-paste command, with $CHINA_SNAPSHOT_KEY in place of the key." }, "api": { "type": "object", "additionalProperties": true } } } } } } } } }, "409": { "description": "error.code key_exists: this address already has a live key. next_steps explains how to find it in the inbox, replace it, or upgrade.", "content": { "application/json": { "schema": { "type": "object", "required": [ "ok", "status", "error" ], "properties": { "ok": { "type": "boolean", "const": false }, "status": { "type": "string", "const": "error" }, "error": { "type": "object", "required": [ "code", "message" ], "properties": { "code": { "type": "string", "enum": [ "invalid_request", "invalid_email", "email_required", "forbidden_origin", "method_not_allowed", "key_exists", "rate_limited", "unavailable", "email_failed" ] }, "message": { "type": "string" }, "hint": { "type": "string" } } }, "retry_after": { "type": "integer", "description": "Seconds, on 429." }, "docs_url": { "type": "string", "format": "uri" }, "openapi_url": { "type": "string", "format": "uri" }, "llms_url": { "type": "string", "format": "uri" }, "upgrade_url": { "type": "string", "format": "uri" }, "next_steps": { "type": "array", "items": { "type": "object", "required": [ "id", "text" ], "properties": { "id": { "type": "string", "description": "Stable step id, e.g. check_inbox, first_request, read_docs, upgrade, replace_key, store_key." }, "text": { "type": "string" }, "url": { "type": "string" }, "example": { "type": "string", "description": "A copy-paste command, with $CHINA_SNAPSHOT_KEY in place of the key." }, "api": { "type": "object", "additionalProperties": true } } } } } } } } }, "429": { "description": "error.code rate_limited: too many requests from this network. Honour Retry-After; retry_after is in seconds.", "headers": { "Retry-After": { "schema": { "type": "integer" } } }, "content": { "application/json": { "schema": { "type": "object", "required": [ "ok", "status", "error" ], "properties": { "ok": { "type": "boolean", "const": false }, "status": { "type": "string", "const": "error" }, "error": { "type": "object", "required": [ "code", "message" ], "properties": { "code": { "type": "string", "enum": [ "invalid_request", "invalid_email", "email_required", "forbidden_origin", "method_not_allowed", "key_exists", "rate_limited", "unavailable", "email_failed" ] }, "message": { "type": "string" }, "hint": { "type": "string" } } }, "retry_after": { "type": "integer", "description": "Seconds, on 429." }, "docs_url": { "type": "string", "format": "uri" }, "openapi_url": { "type": "string", "format": "uri" }, "llms_url": { "type": "string", "format": "uri" }, "upgrade_url": { "type": "string", "format": "uri" }, "next_steps": { "type": "array", "items": { "type": "object", "required": [ "id", "text" ], "properties": { "id": { "type": "string", "description": "Stable step id, e.g. check_inbox, first_request, read_docs, upgrade, replace_key, store_key." }, "text": { "type": "string" }, "url": { "type": "string" }, "example": { "type": "string", "description": "A copy-paste command, with $CHINA_SNAPSHOT_KEY in place of the key." }, "api": { "type": "object", "additionalProperties": true } } } } } } } } }, "503": { "description": "error.code unavailable or email_failed: nothing was issued; retry later.", "content": { "application/json": { "schema": { "type": "object", "required": [ "ok", "status", "error" ], "properties": { "ok": { "type": "boolean", "const": false }, "status": { "type": "string", "const": "error" }, "error": { "type": "object", "required": [ "code", "message" ], "properties": { "code": { "type": "string", "enum": [ "invalid_request", "invalid_email", "email_required", "forbidden_origin", "method_not_allowed", "key_exists", "rate_limited", "unavailable", "email_failed" ] }, "message": { "type": "string" }, "hint": { "type": "string" } } }, "retry_after": { "type": "integer", "description": "Seconds, on 429." }, "docs_url": { "type": "string", "format": "uri" }, "openapi_url": { "type": "string", "format": "uri" }, "llms_url": { "type": "string", "format": "uri" }, "upgrade_url": { "type": "string", "format": "uri" }, "next_steps": { "type": "array", "items": { "type": "object", "required": [ "id", "text" ], "properties": { "id": { "type": "string", "description": "Stable step id, e.g. check_inbox, first_request, read_docs, upgrade, replace_key, store_key." }, "text": { "type": "string" }, "url": { "type": "string" }, "example": { "type": "string", "description": "A copy-paste command, with $CHINA_SNAPSHOT_KEY in place of the key." }, "api": { "type": "object", "additionalProperties": true } } } } } } } } } }, "x-codeSamples": [ { "lang": "curl", "label": "curl", "source": "curl -s -X POST 'https://china-snapshot.com/api/key_signup' \\\n -H 'Content-Type: application/json' \\\n -d '{\"email\":\"you@example.com\"}'" } ] } ``` --- Source: https://china-snapshot.com/docs/api/replace-api-key.md # Replace a self-serve API key by emailed link replaceApiKey · POST /api/keys/replace ## Endpoint `POST /api/keys/replace` Send the address the key belongs to. The answer is always 202 status=check_email, whether or not the address holds a key, so the route cannot reveal who has one. If it does, that address is emailed a one-time link (30 minutes, single use) to /api/keys/confirm; opening it changes nothing, and confirming issues a new key (shown once on the confirmation page) and revokes the old one in one transaction, keeping its tier. Limited per IP and per address. Keys created from a signed-in account are managed on /account instead. ## Authentication ```json [] ``` An empty security requirement means this operation does not declare authentication. Read operation notes for entitlement and protocol-method requirements. ## Parameters No parameters declared by the specification. ## Request body ```json { "required": true, "content": { "application/json": { "schema": { "type": "object", "required": [ "email" ], "properties": { "email": { "type": "string", "format": "email", "maxLength": 254, "description": "Where the key is emailed. One live key per address." } } }, "example": { "email": "you@example.com" } } } } ``` ## Responses and errors | HTTP status | Meaning | | --- | --- | | 202 | Always the same body: check the inbox the key belongs to. | | 400 | code invalid_parameter: the body is not JSON or the email is not a valid address. | | 429 | code rate_limited: too many requests from this network or for this address. Honour Retry-After. | ## Response examples No response example is supplied by OpenAPI for this operation. The complete response schemas are preserved below; use an actual response for observed values. ## Retry guidance See [errors and retries](/docs/api/overview#errors-and-retries). Honor operation-specific response headers. A POST may create state; do not replay it automatically. ## Complete operation contract The expanded contract below includes all parameter constraints, response headers, schemas and examples from OpenAPI. ```json { "path": "/api/keys/replace", "method": "POST", "parameters": [], "operationId": "replaceApiKey", "summary": "Replace a self-serve API key by emailed link", "description": "Send the address the key belongs to. The answer is always 202 status=check_email, whether or not the address holds a key, so the route cannot reveal who has one. If it does, that address is emailed a one-time link (30 minutes, single use) to /api/keys/confirm; opening it changes nothing, and confirming issues a new key (shown once on the confirmation page) and revokes the old one in one transaction, keeping its tier. Limited per IP and per address. Keys created from a signed-in account are managed on /account instead.", "tags": [ "keys", "Manage access" ], "security": [], "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "required": [ "email" ], "properties": { "email": { "type": "string", "format": "email", "maxLength": 254, "description": "Where the key is emailed. One live key per address." } } }, "example": { "email": "you@example.com" } } } }, "responses": { "202": { "description": "Always the same body: check the inbox the key belongs to.", "content": { "application/json": { "schema": { "type": "object", "required": [ "ok", "status" ] } } } }, "400": { "description": "code invalid_parameter: the body is not JSON or the email is not a valid address." }, "429": { "description": "code rate_limited: too many requests from this network or for this address. Honour Retry-After." } }, "x-codeSamples": [ { "lang": "curl", "label": "curl", "source": "curl -s -X POST 'https://china-snapshot.com/api/keys/replace' \\\n -H 'Content-Type: application/json' \\\n -d '{\"email\":\"you@example.com\"}'" } ] } ``` --- Source: https://china-snapshot.com/docs/api/revoke-api-key.md # Revoke a self-serve API key by emailed link revokeApiKey · POST /api/keys/revoke ## Endpoint `POST /api/keys/revoke` Send the address the key belongs to. The answer is always 202 status=check_email, whether or not the address holds a key, so the route cannot reveal who has one. If it does, that address is emailed a one-time link (30 minutes, single use) to /api/keys/confirm; opening it changes nothing, and confirming revokes the key. Limited per IP and per address. Keys created from a signed-in account are managed on /account instead. ## Authentication ```json [] ``` An empty security requirement means this operation does not declare authentication. Read operation notes for entitlement and protocol-method requirements. ## Parameters No parameters declared by the specification. ## Request body ```json { "required": true, "content": { "application/json": { "schema": { "type": "object", "required": [ "email" ], "properties": { "email": { "type": "string", "format": "email", "maxLength": 254, "description": "Where the key is emailed. One live key per address." } } }, "example": { "email": "you@example.com" } } } } ``` ## Responses and errors | HTTP status | Meaning | | --- | --- | | 202 | Always the same body: check the inbox the key belongs to. | | 400 | code invalid_parameter: the body is not JSON or the email is not a valid address. | | 429 | code rate_limited: too many requests from this network or for this address. Honour Retry-After. | ## Response examples No response example is supplied by OpenAPI for this operation. The complete response schemas are preserved below; use an actual response for observed values. ## Retry guidance See [errors and retries](/docs/api/overview#errors-and-retries). Honor operation-specific response headers. A POST may create state; do not replay it automatically. ## Complete operation contract The expanded contract below includes all parameter constraints, response headers, schemas and examples from OpenAPI. ```json { "path": "/api/keys/revoke", "method": "POST", "parameters": [], "operationId": "revokeApiKey", "summary": "Revoke a self-serve API key by emailed link", "description": "Send the address the key belongs to. The answer is always 202 status=check_email, whether or not the address holds a key, so the route cannot reveal who has one. If it does, that address is emailed a one-time link (30 minutes, single use) to /api/keys/confirm; opening it changes nothing, and confirming revokes the key. Limited per IP and per address. Keys created from a signed-in account are managed on /account instead.", "tags": [ "keys", "Manage access" ], "security": [], "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "required": [ "email" ], "properties": { "email": { "type": "string", "format": "email", "maxLength": 254, "description": "Where the key is emailed. One live key per address." } } }, "example": { "email": "you@example.com" } } } }, "responses": { "202": { "description": "Always the same body: check the inbox the key belongs to.", "content": { "application/json": { "schema": { "type": "object", "required": [ "ok", "status" ] } } } }, "400": { "description": "code invalid_parameter: the body is not JSON or the email is not a valid address." }, "429": { "description": "code rate_limited: too many requests from this network or for this address. Honour Retry-After." } }, "x-codeSamples": [ { "lang": "curl", "label": "curl", "source": "curl -s -X POST 'https://china-snapshot.com/api/keys/revoke' \\\n -H 'Content-Type: application/json' \\\n -d '{\"email\":\"you@example.com\"}'" } ] } ``` --- Source: https://china-snapshot.com/docs/api/start-pro-checkout.md # Start the Pro subscription checkout startProCheckout · POST /api/checkout/pro ## Endpoint `POST /api/checkout/pro` Creates an Airwallex hosted checkout for Pro (x-api-tiers lists its limits and price) and, with Accept: application/json, returns {checkout: true, url} for a person to open and pay. Without that Accept header it answers 303 to the hosted page. Pay with the address a key was emailed to and that key moves to Pro. Answers 200 {checkout: false} while checkout is not open. Nothing is charged until the hosted checkout is completed. People can use https://china-snapshot.com/api#upgrade instead. ## Authentication ```json [] ``` An empty security requirement means this operation does not declare authentication. Read operation notes for entitlement and protocol-method requirements. ## Parameters No parameters declared by the specification. ## Request body ```json { "required": true, "content": { "application/x-www-form-urlencoded": { "schema": { "type": "object", "required": [ "email" ], "properties": { "email": { "type": "string", "format": "email", "maxLength": 254 } } } } } } ``` ## Responses and errors | HTTP status | Meaning | | --- | --- | | 200 | {checkout: true, url} with the hosted checkout URL, or {checkout: false, message} when checkout is not open. | | 303 | Redirect to the hosted checkout (no Accept: application/json). | | 400 | No usable email. | | 429 | Rate limit exceeded; honour Retry-After. | | 502 | The checkout could not be started; nothing was charged. | ## Response examples No response example is supplied by OpenAPI for this operation. The complete response schemas are preserved below; use an actual response for observed values. ## Retry guidance See [errors and retries](/docs/api/overview#errors-and-retries). Honor operation-specific response headers. A POST may create state; do not replay it automatically. ## Complete operation contract The expanded contract below includes all parameter constraints, response headers, schemas and examples from OpenAPI. ```json { "path": "/api/checkout/pro", "method": "POST", "parameters": [], "operationId": "startProCheckout", "summary": "Start the Pro subscription checkout", "description": "Creates an Airwallex hosted checkout for Pro (x-api-tiers lists its limits and price) and, with Accept: application/json, returns {checkout: true, url} for a person to open and pay. Without that Accept header it answers 303 to the hosted page. Pay with the address a key was emailed to and that key moves to Pro. Answers 200 {checkout: false} while checkout is not open. Nothing is charged until the hosted checkout is completed. People can use https://china-snapshot.com/api#upgrade instead.", "tags": [ "keys", "Manage access" ], "security": [], "requestBody": { "required": true, "content": { "application/x-www-form-urlencoded": { "schema": { "type": "object", "required": [ "email" ], "properties": { "email": { "type": "string", "format": "email", "maxLength": 254 } } } } } }, "responses": { "200": { "description": "{checkout: true, url} with the hosted checkout URL, or {checkout: false, message} when checkout is not open.", "content": { "application/json": { "schema": { "type": "object", "required": [ "checkout" ], "properties": { "checkout": { "type": "boolean" }, "url": { "type": "string", "format": "uri" }, "message": { "type": "string" } } } } } }, "303": { "description": "Redirect to the hosted checkout (no Accept: application/json)." }, "400": { "description": "No usable email." }, "429": { "description": "Rate limit exceeded; honour Retry-After." }, "502": { "description": "The checkout could not be started; nothing was charged." } }, "x-codeSamples": [ { "lang": "curl", "label": "curl", "source": "curl -s -X POST 'https://china-snapshot.com/api/checkout/pro' \\\n -H 'Accept: application/json' \\\n -d 'email=you@example.com'" } ] } ``` --- Source: https://china-snapshot.com/docs/api/developer-portal.md # The developer portal developerPortal · GET /developers ## Endpoint `GET /developers` A public page documenting the API, MCP, CLI, OpenAPI spec, and agent-readable files. ## Authentication ```json [] ``` An empty security requirement means this operation does not declare authentication. Read operation notes for entitlement and protocol-method requirements. ## Parameters No parameters declared by the specification. ## Responses and errors | HTTP status | Meaning | | --- | --- | | 200 | The developer portal HTML page. | ## Response examples No response example is supplied by OpenAPI for this operation. The complete response schemas are preserved below; use an actual response for observed values. ## Retry guidance See [errors and retries](/docs/api/overview#errors-and-retries). Honor operation-specific response headers. A POST may create state; do not replay it automatically. ## Complete operation contract The expanded contract below includes all parameter constraints, response headers, schemas and examples from OpenAPI. ```json { "path": "/developers", "method": "GET", "parameters": [], "operationId": "developerPortal", "summary": "The developer portal", "description": "A public page documenting the API, MCP, CLI, OpenAPI spec, and agent-readable files.", "tags": [ "docs" ], "security": [], "responses": { "200": { "description": "The developer portal HTML page." } }, "x-codeSamples": [ { "lang": "curl", "label": "curl", "source": "curl -s 'https://china-snapshot.com/developers'" } ] } ``` --- Source: https://china-snapshot.com/docs/api/get-open-api.md # This OpenAPI document getOpenApi · GET /openapi.json ## Endpoint `GET /openapi.json` The complete OpenAPI 3.1 description of the China Snapshot API surface. ## Authentication ```json [] ``` An empty security requirement means this operation does not declare authentication. Read operation notes for entitlement and protocol-method requirements. ## Parameters No parameters declared by the specification. ## Responses and errors | HTTP status | Meaning | | --- | --- | | 200 | The OpenAPI 3.1 JSON document. | ## Response examples No response example is supplied by OpenAPI for this operation. The complete response schemas are preserved below; use an actual response for observed values. ## Retry guidance See [errors and retries](/docs/api/overview#errors-and-retries). Honor operation-specific response headers. A POST may create state; do not replay it automatically. ## Complete operation contract The expanded contract below includes all parameter constraints, response headers, schemas and examples from OpenAPI. ```json { "path": "/openapi.json", "method": "GET", "parameters": [], "operationId": "getOpenApi", "summary": "This OpenAPI document", "description": "The complete OpenAPI 3.1 description of the China Snapshot API surface.", "tags": [ "docs" ], "security": [], "responses": { "200": { "description": "The OpenAPI 3.1 JSON document." } }, "x-codeSamples": [ { "lang": "curl", "label": "curl", "source": "curl -s 'https://china-snapshot.com/openapi.json'" } ] } ``` --- Source: https://china-snapshot.com/docs/mcp/overview.md # Connect through MCP Remote research tools over Streamable HTTP, with an entitled Bearer credential. ## Endpoint and access The endpoint is https://china-snapshot.com/api/mcp. GET describes it; protocol messages use POST. initialize, notifications/initialized and ping are public. tools/list and tools/call need an API key: a free key (POST /api/key_signup) works, with 250 tool calls a month. ```curl curl --fail-with-body --silent --show-error --max-time 30 "https://china-snapshot.com/api/mcp" ``` ```python import json from urllib.request import Request, urlopen request = Request('https://china-snapshot.com/api/mcp') with urlopen(request, timeout=30) as response: data = json.load(response) print(json.dumps(data, ensure_ascii=False)) ``` ```typescript const response = await fetch("https://china-snapshot.com/api/mcp"); if (!response.ok) throw new Error(`HTTP ${response.status}`); console.log(await response.json()); ``` ## Claude Code Use Claude Code's remote HTTP MCP setup. Configure the key in your environment first; do not commit the expanded command or paste its secret into chat. Verify the connection using the client's MCP status UI. ```bash claude mcp add --transport http china-snapshot https://china-snapshot.com/api/mcp --header "Authorization: Bearer $CS_API_KEY" ``` ## Cursor In your project's .cursor/mcp.json, configure the remote server and an environment variable reference. Set CS_API_KEY in the environment available to Cursor. Client configuration support depends on your installed version. ```json { "mcpServers": { "china-snapshot": { "url": "https://china-snapshot.com/api/mcp", "headers": {"Authorization": "Bearer ${env:CS_API_KEY}"} } } } ``` ## ChatGPT Coming soon: a verified native ChatGPT connector configuration. This repo provides Bearer authentication but no MCP OAuth authorization server. Do not choose unauthenticated setup and expect paid tools to work. A supported client or separately configured adapter must securely supply the Bearer header; no working native ChatGPT configuration is claimed here. ## Exposed tools The list below is generated from the tool definitions the endpoint serves. find_records, get_record, get_observations, get_changes and export_records read the same rows and field names as /api/datasets and /api/changes; list_watchlists returns the maintained topic filters. ### get_digest Get the latest complete China Snapshot professional briefing. Returns {edition_key, published_at, status, preview, brief, commercial_mode}. When no edition exists yet it returns edition_key=null with `reason` and `detail` fields rather than failing, and a pre-launch edition is returned with preview=true plus the same two fields — check `preview` before quoting an edition as published. ```json { "type": "object", "properties": {}, "additionalProperties": false } ``` ### get_signal_packet Retrieve the latest available validated Professional Signal Packet v1, or the protected historical beta-evaluation case when packet_id matches exactly. Historical evaluation material is never the no-argument default. The packet is bounded, not exhaustive: observations are source-backed; interpretations are generated analysis; uncertainty must be retained; historical comparisons may be non-exhaustive; adjacent market/social signals do not establish causation. retained_last_known_good=true means historical context, not today's change. availability=historical_evaluation means a preserved exercise, not current evidence. No packet is not evidence that no relevant event occurred elsewhere. ```json { "type": "object", "properties": { "packet_id": { "type": "string", "maxLength": 180 } }, "additionalProperties": false } ``` ### export_signal_packet Export the same complete validated Professional packet as Markdown or JSON. Markdown preserves Observation, Interpretation, Uncertainty, Watch next, and Evidence labels. The same bounded-coverage and retained-last-known-good cautions as get_signal_packet apply; no direct Professional asset URL is returned. ```json { "type": "object", "required": [ "format" ], "properties": { "packet_id": { "type": "string", "maxLength": 180 }, "format": { "type": "string", "enum": [ "markdown", "json" ] } }, "additionalProperties": false } ``` ### search_stories Search the bilingual source archive and return attributed evidence records. Honesty fields on every response: `coverage` ('full' or 'none'), per-item `low_confidence`, `mode` ('semantic' or 'static') and the `limits` thresholds. coverage='none' means the corpus has no material clearing the relevance floor — items is empty by design, not an error; do not treat it as absence of evidence elsewhere. `count` and `returned` are both the number of items in this response; `total_matched` appears only in 'static' keyword mode and is the corpus-wide match count. There is no pagination: results are the top-ranked slice up to `limit` (max 20), so absence from this response is not absence from the archive. ```json { "type": "object", "required": [ "query" ], "properties": { "query": { "type": "string", "maxLength": 180 }, "limit": { "type": "integer", "minimum": 1, "maximum": 20 } }, "additionalProperties": false } ``` ### get_entity_timeline Return recent source records mentioning an entity, newest evidence first. Retrieval is semantic, so this is a relevance-ranked chronology, not an exhaustive register: `exhaustive` is always false, an item can rank in without naming the entity, and `window` reports only the date span of the records returned — a gap inside it is not evidence that nothing happened. Carries the same `coverage`, `low_confidence`, `mode` and `limits` honesty fields as search_stories. ```json { "type": "object", "required": [ "entity" ], "properties": { "entity": { "type": "string", "maxLength": 120 }, "limit": { "type": "integer", "minimum": 1, "maximum": 20 } }, "additionalProperties": false } ``` ### find_records Find records in the retained archive (documents, social hot-list observations, indicators) by literal text, publisher, reviewed topic (watch/topic) and UTC collection-day range; no ranking, no model. Same query, order and cursor as GET /api/datasets. Documents are one record per source URL with first_seen/last_seen, observation_count, version_count and change_type (new, newly_discovered, revised); seeing a URL again is never a new document. dataset=social with mode=topics returns one row per (platform, topic). Returns {items, row_count, next_cursor, as_of, coverage}. next_cursor is null at the end; pass it back unchanged with the same filters to continue. At most 20 rows per call. Absence from a page is not absence from the archive. ```json { "type": "object", "properties": { "dataset": { "type": "string", "enum": [ "documents", "social", "indicators" ], "description": "Default documents." }, "q": { "type": "string", "maxLength": 180, "description": "Literal case-insensitive substring of title or publisher." }, "source": { "type": "string", "maxLength": 180, "description": "Exact publisher label." }, "from": { "type": "string", "description": "Inclusive UTC collection day, YYYY-MM-DD (first_seen for documents, captured_at otherwise)." }, "to": { "type": "string", "description": "Inclusive UTC collection day, YYYY-MM-DD." }, "text": { "type": "string", "enum": [ "available" ], "description": "Documents only: records with approved original text." }, "mode": { "type": "string", "enum": [ "raw", "release", "topics" ], "description": "raw (default) = one row per record or collection run; release = indicators only, one row per series and period; topics = social only, one row per (platform, topic) within the window (default 30 days, max 92)." }, "watch": { "type": "string", "enum": [ "data-security", "export-controls", "property-support", "semiconductors" ], "description": "Reviewed topic filter (see list_watchlists): a title match on a listed bilingual term, nothing inferred." }, "topic": { "type": "string", "enum": [ "data-security", "export-controls", "property-support", "semiconductors" ], "description": "Alias of watch." }, "change_type": { "type": "string", "enum": [ "new", "newly_discovered", "revised" ], "description": "Documents only: new, newly_discovered (published more than 2 days before we first saw it) or revised (a changed body was observed)." }, "series": { "type": "string", "description": "Indicators only: exact series_id such as cpi-yoy." }, "cursor": { "type": "string", "description": "next_cursor from the previous page, with the same filters. Valid 24 hours." }, "fields": { "type": "string", "description": "Comma-separated field keys; default all." }, "limit": { "type": "integer", "minimum": 1, "maximum": 20 } }, "additionalProperties": false } ``` ### get_record Retrieve one record by its stable id (documents:N, social:N, indicators:N), including individually approved original text for documents. Same as GET /api/datasets?id=. ```json { "type": "object", "required": [ "id" ], "properties": { "id": { "type": "string", "pattern": "^(documents|social|indicators):[1-9][0-9]*$" } }, "additionalProperties": false } ``` ### get_observations Inspect the observations of one indicator series, oldest first, each marked origin=archive (a release we watched appear, with release_date and revised) or origin=backfill (older published history). Same as GET /api/series/{series_id}. Observations of documents and social records are not available through this tool yet. ```json { "type": "object", "required": [ "series_id" ], "properties": { "series_id": { "type": "string", "pattern": "^[a-z0-9]+(-[a-z0-9]+)*$", "maxLength": 120 }, "from": { "type": "string", "description": "Earliest reference period, YYYY-MM or YYYY-MM-DD." }, "to": { "type": "string", "description": "Latest reference period, inclusive." } }, "additionalProperties": false } ``` ### get_changes Retrieve documents added or revised since a time, oldest first, for polling. Same feed as GET /api/changes. Each item has change_type (new: we first collected this URL; newly_discovered: new to us but published more than 2 days earlier; revised: we extracted a different body for a known URL) and change_at (UTC). Each document appears once per walk, at its latest event. Call with since=, then pass next_cursor back as cursor (next_since as since also works). Events from the last 10 minutes are held back. Optional watch/topic, source and q narrow the feed. content_hash identifies the text at that event; the text is not returned. At most 20 items per call. ```json { "type": "object", "properties": { "since": { "type": "string", "maxLength": 400, "description": "ISO-8601 instant with timezone (inclusive), or a next_since token." }, "cursor": { "type": "string", "maxLength": 400, "description": "next_cursor from the previous call." }, "watch": { "type": "string", "enum": [ "data-security", "export-controls", "property-support", "semiconductors" ] }, "topic": { "type": "string", "enum": [ "data-security", "export-controls", "property-support", "semiconductors" ], "description": "Alias of watch." }, "source": { "type": "string", "maxLength": 180 }, "q": { "type": "string", "maxLength": 180 }, "limit": { "type": "integer", "minimum": 1, "maximum": 20 } }, "additionalProperties": false } ``` ### export_records Export one bounded page of find_records results as CSV or JSON (same columns, formula-safe CSV cells, same next_cursor). Not the whole archive: at most 100 rows per call; follow next_cursor. Returns {format, row_count, next_cursor, content}. ```json { "type": "object", "required": [ "format" ], "properties": { "dataset": { "type": "string", "enum": [ "documents", "social", "indicators" ], "description": "Default documents." }, "q": { "type": "string", "maxLength": 180, "description": "Literal case-insensitive substring of title or publisher." }, "source": { "type": "string", "maxLength": 180, "description": "Exact publisher label." }, "from": { "type": "string", "description": "Inclusive UTC collection day, YYYY-MM-DD (first_seen for documents, captured_at otherwise)." }, "to": { "type": "string", "description": "Inclusive UTC collection day, YYYY-MM-DD." }, "text": { "type": "string", "enum": [ "available" ], "description": "Documents only: records with approved original text." }, "mode": { "type": "string", "enum": [ "raw", "release", "topics" ], "description": "raw (default) = one row per record or collection run; release = indicators only, one row per series and period; topics = social only, one row per (platform, topic) within the window (default 30 days, max 92)." }, "watch": { "type": "string", "enum": [ "data-security", "export-controls", "property-support", "semiconductors" ], "description": "Reviewed topic filter (see list_watchlists): a title match on a listed bilingual term, nothing inferred." }, "topic": { "type": "string", "enum": [ "data-security", "export-controls", "property-support", "semiconductors" ], "description": "Alias of watch." }, "change_type": { "type": "string", "enum": [ "new", "newly_discovered", "revised" ], "description": "Documents only: new, newly_discovered (published more than 2 days before we first saw it) or revised (a changed body was observed)." }, "series": { "type": "string", "description": "Indicators only: exact series_id such as cpi-yoy." }, "cursor": { "type": "string", "description": "next_cursor from the previous page, with the same filters. Valid 24 hours." }, "fields": { "type": "string", "description": "Comma-separated field keys; default all." }, "format": { "type": "string", "enum": [ "csv", "json" ] }, "limit": { "type": "integer", "minimum": 1, "maximum": 100 } }, "additionalProperties": false } ``` ### list_watchlists List the maintained topic filters usable as watch (or topic): slug, label, description, the exact bilingual terms, the datasets each applies to and the date it was last reviewed. Matching is a case-insensitive substring on the title or original title; a term given as a list requires every word in the same title. ```json { "type": "object", "properties": {}, "additionalProperties": false } ``` ## Verify and troubleshoot Fetch discovery first, then initialize and list tools with the credential. A successful initialization alone does not prove research entitlement. Read both HTTP status and the JSON-RPC error body. Respect Retry-After on quota failures and use [plans](/docs/plans/limits) to distinguish dataset and research allowances. ## Client configuration references [Claude Code MCP documentation](https://code.claude.com/docs/en/mcp) and [Cursor MCP documentation](https://cursor.com/docs/mcp) describe the client configuration syntax. These are client-side examples; endpoint authentication and tool behavior come from this repository. --- Source: https://china-snapshot.com/docs/plans/limits.md # Plans and limits Generated from api/tiers.py, with separate public-data and research accounting. ## Public-data tiers | Tier | Monthly price | Requests/min | Requests/month | Rows/page | History | Keys | | --- | --- | --- | --- | --- | --- | --- | | Anonymous | USD 0 | 60 | No configured cap | 100 | 400 days | 0 | | Free key | USD 0 | 60 | 5000 | 500 | 730 days | 1 | | Pro | USD 49 | 300 | 100000 | 5000 | Full retained archive | 3 | | Team | Contact | 600 | No configured cap | 5000 | Full retained archive | 10 | These are build-time configured values. Runtime overrides and the limits returned by your actual response take precedence. Team is the contact tier: it has no list price, and its limits above are the configured defaults for an agreement. History limits describe access to retained records, not a promise that every source goes back that far. ## How usage is counted A keyed public-data request consumes one metered request during admission, before dataset query validation and retrieval. It is not counted per returned row. CSV is one page and one request, subject to the same page ceiling. Anonymous usage has no per-key monthly ledger. Inspect your own usage with GET /api/usage and a Bearer key. ## Research and MCP The Pro research allowance is 5,000 combined research API and MCP tool calls per calendar month, separate from the public-data allowance. Read discovery for transport-specific throttles. MCP protocol initialization is not a metered tools/call. ## When throttled Use response headers, especially Retry-After and the rate-limit fields. A monthly limit can require waiting until the next calendar reset, not a rapid retry loop. Never retry billing or signup blindly. [Error guidance](/docs/api/overview) explains which failures are retryable. --- Source: https://china-snapshot.com/docs/resources/changelog.md # Changelog and deprecation Documentation release notes and the implemented research-version retirement policy. ## This documentation release Adds task guides, an OpenAPI-generated reference, agent setup, Markdown mirrors, search and documentation-scoped light/dark themes. It documents the change feed (/api/changes and mode=changes), document observations and revision hashes, maintained watch= topics and the Team contact tier as the API serves them; the dated API record is the [API changelog](/data/api-changelog.json). ## Deprecation policy The research implementation identifies the current transport as v2. Its internal v1 preview was replaced in place. A future v3 is intended to coexist with v2 during a notice period; Deprecation and Sunset headers will announce retirement. The published policy (info.x-deprecation-policy in [OpenAPI](/openapi.json)) gives at least 180 days' notice before a published operation, parameter, field or error code is removed or changed incompatibly. ## Check before upgrading Read the operation's contract and live headers. Editorial topic versions, collector schema_version and research API version describe different objects. [Schema versioning](/docs/data/schema-versioning) explains the distinctions. --- Source: https://china-snapshot.com/docs/resources/support-status.md # Support and status API availability, collection freshness and editorial publication are independent signals. ## API availability Check the actual request's HTTP status and body. A successful discovery response confirms that route answers; it does not prove every dataset is accessible or that your key is entitled. A successful empty query is not an outage. ## Collection freshness Use source observation timestamps, [coverage](/coverage), and the [endpoint catalog](/data/endpoints.json). A functioning API can serve delayed collection. Compare the source-specific timestamps rather than treating page-load time as freshness. ## Editorial publication The [Snapshot](/snapshot) and daily digest represent editorial output. A late editorial edition does not imply the underlying archive API is unavailable, and a new edition does not prove every collector succeeded. ## Get help Email [contact@china-snapshot.com](mailto:contact@china-snapshot.com) with the endpoint, request time, status code and a redacted query. Never include a key. For key replacement, write from the address that received it. ## Coming soon A dedicated consolidated public incident/status history is not implemented by this docs change. The signals above remain separate; this page does not invent a green operational badge.