Let’s define a practical path from decision to delivery.
OPENAPI · V1
Authorized server applications can query and publish official-site knowledge articles and news, and query knowledge-page readership analytics. New content is published directly to the COS content snapshot and served through the existing site APIs and pages.
Production API origin: https://project.xiaotunqifu.com. All operations use HTTPS, POST and Content-Type: application/json.
| Endpoint | Path | Description |
|---|---|---|
| List knowledge articles | /api/openapi/knowledge/list | Paged metadata of published knowledge articles |
| Create a knowledge article | /api/openapi/knowledge/create | Publish a bilingual knowledge article (/faq) |
| Knowledge analytics | /api/openapi/knowledge/statistics | Readership metrics over a date range |
| List news | /api/openapi/news/list | Paged news metadata |
| Create a news article | /api/openapi/news/create | Publish site news (/article, /info) |
| Update news status | /api/openapi/news/update | Withdraw, unlist, per-channel visibility and relisting |
All endpoints share the same credentials: the assigned KNOWLEDGE_RELEASE_SECRET_ID and KNOWLEDGE_RELEASE_SECRET_KEY. The key is a 64-character hexadecimal string representing 32 bytes. Configure credentials on your own server; web pages never hold them.
Requests and successful responses use AES-256-GCM. The plaintext is UTF-8 JSON; each encryption uses a fresh random 12-byte IV and a 16-byte authentication tag. iv, ciphertext and tag use standard padded Base64, with the tag separate from the ciphertext.
The request JSON envelope contains:
| Field | Type and meaning |
|---|---|
| secretId | The assigned Secret ID |
| timestamp | Integer Unix seconds; ±300 seconds tolerated |
| nonce | Single-use 16–128 character string of letters, digits, _ and -; a Base64URL-encoded 24-byte random value is recommended |
| iv | Base64 of the 12-byte IV |
| ciphertext | Base64 of the encrypted JSON |
| tag | Base64 of the 16-byte GCM tag |
The AAD is the UTF-8 encoding of these lines, joined with \n, with no trailing newline. path is the full endpoint path from the table in section 1, without origin, query string or trailing slash. Each endpoint binds its AAD to its own path: a request encrypted for one endpoint is rejected by another:
BW-RELEASE-V1
request
POST
{path}
{secretId}
{timestamp}
{nonce}
A successful response is {"code":0,"status":200,"data":{...encrypted envelope...}}. Take data and decrypt it with the same key; the second AAD line becomes response, the timestamp is the one from the response, and everything else is unchanged. The response nonce matches the request. Verify the GCM tag and confirm the Secret ID, nonce and timestamp before using the result. HTTP errors return an unencrypted error object with message and status.
Each nonce may be used only once. On timeout or retry, regenerate the nonce, timestamp and IV while keeping the article ID and content identical. The Node.js Crypto documentation covers GCM, AAD and authentication tags.
| HTTP status | Meaning and handling |
|---|---|
| 401 | Credential, authentication, timestamp error, or nonce already used; check configuration and re-encrypt |
| 409 | Publication in progress, ID content conflict, URL conflict, or pagination version change; follow the error message |
| 422 | Invalid article, news or pagination fields; fix the plaintext and re-encrypt |
| 503 | A dependency is unavailable or the publication outcome is uncertain; retry later with the same ID and content |
POST /api/openapi/knowledge/listInput (JSON before encryption):
| Field | Type | Required | Description |
|---|---|---|---|
| page | integer | no | Default 1, range 1–100000 |
| size | integer | no | Default 20, range 1–100 |
| version | string | for later pages | Pass the version from page 1; a 409 means the published version changed and pagination must restart |
{"page":1,"size":20}
The decrypted result contains version, page, size, total and records. Each record has id, routeSlug, aliases, hash and zh/en metadata (title, summary, category, tags, updatedAt); bodies are not included. The Chinese URL is /faq/{routeSlug} and the English URL is /en/faq/{routeSlug}.
POST /api/openapi/knowledge/createInput:
| Field | Type | Required | Description |
|---|---|---|---|
| id | string | yes | Caller-generated and fixed; faq-api- followed by lowercase words separated by single hyphens, up to 150 characters |
| zh / en | object | yes | Bilingual content with identical fields, see below |
| zh.q / en.q | string | yes | Title, up to 200 characters; the English title must use English characters and produces the stable English URL |
| zh.summary / en.summary | string | yes | Summary, up to 2000 characters |
| zh.category / en.category | string | yes | Category, up to 100 characters |
| zh.updatedAt / en.updatedAt | string | yes | ISO date-time |
| zh.a / en.a | string | yes | Markdown body, up to 200000 characters |
| zh.tags / en.tags | string[] | yes | Up to 20 non-empty strings of at most 80 characters; an empty array is allowed |
{
"id": "faq-api-crm-approval-guide",
"zh": {
"q": "CRM 审批流程如何设计?",
"summary": "从审批角色、权限和记录追溯规划 CRM 审批。",
"category": "软件定制",
"tags": ["CRM", "审批流程"],
"updatedAt": "2026-09-24T10:00:00+08:00",
"a": "## 审批角色\n\n先确定发起人、审批人和可见范围。"
},
"en": {
"q": "How to design CRM approval workflows",
"summary": "Plan approval roles, permissions and an auditable history for your CRM.",
"category": "Custom Software",
"tags": ["CRM", "Approvals"],
"updatedAt": "2026-09-24T10:00:00+08:00",
"a": "## Approval roles\n\nDefine who submits, who approves and who can view each request."
}
}
This endpoint only creates. Retrying with the same ID and identical content is safe; the same ID with different content returns 409, and a URL conflicting with an existing article or topic returns 409.
Decrypted result example:
{
"id": "faq-api-crm-approval-guide",
"version": "64-character release digest",
"created": true,
"published": true,
"status": "complete",
"pending": [],
"urls": {
"zh": "/faq/how-to-design-crm-approval-workflows",
"en": "/en/faq/how-to-design-crm-approval-workflows"
}
}
published: true means the COS release succeeded; status: complete means search and website-cache synchronization finished. With status: pending, pending contains search or website-cache, and resubmitting the identical article resumes synchronization. A duplicate submission returns created: false. On timeout or 503 the outcome may be uncertain; retry with the original ID and content.
POST /api/openapi/knowledge/statisticsQuery the readership of boilingwater.cn knowledge pages over a selected period, with Chinese and English paths counted separately.
Input (pass {} for defaults):
| Field | Type | Required | Description |
|---|---|---|---|
| startDate / endDate | string | no | Pass both as YYYY-MM-DD, inclusive in the Shanghai timezone; at most 366 days, no future dates. Defaults to the last 30 days including today |
| locale | string | no | all (default), zh or en |
| path | string | no | Exact page path such as /faq/example or /en/faq/example, without origin, query string or trailing slash |
| keyword | string | no | Case-insensitive substring match on title or path, up to 500 characters |
| sortBy | string | no | views (default), visitors (deduplicated visitors), durationMs (total visible milliseconds), averageMs (average visible milliseconds per view) |
| sortOrder | string | no | desc (default) or asc |
| page | integer | no | Default 1, range 1–100000 |
| size | integer | no | Default 20, range 1–100 |
Decrypted result example:
{
"generatedAt": "2026-09-27T02:00:00.000Z",
"site": "boilingwater.cn",
"timezone": "Asia/Shanghai",
"startDate": "2026-09-24",
"endDate": "2026-09-26",
"startedAt": "2026-09-01T01:00:00.000Z",
"visitorsStartedAt": "2026-09-23T01:00:00.000Z",
"visitorsComplete": true,
"page": 1,
"size": 10,
"sortBy": "views",
"sortOrder": "desc",
"total": 1,
"totalPages": 1,
"records": [{
"path": "/faq/example", "title": "示例文章", "locale": "zh",
"views": 12, "visitors": 8, "durationMs": 240000, "averageMs": 20000
}]
}
total is the number of matching pages; pages beyond the range return empty records. The full date range is aggregated before filtering, sorting and pagination; ties order by path and missing metrics sort last. Statistics keep updating as visits continue, so cross-page queries are not a frozen snapshot.
Views come from the site's browser analytics and are not merged with legacy article counters; heartbeats and retries do not add views. Duration only counts visible time reported while the page is visible, attributed to the visit's start day; averageMs = durationMs / views, rounded to milliseconds, and null when there are no samples. Statistics include all collected traffic without bot filtering.
visitors deduplicates browser identities across the selected dates; one visitor viewing the same page repeatedly counts once. It is not a count of real people and per-page visitor counts cannot be summed into a site-wide figure. Visitor counting starts from the collection start visitorsStartedAt (null when never collected). A range covering the start day or earlier returns visitorsComplete=false with visitors=null; missing history is not treated as zero. Compare visitor counts only over complete ranges after the start; today's figures still grow. startedAt is the overall browser-analytics start; data earlier than that is incomplete.
This endpoint only returns knowledge paths with collected data in the period, which may include historical pages; it is not the list of currently published articles — use list for article metadata. Invalid filters return HTTP 400; an invalid date range returns HTTP 422. Queries never publish or modify articles.
POST /api/openapi/news/listPaged metadata of official-site news (the insights channel), sorted by createTime and id descending.
Input:
| Field | Type | Required | Description |
|---|---|---|---|
| page | integer | no | Default 1, range 1–100000 |
| size | integer | no | Default 20, range 1–100 |
| version | string | for later pages | Pass the version from page 1; a 409 means the snapshot changed and pagination must restart |
The decrypted result contains version, page, size, total and records. Each record is the metadata of one news article: id, title, description, image, type, flag, status, siteStatus, siteHiddenStatus, wxStatus, appStatus, createTime, updateTime, labels, plus optional legacy fields such as url, feedId and contentType. The body (content) is not included. Records with flag=0 are deleted (retained but not displayed); status=0 means unlisted.
POST /api/openapi/news/createInput:
| Field | Type | Required | Description |
|---|---|---|---|
| id | string | yes | Decimal string in the dedicated 8-prefixed range (16–19 digits), isolated from historical IDs; example: node -e 'console.log(String(8000000000000000000n + BigInt(Date.now()) * 1000n))' |
| type | integer | yes | Channel: 1 news flash, 2 industry insights, 3 video channel, 4 feature |
| title | string | yes | Non-empty title |
| description | string | yes | Non-empty summary |
| image | string | yes | Full HTTPS cover URL. Upload the image through the existing upload channel first (such as POST /api/file/upload); this endpoint does not upload images. Before publishing, the server fetches the URL and requires a public PNG/JPEG/WebP/AVIF/GIF image of at most 10 MiB |
| markdown | string | one of two | Markdown body, converted to HTML at publication |
| content | string | null | one of two | Trusted HTML body; may be omitted for type=3 with a url (pure external-link/video news stores null) |
| labels | string[] | no | Tags, default [], up to 20 non-empty strings |
| flag | integer | no | Validity flag, default 1; 0 means deleted (retained in the snapshot but not displayed) |
| status | integer | no | Publication status, default 1 (listed); 0 unlists |
| siteStatus | integer | no | Website visibility, default 1 |
| siteHiddenStatus | integer | no | Website hidden flag, default 0 |
| wxStatus | integer | no | Mini-program visibility, default 0 |
| appStatus | integer | no | App visibility, default 0 |
| createTime | string | no | Creation time, ISO timestamp with timezone; defaults to the current Shanghai time (+08:00). Immutable after publication |
| updateTime | string | no | Update time, same format; defaults to the current Shanghai time |
| url | string | no | External link (type 3 video/external) |
| feedId | string | no | Video-channel feedId |
| contentType | integer | no | Content type (legacy semantics) |
| createBy / updateBy | string | no | Author fields (legacy migration compatibility) |
{
"id": "8001790000000000000",
"type": 1,
"title": "【今日资讯】示例主标题",
"description": "本期 5 条,覆盖平台规则与产品动态。",
"image": "https://file.boilingwater.cn/website/news/2026-09-27-example-cover.png",
"labels": ["日资讯"],
"markdown": "## 主资讯\n\n正文内容。"
}
This endpoint only creates. Retrying with the same ID and identical content is safe (created: false); the same ID with different content returns 409; a concurrent publication returns 409 — retry later with identical content. On timeout or 503 the outcome may be uncertain; retry with the original ID and content.
Decrypted result example:
{
"id": "8001790000000000000",
"version": "64-character snapshot digest",
"created": true,
"published": true,
"urls": { "zh": "/article/8001790000000000000/1", "en": "/en/article/8001790000000000000/1" },
"record": { "id": "8001790000000000000", "title": "…", "content": "<h2>主资讯</h2>…" }
}
URL rules: types 1/2/3 produce /article/{id}/{type}, type 4 produces /info/{id}; the English URL adds the /en prefix. record is the complete stored record including the rendered HTML body.
After publishing:
/api/news and the insights pages; the site sitemap reads the news API dynamically, so no extra update is needed.record as website/src/content/news/{id}.json (that directory is the news source-backup directory).news.update for status changes (withdrawal, unlisting, relisting) — do not modify the COS snapshot directly.POST /api/openapi/news/updateUpdate the display and withdrawal status of a published news article. Only status fields are accepted; content fields such as title, body, type and createTime cannot be changed here. Typical uses: withdrawal, unlisting, per-channel visibility and relisting.
Input:
| Field | Type | Required | Description |
|---|---|---|---|
| id | string | yes | Published news ID (legacy IDs and 8-prefixed new IDs alike) |
| flag | integer | at least one | Validity flag; 0 withdraws (retained in the snapshot but not displayed) |
| status | integer | at least one | 1 listed, 0 unlisted |
| siteStatus | integer | at least one | Website visibility |
| siteHiddenStatus | integer | at least one | Website hidden flag |
| wxStatus | integer | at least one | Mini-program visibility |
| appStatus | integer | at least one | App visibility |
| updateTime | string | no | ISO timestamp with timezone; defaults to the current Shanghai time (+08:00) |
At least one of the six status fields (flag, status, siteStatus, siteHiddenStatus, wxStatus, appStatus) is required; no other fields are accepted. A full withdrawal usually passes flag: 0, status: 0 and siteStatus: 0 together; passing only status: 0 hides the article from lists while the detail page remains reachable. flag is the shared deletion state — when a record also serves the mini-program/App, do not zero the other channels for a website-only withdrawal.
{"id":"8001790000000000000","flag":0,"status":0,"siteStatus":0}
Decrypted result example:
{
"id": "8001790000000000000",
"version": "64-character snapshot digest",
"updated": true,
"published": true,
"record": { "id": "8001790000000000000", "flag": 0, "status": 0, "updateTime": "2026-09-28T09:00:00+08:00" }
}
updated: false means the target status already matches the current state — a no-op that creates no new version, so identical requests are safe to retry. An unknown ID returns 404; a concurrent publication returns 409 — retry later; on timeout or 503 retry with the identical request (status changes are idempotent). Status changes take effect through the /api/news lists and details within about 30 seconds.
Download release-client.mjs — it performs request encryption, the HTTP call and response decryption on Node.js 24. With credentials in the environment:
import { readFile } from 'node:fs/promises';
import { releaseRequest } from './release-client.mjs';
// Knowledge actions: list / create / statistics.
// News actions: news.list / news.create.
console.log(await releaseRequest('list', { page: 1, size: 20 }));
console.log(await releaseRequest('news.list', { page: 1, size: 20 }));
// article.json follows section 5, news.json follows section 8; run only when ready to publish.
const article = JSON.parse(await readFile('./article.json', 'utf8'));
console.log(await releaseRequest('create', article));
const news = JSON.parse(await readFile('./news.json', 'utf8'));
console.log(await releaseRequest('news.create', news));
Set KNOWLEDGE_RELEASE_BASE_URL to use another configured API environment. The function also accepts a third argument { baseUrl, secretId, secretKey }.