{
  "openapi": "3.1.0",
  "info": {
    "title": "QueryWin Content API",
    "version": "1.0.0",
    "description": "Programmatic access to QueryWin's two pipelines. **Content gaps**: find the searches\nyour site already gets impressions for but has no page written for, generate an\nevidence-backed outline, turn it into a publishable draft, and tell us where you\npublished it. **Distribution**: pick the directories, launch platforms, communities\nand AI-cited sites to submit a product to, take the prepared submission material,\nand report where it went live.\n\n## The content pipeline\n\n```\nGET  /v1/topics            what to write this week      free\nPOST /v1/topics/outline    evidence-backed outline      costs credits\nPOST /v1/topics/article    publishable draft            costs credits (most expensive)\n     -> your script publishes it to your own blog\nPOST /v1/topics/published  tell us the URL              free, also pings IndexNow\n```\n\n## The distribution pipeline\n\n```\nGET  /v1/products                 your products and profile completeness   free\nGET  /v1/channels?productId=      where to submit, sorted by fit           free\nPOST /v1/campaigns                one task per chosen channel              free\nGET  /v1/tasks/{id}               the material to submit                   free\nPOST /v1/tasks/{id}/materials     rewrite it for this channel              costs credits\n     -> you (or your agent, with your own accounts) submit it\nPOST /v1/tasks/{id}/status        submitted, then published + listing URL  free\n```\n\nQueryWin re-checks every published listing about 72 hours later for a link or\nmention and sets `verified` itself. `published` is what you reported; `verified`\nis what we saw. They are different fields and stay different.\n\n## What this API does not do\n\nQueryWin never connects to your CMS, never holds your site credentials, never\npresses publish for you, and never submits your product anywhere itself. This API\nhands you the content and the submission material; the write happens on your\nmachine with your own credentials and accounts. That is the boundary — not a loophole\nin it.\n\n## Spending money\n\n`POST /v1/topics/outline`, `POST /v1/topics/article` and `POST /v1/tasks/{id}/materials`\ndeduct credits. All three require a `confirmSpend` field, which is an **authorization\nceiling**, not an exact amount: send a number greater than or equal to the current\nprice and the call goes through, charging whatever it actually costs (often zero,\nwhen a cached result for identical inputs is returned). If the price ever rises above\nyour ceiling, the call fails with `confirm_spend_too_low` instead of quietly charging\nmore. Call `GET /v1/usage` first to read the current prices, your free allowances,\nyour credit balance, today's remaining daily limits, and your plan's distribution\nquota.\n\n## Response envelope\n\nEvery response is wrapped: `{\"success\": true, \"data\": {...}}` on success,\n`{\"success\": false, \"error\": \"<code>\"}` on failure. Errors are **codes**, never\nprose — the product is bilingual and a hardcoded English sentence would end up\nprinted on a Chinese page.\n\nBusiness outcomes are not errors. A generation call that could not produce a valid\nresult returns **HTTP 200** with `data.ok = false` and a `data.failure` code, so\nyou can tell it apart from an auth failure or a network problem.\n\n## Authentication\n\nSend your key as `Authorization: Bearer qw_live_...`. A `X-API-Key: qw_live_...`\nheader is also accepted for tools that only let you set one header. Create and\nrevoke keys in the QueryWin dashboard under **API**; the plaintext key is shown\nexactly once, at creation.\n\n**The API and MCP require a paid plan** (Pro and up). Free workspaces cannot create\nkeys, and existing keys are refused with `403 plan_required` once a plan lapses.\n\nRate limit: 120 requests per minute per key.\n\nHuman-readable docs for this API live at\n[querywin.com/developers](https://www.querywin.com/developers).\n\n## Scopes\n\nEach key carries the permissions granted when it was created. `GET /v1/usage`\nreports them, so you never have to discover them by hitting a 403.\n\n| Scope | Grants | Default |\n|---|---|---|\n| `read` | Every GET endpoint | Always present |\n| `publish` | `POST /v1/topics/published`, `POST /v1/campaigns`, `POST /v1/tasks/{id}/status` — record what you published or submitted | Off |\n| `spend` | `POST /v1/topics/outline`, `/article` and `POST /v1/tasks/{id}/materials` — these deduct credits | Off |\n\nA key created without explicit scopes is **read-only**. Calling an endpoint your key\nlacks the scope for returns **403** with `error: \"missing_scope_<name>\"` and the\nscopes you do have. There is no hierarchy: `publish` does not imply `spend`, and\n`spend` does not imply `publish`.\n\n## MCP\n\nThe same pipeline is available to AI assistants over the Model Context Protocol at\n**`/mcp`** (Streamable HTTP), authenticated with the same key and the same header.\nTools are filtered by scope: with a read-only key the generation tools do not appear\nin `tools/list` at all.",
    "contact": {
      "name": "QueryWin",
      "url": "https://www.querywin.com"
    },
    "termsOfService": "https://www.querywin.com/terms",
    "x-description-zh": "以脚本方式接入 QueryWin 的两条链路。**内容缺口**：找出你的网站已经有曝光、\n却没有一页在承接的搜索词，生成有据可依的大纲，再写成可发布的正文草稿，\n最后把发布地址回写给我们。**分发**：选定要投递的目录、启动平台、社区与被 AI 引用的站点，\n取走已备好的投递素材，投完后回写上线地址。\n\n## 内容链\n\n```\nGET  /v1/topics            本周该写哪一篇        免费\nPOST /v1/topics/outline    有据可依的大纲        扣积分\nPOST /v1/topics/article    可发布的正文草稿      扣积分（最贵的一步）\n     -> 由你的脚本发布到你自己的网站\nPOST /v1/topics/published  回写发布地址          免费，同时代推 IndexNow\n```\n\n## 分发链\n\n```\nGET  /v1/products                 产品清单与档案完整度        免费\nGET  /v1/channels?productId=      可投递的入口，按匹配度排序    免费\nPOST /v1/campaigns                每个选定的渠道一条任务        免费\nGET  /v1/tasks/{id}               取投递素材                  免费\nPOST /v1/tasks/{id}/materials     为该渠道改写素材              扣积分\n     -> 由你（或你的 agent，用你自己的账号）投递\nPOST /v1/tasks/{id}/status        回写已提交、已上线与上线地址   免费\n```\n\n每条标记为已上线的条目，QueryWin 会在约 72 小时后回查该页面是否出现链接或提及，\n并自行置为 `verified`。`published` 是你回写的，`verified` 是我们看到的；两个字段各自独立，🚫 不合并。\n\n## 这套接口不做什么\n\nQueryWin 不接你的 CMS、不持有你的网站凭据、不代你按发布键，也不会代你向任何站点投递。\n这套接口交给你的是内容与投递素材，写入发生在你自己的机器上、用你自己的凭据与账号。\n这是边界本身，不是绕过边界的口子。\n\n## 什么时候花钱\n\n只有 `POST /v1/topics/outline`、`POST /v1/topics/article` 与 `POST /v1/tasks/{id}/materials` 扣积分。\n三者都要求带 `confirmSpend`，它是**授权上限**而不是精确金额：传一个不小于当前单价的数即可，\n实际花多少扣多少（输入不变命中缓存时往往为 0）。一旦价格涨到超过你的上限，\n请求会以 `confirm_spend_too_low` 失败，而不是悄悄多扣。调用前先请求\n`GET /v1/usage` 读当前单价、免费额度、积分余额、今天还剩多少日额度，以及当前档位的分发配额。\n\n## 响应外壳\n\n所有响应都带一层外壳：成功是 `{\"success\": true, \"data\": {...}}`，\n失败是 `{\"success\": false, \"error\": \"<错误码>\"}`。错误一律返回**代码**而不是句子 ——\nQueryWin 是中英双语站，后端拼死的英文句子会原样印在中文页面上。\n\n业务结果不是错误。生成类接口没能产出有效结果时仍然返回 **HTTP 200**，\n把 `data.ok` 置为 false 并给出 `data.failure`，这样你能把它和鉴权失败、\n网络中断区分开。\n\n## 认证\n\n把密钥放进 `Authorization: Bearer qw_live_...`。只允许配置一个请求头的工具\n可以改用 `X-API-Key: qw_live_...`。密钥在 QueryWin 工作台的 **开放 API** 一项里创建和吊销；\n明文只在创建那一次显示。\n\n**开放 API 与 MCP 需要付费档位**（Pro 及以上）。免费版建不了密钥；付费到期后已有密钥会以 `403 plan_required` 拒绝。\n\n请求频率：每把密钥每分钟 120 次。"
  },
  "servers": [
    {
      "url": "https://www.querywin.com/api",
      "description": "QueryWin API",
      "x-description-zh": "QueryWin 接口地址"
    }
  ],
  "tags": [
    {
      "name": "Account",
      "description": "Which sites this key can act on, and what it may spend",
      "x-description-zh": "这把密钥能操作哪些网站，以及可以花多少"
    },
    {
      "name": "Topics",
      "description": "The content gap pipeline",
      "x-description-zh": "内容缺口这条链"
    },
    {
      "name": "Distribution",
      "description": "The distribution pipeline: products, channels, campaigns, tasks",
      "x-description-zh": "分发链：产品、渠道、分发计划、任务"
    }
  ],
  "security": [
    {
      "bearerAuth": []
    },
    {
      "apiKeyHeader": []
    }
  ],
  "paths": {
    "/v1/sites": {
      "get": {
        "tags": [
          "Account"
        ],
        "operationId": "listSites",
        "summary": "List the sites this key can act on",
        "description": "Start here. Every other endpoint takes the `siteId` returned by this call. Omitting `siteId` elsewhere falls back to the earliest connected site, which is fine for single-site accounts and a bug waiting to happen for everyone else.",
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "success",
                    "data"
                  ],
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    },
                    "data": {
                      "$ref": "#/components/schemas/SiteList"
                    }
                  }
                }
              }
            },
            "x-description-zh": "成功"
          },
          "401": {
            "description": "Missing, malformed, revoked, or expired key",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "x-description-zh": "密钥缺失、格式不对、已吊销或已过期"
          }
        },
        "x-summary-zh": "该密钥可操作的网站清单",
        "x-description-zh": "从这里开始。其余每个接口都要用本接口返回的 `siteId`。别处不传 `siteId` 时会回落到最早接入的那个网站 —— 单站账号无所谓，多站账号迟早要出事。"
      }
    },
    "/v1/usage": {
      "get": {
        "tags": [
          "Account"
        ],
        "operationId": "getUsage",
        "summary": "Current prices, free allowance, credit balance, and daily limits",
        "description": "Read this before generating anything. It is the same source of truth the web UI uses to decide what to print on the button — availability is decided server-side, not by trying and failing.",
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "success",
                    "data"
                  ],
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    },
                    "data": {
                      "$ref": "#/components/schemas/Usage"
                    }
                  }
                }
              }
            },
            "x-description-zh": "成功"
          },
          "401": {
            "description": "Authentication failed",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "x-description-zh": "鉴权失败"
          }
        },
        "x-summary-zh": "当前单价、免费额度、积分余额与每日上限",
        "x-description-zh": "生成任何东西之前先读它。界面上按钮该显示什么，用的就是这同一个事实源 —— 能不能用由服务端判定，🚫 不要靠「试一次、失败了再说」。"
      }
    },
    "/v1/topics": {
      "get": {
        "tags": [
          "Topics"
        ],
        "operationId": "listTopics",
        "summary": "Searches with impressions but no page written for them",
        "description": "Free, no external billing (the API itself requires a paid plan). Computed fresh on each call from your own Search Console data — there is no external keyword database involved, and that is the point: \"you already have impressions and no page for it\" is not something a keyword tool can tell you.\n\nResults are ordered by opportunity. Topics the user dismissed in the UI are excluded.",
        "parameters": [
          {
            "name": "siteId",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "From `GET /v1/sites`. Defaults to the earliest connected site.",
            "x-description-zh": "来自 `GET /v1/sites`。不传则用最早接入的网站。"
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "success",
                    "data"
                  ],
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    },
                    "data": {
                      "$ref": "#/components/schemas/TopicList"
                    }
                  }
                }
              }
            },
            "x-description-zh": "成功"
          },
          "401": {
            "description": "Authentication failed",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "x-description-zh": "鉴权失败"
          },
          "404": {
            "description": "`site_not_found` — the siteId does not belong to this account",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "x-description-zh": "`site_not_found` —— 该 siteId 不属于这个账号"
          }
        },
        "x-summary-zh": "有曝光、却没有页面承接的搜索词",
        "x-description-zh": "免费，不产生任何外部账单（开放 API 本身需要付费档位）。每次调用都从你自己的 Search Console 数据现算，不涉及任何外部关键词库 —— 这正是关键：「已经有曝光、却没有页面」这件事，关键词工具告诉不了你。\n\n结果按机会分排序。已在界面上被忽略的选题不会出现在这里。"
      }
    },
    "/v1/topics/outline": {
      "get": {
        "tags": [
          "Topics"
        ],
        "operationId": "getOutline",
        "summary": "Read an already-generated outline",
        "description": "Never triggers generation and never costs anything. `outline` is null if none exists yet.",
        "parameters": [
          {
            "name": "key",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "The cluster key from `GET /v1/topics`.",
            "x-description-zh": "来自 `GET /v1/topics` 的选题标识。"
          },
          {
            "name": "siteId",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "From `GET /v1/sites`. Defaults to the earliest connected site.",
            "x-description-zh": "来自 `GET /v1/sites`。不传则用最早接入的网站。"
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "success",
                    "data"
                  ],
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    },
                    "data": {
                      "$ref": "#/components/schemas/OutlineResult"
                    }
                  }
                }
              }
            },
            "x-description-zh": "成功"
          },
          "400": {
            "description": "`key_required`",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "x-description-zh": "`key_required`"
          },
          "401": {
            "description": "Authentication failed",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "x-description-zh": "鉴权失败"
          }
        },
        "x-summary-zh": "读取已生成的大纲",
        "x-description-zh": "🚫 不会触发生成，也不消耗任何积分。尚未生成时 `outline` 为 null。"
      },
      "post": {
        "tags": [
          "Topics"
        ],
        "operationId": "generateOutline",
        "summary": "Generate an outline (costs credits)",
        "description": "Synchronous; expect roughly 10-20 seconds.\n\nIdentical inputs return the cached outline **without charging again** — the fingerprint covers the topic cluster and its competitor classification, so retrying a failed HTTP request is safe.\n\nReturns HTTP 200 with `ok: false` and a `failure` code for business outcomes (engine unavailable, output failed validation). Insufficient credits is a real 402.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/GenerateRequest"
              },
              "example": {
                "siteId": "clx1site000",
                "key": "standard wardrobe depth",
                "confirmSpend": 80
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "success",
                    "data"
                  ],
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    },
                    "data": {
                      "$ref": "#/components/schemas/OutlineOutcome"
                    }
                  }
                }
              }
            },
            "x-description-zh": "成功"
          },
          "400": {
            "description": "`key_required`, `confirm_spend_required`, or `confirm_spend_too_low` (the body carries the current `price`)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "x-description-zh": "`key_required`、`confirm_spend_required` 或 `confirm_spend_too_low`（响应体里带当前 `price`）"
          },
          "401": {
            "description": "Authentication failed",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "x-description-zh": "鉴权失败"
          },
          "402": {
            "description": "`insufficient_credits` — body carries `requiredCredits`, `currentBalance`, `shortfall`",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "x-description-zh": "`insufficient_credits` —— 响应体带 `requiredCredits`、`currentBalance`、`shortfall`"
          },
          "403": {
            "description": "`missing_scope_spend` — this key was not granted the `spend` scope",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "x-description-zh": "`missing_scope_spend` —— 这把密钥没有 `spend` 权限"
          },
          "429": {
            "description": "`rate_limited` or `daily_limit_reached` (body carries `resetAt`)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "x-description-zh": "`rate_limited` 或 `daily_limit_reached`（响应体带 `resetAt`）"
          }
        },
        "x-summary-zh": "生成大纲（扣积分）",
        "x-description-zh": "同步返回，通常 10–20 秒。\n\n输入不变时直接返回缓存的大纲，**不再重复收费** —— 指纹覆盖选题簇及其竞品判定，所以请求失败后重试是安全的。\n\n业务结果（引擎不可用、输出未通过校验）返回 HTTP 200，带 `ok: false` 与 `failure` 码。积分不足才是真正的 402。"
      }
    },
    "/v1/topics/article": {
      "get": {
        "tags": [
          "Topics"
        ],
        "operationId": "getArticle",
        "summary": "Read an already-generated draft",
        "description": "Never triggers generation and never costs anything. `article` is null if none exists yet.",
        "parameters": [
          {
            "name": "key",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "The cluster key from `GET /v1/topics`.",
            "x-description-zh": "来自 `GET /v1/topics` 的选题标识。"
          },
          {
            "name": "siteId",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "From `GET /v1/sites`. Defaults to the earliest connected site.",
            "x-description-zh": "来自 `GET /v1/sites`。不传则用最早接入的网站。"
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "success",
                    "data"
                  ],
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    },
                    "data": {
                      "$ref": "#/components/schemas/ArticleResult"
                    }
                  }
                }
              }
            },
            "x-description-zh": "成功"
          },
          "400": {
            "description": "`key_required`",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "x-description-zh": "`key_required`"
          },
          "401": {
            "description": "Authentication failed",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "x-description-zh": "鉴权失败"
          }
        },
        "x-summary-zh": "读取已生成的正文草稿",
        "x-description-zh": "🚫 不会触发生成，也不消耗任何积分。尚未生成时 `article` 为 null。"
      },
      "post": {
        "tags": [
          "Topics"
        ],
        "operationId": "generateArticle",
        "summary": "Turn the outline into a publishable draft (costs credits)",
        "description": "The most expensive call in the product. Synchronous; can take one to two minutes.\n\n**An outline must exist first** — without one you get `failure: \"no_outline\"`. The outline is what carries the evidence (the real searches behind the cluster, the pages AI cites today, deduplication against your existing pages). Skipping it would reduce this to an AI writing tool with nothing behind it.\n\n`article.markdown` is the body to publish. `article.jsonLd` is structured data already filled in with this article's content. **`article.warnings` should be logged, not dropped** — each one points at a specific phrase that reads as AI-generated, and in an automated pipeline nobody re-reads the draft before it goes out.\n\nDrafts that fail structural validation (missing sections, unanswered required questions, invented links, broken JSON-LD) are discarded and **not charged**.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/GenerateRequest"
              },
              "example": {
                "siteId": "clx1site000",
                "key": "standard wardrobe depth",
                "confirmSpend": 80
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "success",
                    "data"
                  ],
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    },
                    "data": {
                      "$ref": "#/components/schemas/ArticleOutcome"
                    }
                  }
                }
              }
            },
            "x-description-zh": "成功"
          },
          "400": {
            "description": "`key_required`, `confirm_spend_required`, or `confirm_spend_too_low` (the body carries the current `price`)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "x-description-zh": "`key_required`、`confirm_spend_required` 或 `confirm_spend_too_low`（响应体里带当前 `price`）"
          },
          "401": {
            "description": "Authentication failed",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "x-description-zh": "鉴权失败"
          },
          "402": {
            "description": "`insufficient_credits` — body carries `requiredCredits`, `currentBalance`, `shortfall`",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "x-description-zh": "`insufficient_credits` —— 响应体带 `requiredCredits`、`currentBalance`、`shortfall`"
          },
          "403": {
            "description": "`missing_scope_spend` — this key was not granted the `spend` scope",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "x-description-zh": "`missing_scope_spend` —— 这把密钥没有 `spend` 权限"
          },
          "429": {
            "description": "`rate_limited` or `daily_limit_reached` (body carries `resetAt`)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "x-description-zh": "`rate_limited` 或 `daily_limit_reached`（响应体带 `resetAt`）"
          }
        },
        "x-summary-zh": "把大纲写成可发布的正文草稿（扣积分）",
        "x-description-zh": "这条链上最贵的一步，也是最慢的一步：同步返回，通常 60–120 秒。\n\n要求该选题**已经有大纲** —— 正文依据大纲撰写，这样文章才与它要承接的搜索词对得上。没有大纲时返回 `no_outline`。\n\n与大纲一样：输入不变返回缓存结果且不重复收费；业务结果返回 HTTP 200 带 `ok: false`；积分不足返回 402。\n\n🔴 返回的 `warnings` 是**读起来像 AI 写的**那些句子。草稿仍然可用 —— 我们把它们报出来，而不是悄悄改掉。自动化流程里，这是唯一一个还有人能发现问题的时刻。"
      }
    },
    "/v1/topics/published": {
      "post": {
        "tags": [
          "Topics"
        ],
        "operationId": "markPublished",
        "summary": "Report where you published it",
        "description": "Closes the loop. Marks the topic as published, stores the URL, and submits it to IndexNow on your behalf.\n\n**IndexNow covers Bing, Yandex, Seznam and Naver — not Google.** Google has no equivalent instant-indexing endpoint; it finds the page through your sitemap.\n\nThe IndexNow submission never fails the request: your article is already published, and that is the fact this call records. Check the `indexnow` field for what actually happened. Submission requires the IndexNow key file to be verified for the site (set that up once in the dashboard).\n\nRecording the URL is also what lets QueryWin re-measure the searches this article targets once it has had time to land.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/PublishedRequest"
              },
              "example": {
                "siteId": "clx1site000",
                "key": "standard wardrobe depth",
                "url": "https://example.com/blog/standard-wardrobe-depth"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "success",
                    "data"
                  ],
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    },
                    "data": {
                      "$ref": "#/components/schemas/PublishedResult"
                    }
                  }
                }
              }
            },
            "x-description-zh": "成功"
          },
          "400": {
            "description": "`key_required`, `url_required`, or `invalid_url` (http/https only)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "x-description-zh": "`key_required`、`url_required` 或 `invalid_url`（仅支持 http/https）"
          },
          "401": {
            "description": "Authentication failed",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "x-description-zh": "鉴权失败"
          },
          "403": {
            "description": "`missing_scope_publish` — this key was not granted the `publish` scope",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "x-description-zh": "`missing_scope_publish` —— 这把密钥没有 `publish` 权限"
          },
          "404": {
            "description": "`site_not_found`",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "x-description-zh": "`site_not_found`"
          }
        },
        "x-summary-zh": "回写发布地址",
        "x-description-zh": "免费，但**有对外副作用**：该地址会被推送到 Bing、Yandex、Seznam、Naver。不含 Google —— Google 不支持 IndexNow，它会按自己的节奏发现这个改动。\n\n这一步同时把这条选题标记为已发布，它之后不再出现在 `GET /v1/topics` 里。要求密钥带 `publish` 权限。"
      }
    },
    "/v1/products": {
      "get": {
        "tags": [
          "Distribution"
        ],
        "operationId": "listProducts",
        "summary": "Your products and how complete each profile is",
        "description": "Start here for the distribution pipeline; every other distribution endpoint takes a `productId`. `completeness` and `missing` come from the product profile in the dashboard: an empty field there is an empty field in every submission, and required gaps stop a task as `blocked` / `missing_material` until the profile is completed. Free.",
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "success",
                    "data"
                  ],
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    },
                    "data": {
                      "$ref": "#/components/schemas/ProductList"
                    }
                  }
                }
              }
            },
            "x-description-zh": "成功"
          },
          "401": {
            "description": "Authentication failed",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "x-description-zh": "鉴权失败"
          }
        },
        "x-summary-zh": "产品清单与各自的档案完整度",
        "x-description-zh": "分发链从这里开始；其余分发接口都要带 `productId`。`completeness` 与 `missing` 来自工作台里的产品档案：档案里空着的格，投递素材里就是空的；必填格缺失时任务会停在 `blocked` / `missing_material`，直到档案补齐。免费。"
      }
    },
    "/v1/channels": {
      "get": {
        "tags": [
          "Distribution"
        ],
        "operationId": "listChannels",
        "summary": "Where a product can be submitted, sorted by fit",
        "description": "The channel library (directories, launch platforms, AI tool lists, communities, publishing platforms), your own entries, and — when `productId` is given — the sites AI answers already cite for that product's searches (`source: \"rivals\"`). With `productId` the list is sorted by relevance and each channel carries `taskStatus` (non-null when the product already has a task there). Free.\n\n**`citedByAi` means AI answers cited that site for your searches. It does not mean the site will list your product** — reaching out to ask is what the task is for.",
        "parameters": [
          {
            "name": "productId",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Sort by fit for this product, attach `taskStatus`, and include its AI-cited candidates.",
            "x-description-zh": "按该产品的匹配度排序、附上 `taskStatus`，并包含它的 AI 引用来源候选。"
          },
          {
            "name": "kind",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "enum": [
                "directory",
                "launch",
                "ai_directory",
                "community",
                "content",
                "other"
              ]
            },
            "description": "Channel type.",
            "x-description-zh": "渠道类型。"
          },
          {
            "name": "source",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "enum": [
                "seed",
                "user",
                "rivals"
              ]
            },
            "description": "`seed` = the library, `user` = added by you, `rivals` = AI-cited sites for this product.",
            "x-description-zh": "`seed` = 内置渠道，`user` = 自行添加，`rivals` = 该产品的 AI 引用来源。"
          },
          {
            "name": "pricing",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "enum": [
                "free",
                "conditional",
                "paid",
                "unknown"
              ]
            },
            "description": "Cost to submit.",
            "x-description-zh": "投递费用。"
          },
          {
            "name": "q",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Search name, domain and topics.",
            "x-description-zh": "按名称、域名与主题搜索。"
          },
          {
            "name": "hideSubmitted",
            "in": "query",
            "required": false,
            "schema": {
              "type": "boolean"
            },
            "description": "Leave out channels this product already has an open or submitted task for. Needs `productId`.",
            "x-description-zh": "隐藏该产品已有进行中或已投递任务的渠道。需要 `productId`。"
          },
          {
            "name": "page",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 1
            },
            "description": "Defaults to 1.",
            "x-description-zh": "缺省 1。"
          },
          {
            "name": "pageSize",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100
            },
            "description": "Defaults to 30.",
            "x-description-zh": "缺省 30。"
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "success",
                    "data"
                  ],
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    },
                    "data": {
                      "$ref": "#/components/schemas/ChannelList"
                    }
                  }
                }
              }
            },
            "x-description-zh": "成功"
          },
          "401": {
            "description": "Authentication failed",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "x-description-zh": "鉴权失败"
          }
        },
        "x-summary-zh": "可投递的入口，按匹配度排序",
        "x-description-zh": "内置渠道（目录、启动平台、AI 工具榜、社区、内容平台）、你自行添加的条目，以及 —— 传了 `productId` 时 —— AI 答案在该产品的搜索词上引用过的站点（`source: \"rivals\"`）。带 `productId` 时按相关性排序，每条渠道带 `taskStatus`（该产品在此已有任务时非空）。免费。\n\n**`citedByAi` 表示 AI 答案在你的搜索词上引用过该站，🚫 不表示该站会收录你的产品** —— 去争取被提及，正是任务要做的事。"
      }
    },
    "/v1/campaigns": {
      "get": {
        "tags": [
          "Distribution"
        ],
        "operationId": "listCampaigns",
        "summary": "Campaigns and your distribution quota",
        "description": "Campaigns (archived ones excluded unless `includeArchived=true`) plus the plan's distribution limits and current usage. Free.",
        "parameters": [
          {
            "name": "productId",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Only this product's campaigns.",
            "x-description-zh": "只看该产品的计划。"
          },
          {
            "name": "includeArchived",
            "in": "query",
            "required": false,
            "schema": {
              "type": "boolean"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "success",
                    "data"
                  ],
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    },
                    "data": {
                      "$ref": "#/components/schemas/CampaignList"
                    }
                  }
                }
              }
            },
            "x-description-zh": "成功"
          },
          "401": {
            "description": "Authentication failed",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "x-description-zh": "鉴权失败"
          }
        },
        "x-summary-zh": "分发计划与分发配额",
        "x-description-zh": "分发计划（缺省不含已归档；`includeArchived=true` 才含），以及当前档位的分发上限与已用量。免费。"
      },
      "post": {
        "tags": [
          "Distribution"
        ],
        "operationId": "createCampaign",
        "summary": "Create a campaign from an explicit list of channels",
        "description": "One task per channel id, with its submission material prepared from the product profile at once, at no cost. Pass the channels that were actually chosen — a campaign is the record of where you decided to submit, not a filter the server expands.\n\nChannels the product already has an open or submitted task for are skipped and listed in `skipped` with a reason (`already_open`, `already_submitted`, `not_found`, `broken`, `inactive`, `other_product`). If nothing is left, the call returns HTTP 200 with `ok: false` and `failure: \"no_valid_targets\"`. Exceeding the plan's distribution quota is a refusal: **409 `quota_exceeded`** with `dimension`, `limit`, `used` and `requested` — nothing is created, not even the part that would have fit.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CreateCampaignRequest"
              },
              "example": {
                "productId": "clx1prod000",
                "name": "Directories, September",
                "targetIds": [
                  "clx1tgt001",
                  "clx1tgt002"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "success",
                    "data"
                  ],
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    },
                    "data": {
                      "$ref": "#/components/schemas/CreateCampaignOutcome"
                    }
                  }
                }
              }
            },
            "x-description-zh": "成功"
          },
          "400": {
            "description": "`product_id_required`, `name_required` / `name_too_long` (80), `target_ids_required` / `too_many_targets` (100), or `invalid_date`",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "x-description-zh": "`product_id_required`、`name_required` / `name_too_long`（80 字）、`target_ids_required` / `too_many_targets`（100 条）或 `invalid_date`"
          },
          "401": {
            "description": "Authentication failed",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "x-description-zh": "鉴权失败"
          },
          "403": {
            "description": "`missing_scope_publish` — this key was not granted the `publish` scope",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "x-description-zh": "`missing_scope_publish` —— 这把密钥没有 `publish` 权限"
          },
          "404": {
            "description": "`product_not_found` — the productId does not belong to this account",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "x-description-zh": "`product_not_found` —— 该 productId 不属于这个账号"
          },
          "409": {
            "description": "`quota_exceeded` — body carries `dimension` (`active_campaigns` / `tasks_per_month` / `channels`), `limit`, `used`, `requested`",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "x-description-zh": "`quota_exceeded` —— 响应体带 `dimension`（`active_campaigns` / `tasks_per_month` / `channels`）、`limit`、`used`、`requested`"
          }
        },
        "x-summary-zh": "按明确的渠道清单建计划",
        "x-description-zh": "每个渠道 id 一条任务，投递素材当场按产品档案备好，零成本。请传**实际选定**的渠道 —— 计划是「决定投哪里」的记录，🚫 不是由服务端展开的筛选条件。\n\n该产品已有进行中或已投递任务的渠道会被剔除，列在 `skipped` 里并给出理由（`already_open`、`already_submitted`、`not_found`、`broken`、`inactive`、`other_product`）。一条都不剩时返回 HTTP 200，`ok: false` 且 `failure: \"no_valid_targets\"`。超出当前档位的分发配额是拒绝：**409 `quota_exceeded`**，响应体带 `dimension`、`limit`、`used`、`requested` —— 什么都不会建，🚫 不做「能建几条建几条」。"
      }
    },
    "/v1/campaigns/{id}": {
      "get": {
        "tags": [
          "Distribution"
        ],
        "operationId": "getCampaign",
        "summary": "One campaign with its tasks",
        "description": "The campaign and every task in it. Free.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Campaign id from `POST /v1/campaigns` or `GET /v1/campaigns`.",
            "x-description-zh": "来自 `POST /v1/campaigns` 或 `GET /v1/campaigns` 的计划 id。"
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "success",
                    "data"
                  ],
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    },
                    "data": {
                      "$ref": "#/components/schemas/CampaignDetail"
                    }
                  }
                }
              }
            },
            "x-description-zh": "成功"
          },
          "401": {
            "description": "Authentication failed",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "x-description-zh": "鉴权失败"
          },
          "404": {
            "description": "`campaign_not_found`",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "x-description-zh": "`campaign_not_found`"
          }
        },
        "x-summary-zh": "一条计划及其任务",
        "x-description-zh": "该计划与其中的每条任务。免费。"
      }
    },
    "/v1/tasks": {
      "get": {
        "tags": [
          "Distribution"
        ],
        "operationId": "listTasks",
        "summary": "Tasks across campaigns",
        "description": "The submission record, newest activity first. Filter by product, campaign or a comma-separated list of statuses. `byStatus` counts every task in scope before the status filter is applied. Free.",
        "parameters": [
          {
            "name": "productId",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Only this product's tasks.",
            "x-description-zh": "只看该产品的任务。"
          },
          {
            "name": "campaignId",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "status",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Comma-separated, e.g. `prepared,in_progress`.",
            "x-description-zh": "逗号分隔，如 `prepared,in_progress`。"
          },
          {
            "name": "page",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 1
            },
            "description": "Defaults to 1.",
            "x-description-zh": "缺省 1。"
          },
          {
            "name": "pageSize",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100
            },
            "description": "Defaults to 30.",
            "x-description-zh": "缺省 30。"
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "success",
                    "data"
                  ],
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    },
                    "data": {
                      "$ref": "#/components/schemas/TaskList"
                    }
                  }
                }
              }
            },
            "x-description-zh": "成功"
          },
          "401": {
            "description": "Authentication failed",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "x-description-zh": "鉴权失败"
          }
        },
        "x-summary-zh": "跨计划的任务流水",
        "x-description-zh": "投递记录，按最近更新排。可按产品、计划或逗号分隔的状态列表筛选。`byStatus` 统计的是状态筛选**之前**范围内的全部任务。免费。"
      }
    },
    "/v1/tasks/{id}": {
      "get": {
        "tags": [
          "Distribution"
        ],
        "operationId": "getTask",
        "summary": "One task with the material to submit",
        "description": "Every field the channel's form asks for, cut from the product profile to the channel's limits (`source: \"profile\"`), plus the channel-specific rewrite if one exists (`source: \"ai\"`) and any edits made in the dashboard (`source: \"override\"`). `source: \"none\"` means the profile has nothing for that field — do not invent it. Never triggers a rewrite and never costs anything.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Task id from a campaign or `GET /v1/tasks`.",
            "x-description-zh": "来自计划或 `GET /v1/tasks` 的任务 id。"
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "success",
                    "data"
                  ],
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    },
                    "data": {
                      "$ref": "#/components/schemas/TaskDetailResult"
                    }
                  }
                }
              }
            },
            "x-description-zh": "成功"
          },
          "401": {
            "description": "Authentication failed",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "x-description-zh": "鉴权失败"
          },
          "404": {
            "description": "`task_not_found`",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "x-description-zh": "`task_not_found`"
          }
        },
        "x-summary-zh": "一条任务及其投递素材",
        "x-description-zh": "该渠道表单要求的每一格，按渠道的字数上限从产品档案裁出（`source: \"profile\"`），加上已有的渠道改写版本（`source: \"ai\"`）与在工作台里定稿的修改（`source: \"override\"`）。`source: \"none\"` 表示档案里没有这一格 —— 🚫 不要自行编造。🚫 不会触发改写，不消耗任何积分。"
      }
    },
    "/v1/tasks/{id}/materials": {
      "post": {
        "tags": [
          "Distribution"
        ],
        "operationId": "writeTaskMaterials",
        "summary": "Rewrite the material for this channel (costs credits)",
        "description": "One AI pass that adapts the profile for this specific channel: a tighter directory description, a maker's first comment, a community post, a pitch to an editor, or — for AI-cited sites — a pitch plus a paragraph the page owner could add. Synchronous, a few seconds. Uses only facts in the profile; any part containing a link that is not in the profile is discarded and not charged.\n\nIdentical inputs return the previous version **without charging** (`cached: true`); `force: true` rewrites anyway and is charged. The profile material from `GET /v1/tasks/{id}` is usually enough for directories; rewrite when the channel wants a different voice.\n\nReturns HTTP 200 with `ok: false` and `failure: \"engine_failed\"` when nothing survived validation. Insufficient credits is a real 402.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Task id.",
            "x-description-zh": "任务 id。"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/WriteMaterialsRequest"
              },
              "example": {
                "confirmSpend": 10
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "success",
                    "data"
                  ],
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    },
                    "data": {
                      "$ref": "#/components/schemas/WriteMaterialsOutcome"
                    }
                  }
                }
              }
            },
            "x-description-zh": "成功"
          },
          "400": {
            "description": "`confirm_spend_required` or `confirm_spend_too_low` (the body carries the current `price`)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "x-description-zh": "`confirm_spend_required` 或 `confirm_spend_too_low`（响应体里带当前 `price`）"
          },
          "401": {
            "description": "Authentication failed",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "x-description-zh": "鉴权失败"
          },
          "402": {
            "description": "`insufficient_credits` — body carries `requiredCredits`, `currentBalance`, `shortfall`",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "x-description-zh": "`insufficient_credits` —— 响应体带 `requiredCredits`、`currentBalance`、`shortfall`"
          },
          "403": {
            "description": "`missing_scope_spend` — this key was not granted the `spend` scope",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "x-description-zh": "`missing_scope_spend` —— 这把密钥没有 `spend` 权限"
          },
          "404": {
            "description": "`task_not_found`",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "x-description-zh": "`task_not_found`"
          },
          "429": {
            "description": "`rate_limited` or `daily_limit_reached` (body carries `resetAt`)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "x-description-zh": "`rate_limited` 或 `daily_limit_reached`（响应体带 `resetAt`）"
          }
        },
        "x-summary-zh": "为该渠道改写素材（扣积分）",
        "x-description-zh": "一次 AI 改写，把档案改成适合这个渠道的说法：更紧凑的目录描述、创始人首评、社区帖、给编辑的投稿信，或 —— 对 AI 引用来源 —— 投稿信加一段可供对方页面直接采用的补充段落。同步返回，几秒钟。只依据档案中的事实；任何一段出现档案之外的链接就整段舍弃、不收费。\n\n输入不变时返回上一版且**不重复收费**（`cached: true`）；`force: true` 强制重写，照价收费。`GET /v1/tasks/{id}` 里从档案裁出的素材通常已够投目录；渠道需要另一种语气（社区、投稿信）时再改写。\n\n产物一段都没通过校验时返回 HTTP 200，`ok: false` 且 `failure: \"engine_failed\"`。积分不足才是真正的 402。"
      }
    },
    "/v1/tasks/{id}/status": {
      "post": {
        "tags": [
          "Distribution"
        ],
        "operationId": "reportTaskStatus",
        "summary": "Report what happened with the submission",
        "description": "Record the outcome of a submission you made with your own accounts. QueryWin never submits anywhere itself. Set `submitted` once the form went in, then `published` with the **listing URL** (the entry or post itself, not the site home page) once it is live; QueryWin re-checks that page about 72 hours later for a link to your product (directories, AI tool lists) or a mention (everything else) and sets `verified` itself — you cannot set it.\n\n`blocked` is \"needs a person\": pass `reason` (`login`, `captcha`, `payment`, `missing_material`, `other`). `failed` / `skipped` close the task; `prepared` puts it back. Transitions the state machine does not allow are **409 `transition_not_allowed`**; the task's `next` field lists what is allowed from its current status.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Task id.",
            "x-description-zh": "任务 id。"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/TaskStatusRequest"
              },
              "example": {
                "status": "published",
                "listingUrl": "https://example-directory.com/tools/your-product"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "success",
                    "data"
                  ],
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    },
                    "data": {
                      "$ref": "#/components/schemas/TaskDetailResult"
                    }
                  }
                }
              }
            },
            "x-description-zh": "成功"
          },
          "400": {
            "description": "`invalid_status`, `listing_url_required` (published needs `listingUrl`), `invalid_listing_url`, or `reason_required` (blocked needs `reason`)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "x-description-zh": "`invalid_status`、`listing_url_required`（`published` 必须带 `listingUrl`）、`invalid_listing_url` 或 `reason_required`（`blocked` 必须带 `reason`）"
          },
          "401": {
            "description": "Authentication failed",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "x-description-zh": "鉴权失败"
          },
          "403": {
            "description": "`missing_scope_publish` — this key was not granted the `publish` scope",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "x-description-zh": "`missing_scope_publish` —— 这把密钥没有 `publish` 权限"
          },
          "404": {
            "description": "`task_not_found`",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "x-description-zh": "`task_not_found`"
          },
          "409": {
            "description": "`transition_not_allowed` — read the task; `next` lists the allowed statuses",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "x-description-zh": "`transition_not_allowed` —— 先读任务，`next` 列出允许的状态"
          }
        },
        "x-summary-zh": "回写投递结果",
        "x-description-zh": "记录你用自己的账号投递之后发生的事。QueryWin 从不代你向任何站点投递。表单提交后置为 `submitted`；条目上线后置为 `published` 并带**上线地址**（条目或帖子本身，🚫 不是站点首页）；QueryWin 会在约 72 小时后回查该页面是否出现指向产品的链接（目录、AI 工具榜）或品牌提及（其余类型），并自行置为 `verified` —— 你不能设置它。\n\n`blocked` 表示「需要人处理」：请带 `reason`（`login`、`captcha`、`payment`、`missing_material`、`other`）。`failed` / `skipped` 关闭任务；`prepared` 放回待投。状态机不允许的转换返回 **409 `transition_not_allowed`**；任务的 `next` 字段列出当前状态能推到哪些状态。"
      }
    }
  },
  "components": {
    "securitySchemes": {
      "bearerAuth": {
        "type": "http",
        "scheme": "bearer",
        "description": "Your API key: `Authorization: Bearer qw_live_...`",
        "x-description-zh": "你的密钥：`Authorization: Bearer qw_live_...`"
      },
      "apiKeyHeader": {
        "type": "apiKey",
        "in": "header",
        "name": "X-API-Key",
        "description": "Alternative for tools that only let you set one header.",
        "x-description-zh": "给只能配置一个请求头的工具用的替代方案。"
      }
    },
    "schemas": {
      "Error": {
        "type": "object",
        "required": [
          "success",
          "error"
        ],
        "properties": {
          "success": {
            "type": "boolean",
            "enum": [
              false
            ]
          },
          "error": {
            "type": "string",
            "description": "A machine-readable code, never a sentence.",
            "x-description-zh": "机器可读的错误码，🚫 不是一句话。"
          }
        }
      },
      "SiteList": {
        "type": "object",
        "properties": {
          "sites": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Site"
            }
          }
        }
      },
      "Site": {
        "type": "object",
        "properties": {
          "siteId": {
            "type": "string"
          },
          "domain": {
            "type": "string",
            "example": "example.com"
          },
          "gscProperty": {
            "type": "string",
            "description": "The Search Console property, verbatim: `sc-domain:example.com` or `https://example.com/`.",
            "x-description-zh": "Search Console 里的资源标识，原样返回：`sc-domain:example.com` 或 `https://example.com/`。"
          },
          "syncStatus": {
            "type": "string",
            "enum": [
              "pending",
              "syncing",
              "done",
              "failed"
            ]
          },
          "syncedThrough": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "Search Console lags 2-3 days. Every metric on this site is \"as of\" this date — say so if you surface the numbers anywhere.",
            "x-description-zh": "Search Console 的数据有 2–3 天延迟。这个网站上的每个指标都是「截至该日期」的 —— 你要是把这些数字展示出去，请一并说明这一点。"
          }
        }
      },
      "Usage": {
        "type": "object",
        "properties": {
          "scopes": {
            "type": "array",
            "items": {
              "type": "string",
              "enum": [
                "read",
                "publish",
                "spend"
              ]
            },
            "description": "What this key may do. Read it once at startup rather than discovering your permissions by hitting a 403.",
            "x-description-zh": "这把密钥能做什么。启动时读一次，🚫 不要靠撞 403 才发现自己没权限。"
          },
          "credits": {
            "type": "object",
            "properties": {
              "balance": {
                "type": "integer"
              }
            }
          },
          "outline": {
            "$ref": "#/components/schemas/StepUsage"
          },
          "article": {
            "$ref": "#/components/schemas/StepUsage"
          },
          "materials": {
            "$ref": "#/components/schemas/StepUsage"
          },
          "distribution": {
            "$ref": "#/components/schemas/DistributionQuota"
          }
        }
      },
      "StepUsage": {
        "type": "object",
        "properties": {
          "available": {
            "type": "boolean",
            "description": "False when the generation engine is not configured. Do not call the POST endpoint.",
            "x-description-zh": "生成引擎未配置时为 false。此时不要调用对应的 POST 接口。"
          },
          "pricePerOutline": {
            "type": "integer",
            "description": "Credits per outline (present on `outline` only).",
            "x-description-zh": "每份大纲消耗的积分（仅 `outline` 上有）。"
          },
          "pricePerArticle": {
            "type": "integer",
            "description": "Credits per draft (present on `article` only).",
            "x-description-zh": "每篇正文消耗的积分（仅 `article` 上有）。"
          },
          "pricePerTask": {
            "type": "integer",
            "description": "Credits per channel rewrite (present on `materials` only).",
            "x-description-zh": "每次渠道改写的积分（只在 `materials` 上有）。"
          },
          "freeRemaining": {
            "type": "integer",
            "description": "Free generations left on this account, counted by distinct topic (outlines, drafts) or distinct task (materials) — not by button presses. Free generations still require `confirmSpend`.",
            "x-description-zh": "该账号还剩多少次免费生成，按**不同选题**（大纲、正文）或**不同任务**（素材）计数，🚫 不是按点了几次按钮。免费生成同样要求传 `confirmSpend`。"
          },
          "daily": {
            "$ref": "#/components/schemas/DailyLimit"
          }
        }
      },
      "DailyLimit": {
        "type": "object",
        "description": "A backstop against a runaway script, counted in the database across every channel (the web UI counts toward it too). It resets at local midnight, not on a sliding window.",
        "properties": {
          "used": {
            "type": "integer"
          },
          "limit": {
            "type": "integer"
          },
          "remaining": {
            "type": "integer"
          },
          "resetAt": {
            "type": "string",
            "format": "date-time"
          }
        },
        "x-description-zh": "防止脚本失控的兜底闸门，在数据库里跨所有入口计数（界面上的操作同样计入）。按本地时间零点归零，🚫 不是滑动窗口。"
      },
      "TopicList": {
        "type": "object",
        "properties": {
          "siteId": {
            "type": [
              "string",
              "null"
            ]
          },
          "topics": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Topic"
            }
          },
          "totalQueries": {
            "type": "integer",
            "description": "How many distinct searches these topics cover in total.",
            "x-description-zh": "这些选题一共覆盖多少个不同的搜索词。"
          }
        }
      },
      "Topic": {
        "type": "object",
        "properties": {
          "key": {
            "type": "string",
            "description": "The cluster key — pass it back as `key` to every other topic endpoint. It is the normalized text of the representative search, so it can contain spaces, slashes and non-Latin characters. Always send it in the query string or body, never in a URL path.",
            "x-description-zh": "选题标识 —— 调用其余选题类接口时把它原样作为 `key` 传回。它是代表性搜索词归一化之后的文本，因此可能包含空格、斜杠和非拉丁字符。请始终放在查询串或请求体里，🚫 绝不要放进 URL 路径。"
          },
          "title": {
            "type": "string",
            "description": "The highest-impression search in the cluster, verbatim. This is **not** a generated headline — that comes with the outline.",
            "x-description-zh": "该选题簇中曝光最高的那个搜索词，原样返回。这**不是**生成的标题 —— 标题随大纲一起给出。"
          },
          "shape": {
            "type": "string",
            "enum": [
              "comparison",
              "roundup",
              "guide"
            ],
            "description": "`comparison` means this cluster hit your competitor list. The article must contrast, not explain the competitor — otherwise you are writing content for them.",
            "x-description-zh": "`comparison` 表示这条选题命中了你的竞品清单。文章必须写成对比，🚫 不能去介绍竞品 —— 否则你是在替对方写内容。"
          },
          "intent": {
            "type": "string"
          },
          "members": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/TopicMember"
            }
          },
          "impressions": {
            "type": "integer",
            "description": "Measured, from Search Console.",
            "x-description-zh": "实测值，来自 Search Console。"
          },
          "clicks": {
            "type": "integer",
            "description": "Measured, from Search Console.",
            "x-description-zh": "实测值，来自 Search Console。"
          },
          "position": {
            "type": [
              "number",
              "null"
            ],
            "description": "Measured: impression-weighted average position across the cluster.",
            "x-description-zh": "实测值：该选题簇内按曝光加权的平均排名。"
          },
          "competitor": {
            "type": "boolean"
          },
          "score": {
            "type": "number"
          },
          "rank": {
            "type": "integer"
          },
          "upsideClicks": {
            "type": "integer",
            "description": "**An estimate**, not a measurement: extra monthly clicks if a dedicated page reached position 3. It is deliberately a separate field from `clicks` and `impressions`, and it must stay visually separate wherever you display it. Presenting a projection as measured data is the standard failure of this product category.",
            "x-description-zh": "**这是估算**，不是实测：若新建对应页面并进入第 3 位，每月预计增加的点击数。它被刻意与 `clicks`、`impressions` 分成不同字段，你在任何地方展示它时也必须保持视觉上分开。把预测当实测数据呈现，是这个品类最常见的翻车方式。"
          },
          "status": {
            "type": "string",
            "enum": [
              "new",
              "dismissed",
              "planned",
              "published"
            ]
          },
          "outlineAt": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "articleAt": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "Do not infer this from `outlineAt`. Having an outline does not mean a draft exists — they are two separate paid steps.",
            "x-description-zh": "🚫 不要从 `outlineAt` 推断它。有大纲不等于有正文 —— 那是两个分别收费的步骤。"
          }
        }
      },
      "TopicMember": {
        "type": "object",
        "properties": {
          "text": {
            "type": "string",
            "description": "The search, as typed.",
            "x-description-zh": "用户实际输入的搜索词。"
          },
          "impressions": {
            "type": "integer"
          },
          "clicks": {
            "type": "integer"
          },
          "position": {
            "type": [
              "number",
              "null"
            ]
          },
          "landingUrl": {
            "type": [
              "string",
              "null"
            ],
            "description": "The page Search Console currently records for this search, if any.",
            "x-description-zh": "Search Console 当前为该搜索词记录的落地页，可能没有。"
          }
        }
      },
      "OutlineResult": {
        "type": "object",
        "properties": {
          "outline": {
            "oneOf": [
              {
                "$ref": "#/components/schemas/Outline"
              },
              {
                "type": "null"
              }
            ]
          },
          "outlineAt": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "model": {
            "type": [
              "string",
              "null"
            ]
          }
        }
      },
      "OutlineOutcome": {
        "type": "object",
        "properties": {
          "ok": {
            "type": "boolean"
          },
          "failure": {
            "type": "string",
            "enum": [
              "topic_not_found",
              "engine_unavailable",
              "engine_failed",
              "no_valid_outline"
            ],
            "description": "Present only when `ok` is false. HTTP is still 200 — this is an outcome, not an error.",
            "x-description-zh": "仅当 `ok` 为 false 时出现。HTTP 仍然是 200 —— 这是业务结果，不是错误。"
          },
          "outline": {
            "oneOf": [
              {
                "$ref": "#/components/schemas/Outline"
              },
              {
                "type": "null"
              }
            ]
          },
          "generated": {
            "type": "boolean",
            "description": "False when a cached result was returned — nothing was charged.",
            "x-description-zh": "返回缓存结果时为 false —— 本次未收费。"
          },
          "creditsSpent": {
            "type": "integer"
          },
          "freeUsed": {
            "type": "boolean"
          },
          "rejected": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Validation rules the model output tripped. Worth logging as a quality signal.",
            "x-description-zh": "模型输出触发的校验规则。值得记录下来，作为质量信号。"
          }
        }
      },
      "Outline": {
        "type": "object",
        "properties": {
          "title": {
            "type": "string"
          },
          "slug": {
            "type": "string"
          },
          "angle": {
            "type": "string",
            "description": "The argument this page should make.",
            "x-description-zh": "这一页要论证的角度。"
          },
          "sections": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "heading": {
                  "type": "string"
                },
                "points": {
                  "type": "array",
                  "items": {
                    "type": "string"
                  }
                }
              }
            }
          },
          "faq": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "question": {
                  "type": "string"
                },
                "answer": {
                  "type": "string"
                }
              }
            },
            "description": "Questions the page must answer. These are the hooks AI answers quote.",
            "x-description-zh": "页面必须回答的问题。AI 答案摘取的正是这些。"
          },
          "schemaType": {
            "type": "string",
            "description": "Which JSON-LD type suits this page.",
            "x-description-zh": "这一页适合用哪种 JSON-LD 类型。"
          },
          "internalLinks": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Pages on your own site worth linking to. Chosen from real URLs, never invented.",
            "x-description-zh": "你自己网站上值得链过去的页面。从真实 URL 中挑选，🚫 绝不虚构。"
          }
        }
      },
      "ArticleResult": {
        "type": "object",
        "properties": {
          "article": {
            "oneOf": [
              {
                "$ref": "#/components/schemas/Article"
              },
              {
                "type": "null"
              }
            ]
          },
          "articleAt": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "model": {
            "type": [
              "string",
              "null"
            ]
          }
        }
      },
      "ArticleOutcome": {
        "type": "object",
        "properties": {
          "ok": {
            "type": "boolean"
          },
          "failure": {
            "type": "string",
            "enum": [
              "topic_not_found",
              "no_outline",
              "engine_unavailable",
              "engine_failed",
              "no_valid_article"
            ],
            "description": "Present only when `ok` is false. HTTP is still 200.",
            "x-description-zh": "仅当 `ok` 为 false 时出现。HTTP 仍然是 200。"
          },
          "article": {
            "oneOf": [
              {
                "$ref": "#/components/schemas/Article"
              },
              {
                "type": "null"
              }
            ]
          },
          "generated": {
            "type": "boolean",
            "description": "False when a cached result was returned — nothing was charged.",
            "x-description-zh": "返回缓存结果时为 false —— 本次未收费。"
          },
          "creditsSpent": {
            "type": "integer"
          },
          "freeUsed": {
            "type": "boolean"
          },
          "rejected": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Structural failures that caused a draft to be discarded (and not charged): `missing_sections`, `missing_faq`, `invented_link`, `invalid_json_ld`, `comparison_without_contrast`, `body_too_short`.",
            "x-description-zh": "导致草稿被丢弃（且不收费）的结构性问题：`missing_sections`、`missing_faq`、`invented_link`、`invalid_json_ld`、`comparison_without_contrast`、`body_too_short`。"
          }
        }
      },
      "Article": {
        "type": "object",
        "properties": {
          "title": {
            "type": "string"
          },
          "description": {
            "type": "string",
            "description": "Meta description.",
            "x-description-zh": "Meta 描述。"
          },
          "markdown": {
            "type": "string",
            "description": "The body to publish.",
            "x-description-zh": "可发布的正文。"
          },
          "jsonLd": {
            "type": "string",
            "description": "Structured data for this article, already filled in. Valid JSON — put it inside a script tag of type `application/ld+json` on the published page.",
            "x-description-zh": "这篇文章的结构化数据，已填好内容。合法 JSON —— 发布时放进页面上 `type=\"application/ld+json\"` 的 script 标签里。"
          },
          "wordCount": {
            "type": "integer"
          },
          "warnings": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ArticleWarning"
            },
            "description": "Phrases that read as AI-generated. The draft is still usable — these are reported rather than silently rewritten. **Log them.** In an automated pipeline this is the only moment anyone could notice.",
            "x-description-zh": "读起来像 AI 写的那些句子。草稿仍然可用 —— 我们把它们报出来，而不是悄悄改掉。**请记录它们。** 在自动化流程里，这是唯一一个还有人能发现问题的时刻。"
          }
        }
      },
      "ArticleWarning": {
        "type": "object",
        "properties": {
          "kind": {
            "type": "string",
            "enum": [
              "banned_phrase",
              "banned_word",
              "em_dash_overuse",
              "no_short_sentence"
            ]
          },
          "hit": {
            "type": "string",
            "description": "The offending text.",
            "x-description-zh": "命中的那段原文。"
          }
        }
      },
      "GenerateRequest": {
        "type": "object",
        "required": [
          "key",
          "confirmSpend"
        ],
        "properties": {
          "key": {
            "type": "string",
            "description": "The cluster key from `GET /v1/topics`.",
            "x-description-zh": "来自 `GET /v1/topics` 的选题标识。"
          },
          "siteId": {
            "type": "string",
            "description": "Defaults to the earliest connected site.",
            "x-description-zh": "不传则用最早接入的网站。"
          },
          "confirmSpend": {
            "type": "integer",
            "minimum": 0,
            "description": "An authorization **ceiling** in credits, not an exact amount. Send a value >= the current price from `GET /v1/usage`; you are charged what it actually costs, which is often zero on a cache hit. If the price ever exceeds your ceiling the call is refused rather than silently charging more. Required even when you have free allowance left — the allowance runs out, and that should not be the moment your script first discovers this endpoint costs money.",
            "x-description-zh": "**授权上限，不是精确金额。** 传一个不小于当前单价的数即可，实际花多少扣多少 —— 命中缓存时往往为 0。一旦价格涨到超过你的上限，请求会以 `confirm_spend_too_low` 失败，而不是悄悄多扣。当前单价从 `GET /v1/usage` 读。\n\n🔴 这个字段是**必填**的：界面上那个写着价格的确认弹窗，在脚本里并不存在。"
          }
        }
      },
      "DistributionQuota": {
        "type": "object",
        "description": "The plan's distribution limits and current usage. A null limit means unlimited.",
        "properties": {
          "plan": {
            "type": "string"
          },
          "limits": {
            "type": "object",
            "properties": {
              "channels": {
                "type": [
                  "integer",
                  "null"
                ],
                "description": "Distinct channels one product may have tasks for, cumulative.",
                "x-description-zh": "单个产品累计可投递的不同渠道数。"
              },
              "tasksPerMonth": {
                "type": [
                  "integer",
                  "null"
                ],
                "description": "Tasks that may be created per calendar month (UTC), across products.",
                "x-description-zh": "每个自然月（UTC）可新建的任务数，各产品合计。"
              },
              "activeCampaigns": {
                "type": [
                  "integer",
                  "null"
                ],
                "description": "Campaigns that may be active at once.",
                "x-description-zh": "同时进行中的计划数。"
              }
            }
          },
          "usage": {
            "type": "object",
            "properties": {
              "activeCampaigns": {
                "type": "integer"
              },
              "tasksThisMonth": {
                "type": "integer"
              },
              "channels": {
                "type": [
                  "integer",
                  "null"
                ],
                "description": "Only when the request named a product.",
                "x-description-zh": "只在请求指定了产品时有值。"
              }
            }
          }
        },
        "x-description-zh": "当前档位的分发上限与已用量。上限为 null 表示不限。"
      },
      "ProductList": {
        "type": "object",
        "properties": {
          "products": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Product"
            }
          }
        }
      },
      "Product": {
        "type": "object",
        "properties": {
          "productId": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "url": {
            "type": "string"
          },
          "domain": {
            "type": "string"
          },
          "primaryLanguage": {
            "type": "string",
            "enum": [
              "en",
              "zh"
            ]
          },
          "topics": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "completeness": {
            "type": "integer",
            "description": "Profile completeness, 0–100. Filled in the dashboard.",
            "x-description-zh": "档案完整度 0–100。在工作台里填写。"
          },
          "missing": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Profile fields that are empty. Each one is a gap in every submission.",
            "x-description-zh": "档案里空着的格。每一格都是所有投递素材里的空缺。"
          },
          "sites": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "siteId": {
                  "type": "string"
                },
                "domain": {
                  "type": "string"
                },
                "syncedThrough": {
                  "type": [
                    "string",
                    "null"
                  ],
                  "format": "date-time"
                }
              }
            }
          }
        }
      },
      "ChannelList": {
        "type": "object",
        "properties": {
          "productId": {
            "type": [
              "string",
              "null"
            ]
          },
          "items": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Channel"
            }
          },
          "total": {
            "type": "integer"
          },
          "page": {
            "type": "integer"
          },
          "pageSize": {
            "type": "integer"
          }
        }
      },
      "Channel": {
        "type": "object",
        "properties": {
          "targetId": {
            "type": "string",
            "description": "Pass these as `targetIds` to `POST /v1/campaigns`.",
            "x-description-zh": "作为 `targetIds` 传给 `POST /v1/campaigns`。"
          },
          "name": {
            "type": "string"
          },
          "url": {
            "type": "string"
          },
          "submitUrl": {
            "type": "string",
            "description": "The submission form or posting page; for AI-cited sites, the page AI answers cite most.",
            "x-description-zh": "提交表单或发帖页；对 AI 引用来源，是被 AI 答案引用最多的那一页。"
          },
          "kind": {
            "type": "string",
            "enum": [
              "directory",
              "launch",
              "ai_directory",
              "community",
              "content",
              "other"
            ]
          },
          "source": {
            "type": "string",
            "enum": [
              "seed",
              "user",
              "rivals"
            ],
            "description": "`seed` = the library, `user` = added by you, `rivals` = a site AI answers cite for this product's searches.",
            "x-description-zh": "`seed` = 内置渠道，`user` = 自行添加，`rivals` = AI 答案在该产品的搜索词上引用过的站点。"
          },
          "pricingType": {
            "type": "string",
            "enum": [
              "free",
              "conditional",
              "paid",
              "unknown"
            ]
          },
          "priceNote": {
            "type": [
              "string",
              "null"
            ]
          },
          "language": {
            "type": "string",
            "description": "`en`, `zh`, `multi`, or a language code detected from the searches for AI-cited sites.",
            "x-description-zh": "`en`、`zh`、`multi`，或 AI 引用来源按搜索词判出的语言代码。"
          },
          "topics": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "requiresAccount": {
            "type": "boolean"
          },
          "requiresBacklink": {
            "type": "boolean"
          },
          "reviewDays": {
            "type": [
              "integer",
              "null"
            ],
            "description": "Typical review time. The task reminds you to check back after it.",
            "x-description-zh": "通常的审核天数。任务会在此之后提醒你回头查看。"
          },
          "hasFormSpec": {
            "type": "boolean",
            "description": "The channel's form fields are registered, so material is cut to its exact limits.",
            "x-description-zh": "该渠道的表单字段已登记，素材按它的字数上限裁。"
          },
          "relevance": {
            "type": [
              "integer",
              "null"
            ],
            "description": "Fit score for the product named in the request. Only for sorting.",
            "x-description-zh": "与请求所指产品的匹配分。只用于排序。"
          },
          "taskStatus": {
            "type": [
              "string",
              "null"
            ],
            "description": "The product's open or completed task for this channel, if any. `null` = nothing yet.",
            "x-description-zh": "该产品在此渠道进行中或已完成的任务状态；`null` = 尚无。"
          },
          "citedByAi": {
            "type": [
              "object",
              "null"
            ],
            "description": "Only for `source: \"rivals\"`. **AI answers cited this site for the listed searches. This is not a promise that the site will list your product** — asking is what the task is for.",
            "properties": {
              "queries": {
                "type": "integer",
                "description": "How many distinct searches it was cited for.",
                "x-description-zh": "在多少条不同的搜索词上被引用。"
              },
              "samples": {
                "type": "integer",
                "description": "How many AI answer samples cited it.",
                "x-description-zh": "多少次 AI 答案采样引用了它。"
              },
              "searches": {
                "type": "array",
                "items": {
                  "type": "string"
                }
              },
              "pages": {
                "type": "array",
                "items": {
                  "type": "string"
                },
                "description": "The cited pages, most-cited first. Empty for older samples that only recorded the domain.",
                "x-description-zh": "被引用的页面，引用最多的在前。较早的采样只记录了域名，此时为空。"
              }
            },
            "x-description-zh": "只有 `source: \"rivals\"` 有。**AI 答案在所列搜索词上引用过该站。这🚫 不是该站会收录你的产品的承诺** —— 去争取正是任务要做的事。"
          }
        }
      },
      "CreateCampaignRequest": {
        "type": "object",
        "required": [
          "productId",
          "name",
          "targetIds"
        ],
        "properties": {
          "productId": {
            "type": "string"
          },
          "name": {
            "type": "string",
            "maxLength": 80
          },
          "targetIds": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "minItems": 1,
            "maxItems": 100,
            "description": "Channel ids from `GET /v1/channels`. Only the channels that were chosen.",
            "x-description-zh": "来自 `GET /v1/channels` 的渠道 id。只传实际选定的渠道。"
          },
          "endsAt": {
            "type": "string",
            "format": "date-time",
            "description": "Optional deadline shown in the dashboard. Nothing closes automatically.",
            "x-description-zh": "可选期限，显示在工作台里。到期不会自动关闭任何东西。"
          }
        }
      },
      "CreateCampaignOutcome": {
        "type": "object",
        "properties": {
          "ok": {
            "type": "boolean"
          },
          "failure": {
            "type": "string",
            "enum": [
              "no_valid_targets"
            ],
            "description": "Present when `ok` is false.",
            "x-description-zh": "`ok` 为 false 时出现。"
          },
          "campaign": {
            "$ref": "#/components/schemas/Campaign"
          },
          "tasks": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Task"
            }
          },
          "skipped": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "targetId": {
                  "type": "string"
                },
                "reason": {
                  "type": "string",
                  "enum": [
                    "not_found",
                    "broken",
                    "inactive",
                    "other_product",
                    "already_open",
                    "already_submitted"
                  ],
                  "description": "`other_product` = an AI-cited candidate that belongs to a different product; `inactive` = a channel that is disabled or was ignored.",
                  "x-description-zh": "`other_product` = 属于别的产品的 AI 引用来源候选；`inactive` = 已停用或被忽略的渠道。"
                }
              }
            }
          }
        }
      },
      "CampaignList": {
        "type": "object",
        "properties": {
          "campaigns": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Campaign"
            }
          },
          "quota": {
            "$ref": "#/components/schemas/DistributionQuota"
          }
        }
      },
      "CampaignDetail": {
        "type": "object",
        "properties": {
          "campaign": {
            "$ref": "#/components/schemas/Campaign"
          },
          "tasks": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Task"
            }
          }
        }
      },
      "Campaign": {
        "type": "object",
        "properties": {
          "campaignId": {
            "type": "string"
          },
          "productId": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "status": {
            "type": "string",
            "enum": [
              "active",
              "completed",
              "archived"
            ],
            "description": "`completed` is computed: every task is published, verified, failed or skipped.",
            "x-description-zh": "`completed` 是算出来的：所有任务都已上线、已核实、失败或跳过。"
          },
          "quota": {
            "type": "integer",
            "description": "How many channels the campaign was created with.",
            "x-description-zh": "建计划时选了多少个渠道。"
          },
          "startsAt": {
            "type": "string",
            "format": "date-time"
          },
          "endsAt": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "counts": {
            "type": "object",
            "properties": {
              "total": {
                "type": "integer"
              },
              "submitted": {
                "type": "integer",
                "description": "submitted + published + verified.",
                "x-description-zh": "submitted + published + verified。"
              },
              "live": {
                "type": "integer",
                "description": "published + verified.",
                "x-description-zh": "published + verified。"
              },
              "blocked": {
                "type": "integer"
              },
              "done": {
                "type": "integer"
              },
              "byStatus": {
                "type": "object",
                "additionalProperties": {
                  "type": "integer"
                }
              }
            }
          },
          "createdAt": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "TaskList": {
        "type": "object",
        "properties": {
          "items": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Task"
            }
          },
          "total": {
            "type": "integer"
          },
          "page": {
            "type": "integer"
          },
          "pageSize": {
            "type": "integer"
          },
          "byStatus": {
            "type": "object",
            "additionalProperties": {
              "type": "integer"
            }
          }
        }
      },
      "Task": {
        "type": "object",
        "properties": {
          "taskId": {
            "type": "string"
          },
          "campaignId": {
            "type": "string"
          },
          "productId": {
            "type": "string"
          },
          "status": {
            "type": "string",
            "enum": [
              "planned",
              "prepared",
              "in_progress",
              "blocked",
              "submitted",
              "published",
              "verified",
              "failed",
              "skipped"
            ],
            "description": "`published` is what **you** reported. `verified` is what **QueryWin saw** on the listing page (a link for directories and AI tool lists, a mention elsewhere). Keep them apart when you report.",
            "x-description-zh": "`published` 是**你**回写的。`verified` 是 **QueryWin 在上线页面上看到的**（目录与 AI 工具榜看链接，其余看提及）。对外转述时请分开说。"
          },
          "blockedReason": {
            "type": [
              "string",
              "null"
            ],
            "enum": [
              "login",
              "captcha",
              "payment",
              "missing_material",
              "other",
              null
            ]
          },
          "missing": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Required material the profile lacks. Complete the profile in the dashboard; the task re-prepares itself.",
            "x-description-zh": "档案缺少的必填素材。在工作台里补齐档案后，任务会自行重新准备。"
          },
          "listingUrl": {
            "type": [
              "string",
              "null"
            ]
          },
          "markedBy": {
            "type": "string",
            "enum": [
              "user",
              "device",
              "system"
            ],
            "description": "Who made the last status change: a person (or this API), the browser extension, or QueryWin itself.",
            "x-description-zh": "最近一次状态变化是谁做的：人（或本接口）、浏览器扩展、或 QueryWin 自身。"
          },
          "hasGenerated": {
            "type": "boolean",
            "description": "A channel-specific rewrite exists.",
            "x-description-zh": "已有为该渠道改写的版本。"
          },
          "reviewDueAt": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "When to check back after submitting (submittedAt + the channel's review days).",
            "x-description-zh": "提交后该回头查看的时间（submittedAt + 该渠道的审核天数）。"
          },
          "submittedAt": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "publishedAt": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "verifiedAt": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "next": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Statuses you may set from the current one via `POST /v1/tasks/{id}/status`.",
            "x-description-zh": "从当前状态出发，可通过 `POST /v1/tasks/{id}/status` 设置的状态。"
          },
          "target": {
            "type": "object",
            "properties": {
              "targetId": {
                "type": "string"
              },
              "name": {
                "type": "string"
              },
              "url": {
                "type": "string"
              },
              "submitUrl": {
                "type": "string"
              },
              "kind": {
                "type": "string"
              },
              "source": {
                "type": "string",
                "enum": [
                  "seed",
                  "user",
                  "rivals"
                ]
              },
              "language": {
                "type": "string"
              },
              "requiresBacklink": {
                "type": "boolean"
              }
            }
          },
          "check": {
            "type": [
              "object",
              "null"
            ],
            "description": "The re-check QueryWin runs on the listing after publication. Null until the task is published.",
            "properties": {
              "kind": {
                "type": "string",
                "enum": [
                  "link_live",
                  "mention_seen"
                ],
                "description": "What is looked for: a link to the product (directories, AI tool lists) or a mention of the brand (everything else).",
                "x-description-zh": "查什么：指向产品的链接（目录、AI 工具榜）或品牌提及（其余类型）。"
              },
              "state": {
                "type": [
                  "string",
                  "null"
                ],
                "enum": [
                  "confirmed",
                  "unconfirmed",
                  "lost",
                  null
                ],
                "description": "`confirmed` = seen. `unconfirmed` = not found in one round of checks (status unchanged; check the URL). `lost` = it was there and is gone (task failed).",
                "x-description-zh": "`confirmed` = 已看到。`unconfirmed` = 一轮回查都没找到（状态不变；请核对地址）。`lost` = 曾经有、现在没了（任务已置为失败）。"
              },
              "checkedAt": {
                "type": [
                  "string",
                  "null"
                ],
                "format": "date-time"
              },
              "dueAt": {
                "type": [
                  "string",
                  "null"
                ],
                "format": "date-time"
              }
            },
            "x-description-zh": "QueryWin 在条目上线后做的回查。任务上线前为 null。"
          },
          "updatedAt": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "TaskDetailResult": {
        "type": "object",
        "properties": {
          "task": {
            "$ref": "#/components/schemas/TaskDetail"
          }
        }
      },
      "TaskDetail": {
        "allOf": [
          {
            "$ref": "#/components/schemas/Task"
          },
          {
            "type": "object",
            "properties": {
              "materials": {
                "type": [
                  "object",
                  "null"
                ],
                "description": "Null only while the task is `planned` (not prepared yet).",
                "properties": {
                  "lang": {
                    "type": "string",
                    "description": "Language of the material: the channel's, or the profile's primary language for multilingual channels.",
                    "x-description-zh": "素材的语言：渠道的语言；多语言渠道用档案的主语言。"
                  },
                  "fields": {
                    "type": "array",
                    "items": {
                      "$ref": "#/components/schemas/MaterialField"
                    }
                  },
                  "generatedAt": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "format": "date-time"
                  },
                  "finalizedAt": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "format": "date-time",
                    "description": "When edits were last made in the dashboard.",
                    "x-description-zh": "最近一次在工作台里定稿的时间。"
                  }
                },
                "x-description-zh": "只在任务还是 `planned`（尚未准备）时为 null。"
              },
              "citedByAi": {
                "type": [
                  "object",
                  "null"
                ],
                "description": "For AI-cited sites only: the evidence behind the outreach. Same shape as on `Channel`.",
                "x-description-zh": "只有 AI 引用来源有：这次争取提及所依据的证据。结构同 `Channel` 上的同名字段。"
              }
            }
          }
        ]
      },
      "MaterialField": {
        "type": "object",
        "properties": {
          "key": {
            "type": "string",
            "description": "`name`, `tagline`, `shortDesc`, `longDesc`, `url`, `topics`, `pricingModel`, `thumbnail`, `gallery`, `firstComment`, `contactEmail`, … plus rewritten parts: `postTitle`, `postBody`, `pitchSubject`, `pitchBody`, `insertParagraph`.",
            "x-description-zh": "`name`、`tagline`、`shortDesc`、`longDesc`、`url`、`topics`、`pricingModel`、`thumbnail`、`gallery`、`firstComment`、`contactEmail` …，以及改写出的段：`postTitle`、`postBody`、`pitchSubject`、`pitchBody`、`insertParagraph`。"
          },
          "value": {
            "type": [
              "string",
              "array",
              "null"
            ],
            "items": {
              "type": "string"
            },
            "description": "A string, a list (topics, gallery), or null when the profile has nothing.",
            "x-description-zh": "字符串、列表（主题、图集），或档案里没有时为 null。"
          },
          "source": {
            "type": "string",
            "enum": [
              "profile",
              "ai",
              "override",
              "none"
            ],
            "description": "`profile` = cut from the product profile. `ai` = rewritten for this channel. `override` = edited in the dashboard. `none` = empty; do not invent it.",
            "x-description-zh": "`profile` = 从产品档案裁的。`ai` = 为该渠道改写的。`override` = 在工作台里定稿改过的。`none` = 空；🚫 不要自行编造。"
          },
          "required": {
            "type": "boolean"
          },
          "max": {
            "type": "integer",
            "description": "The channel's character limit for this field, when registered. Values are already cut to it.",
            "x-description-zh": "该渠道对这一格的字数上限（已登记时）。值已按它裁过。"
          },
          "note": {
            "type": "string"
          },
          "truncated": {
            "type": "boolean",
            "description": "The profile text was longer than the channel allows and was cut at a sentence boundary.",
            "x-description-zh": "档案文本比渠道允许的长，已按句末截断。"
          }
        }
      },
      "WriteMaterialsRequest": {
        "type": "object",
        "required": [
          "confirmSpend"
        ],
        "properties": {
          "confirmSpend": {
            "type": "integer",
            "minimum": 0,
            "description": "Authorization ceiling in credits, same semantics as for outlines and drafts. Read the price from `GET /v1/usage` (`materials.pricePerTask`). Required even while free allowance remains.",
            "x-description-zh": "授权上限（积分），语义与大纲 / 正文相同。单价从 `GET /v1/usage` 的 `materials.pricePerTask` 读。免费额度还在时同样必填。"
          },
          "force": {
            "type": "boolean",
            "description": "Rewrite even if nothing changed since the last version. Charged.",
            "x-description-zh": "即使上一版之后没有任何变化也重写。照价收费。"
          }
        }
      },
      "WriteMaterialsOutcome": {
        "type": "object",
        "properties": {
          "ok": {
            "type": "boolean"
          },
          "failure": {
            "type": "string",
            "enum": [
              "engine_failed",
              "engine_unavailable"
            ],
            "description": "Present when `ok` is false. Nothing is charged.",
            "x-description-zh": "`ok` 为 false 时出现。不收费。"
          },
          "cached": {
            "type": "boolean",
            "description": "Inputs were unchanged; the previous version was returned and nothing was charged.",
            "x-description-zh": "输入未变；返回的是上一版，未收费。"
          },
          "freeUsed": {
            "type": "boolean"
          },
          "creditsSpent": {
            "type": "integer"
          },
          "task": {
            "$ref": "#/components/schemas/TaskDetail"
          }
        }
      },
      "TaskStatusRequest": {
        "type": "object",
        "required": [
          "status"
        ],
        "properties": {
          "status": {
            "type": "string",
            "enum": [
              "prepared",
              "in_progress",
              "blocked",
              "submitted",
              "published",
              "failed",
              "skipped"
            ],
            "description": "`verified` cannot be set; QueryWin sets it after re-checking the listing.",
            "x-description-zh": "`verified` 不能设置；QueryWin 回查条目后自行设置。"
          },
          "listingUrl": {
            "type": "string",
            "format": "uri",
            "description": "Required for `published`: the live entry or post, not the site home page. http/https only. Optional with `submitted` if you already know it.",
            "x-description-zh": "`published` 必填：上线的条目或帖子本身，🚫 不是站点首页。仅支持 http/https。已知地址时也可随 `submitted` 一并传。"
          },
          "note": {
            "type": "string",
            "maxLength": 500
          },
          "reason": {
            "type": "string",
            "enum": [
              "login",
              "captcha",
              "payment",
              "missing_material",
              "other"
            ],
            "description": "Required for `blocked`.",
            "x-description-zh": "`blocked` 必填。"
          }
        }
      },
      "PublishedRequest": {
        "type": "object",
        "required": [
          "key",
          "url"
        ],
        "properties": {
          "key": {
            "type": "string"
          },
          "siteId": {
            "type": "string"
          },
          "url": {
            "type": "string",
            "format": "uri",
            "description": "Where you published it. http/https only. Not fetched at this point.",
            "x-description-zh": "你把它发布在哪里。仅支持 http/https。此时不会去抓取该地址。"
          }
        }
      },
      "PublishedResult": {
        "type": "object",
        "properties": {
          "clusterKey": {
            "type": "string"
          },
          "status": {
            "type": "string",
            "enum": [
              "published"
            ]
          },
          "publishedUrl": {
            "type": "string"
          },
          "publishedAt": {
            "type": "string",
            "format": "date-time"
          },
          "indexnow": {
            "type": "object",
            "description": "Bing, Yandex, Seznam, Naver. Not Google.",
            "properties": {
              "pushed": {
                "type": "boolean"
              },
              "outcome": {
                "type": "string",
                "description": "`skipped` usually means the key file is not verified yet.",
                "x-description-zh": "`skipped` 通常表示密钥文件还没有验证通过。"
              },
              "engines": {
                "type": "string"
              }
            },
            "x-description-zh": "Bing、Yandex、Seznam、Naver。不含 Google。"
          }
        }
      }
    }
  }
}