# Campaign API


Campaign API 让程序读取活动定义、检查候选更改、创建活动或把更改应用到草稿，并执行一次已由人确认的发布。它与订阅数据交付是两个契约：交付记录从 [HTTP 数据接口](/zh/api/api-data)读取，数据读取 Key 没有下面任何一项权限。

## 地址、凭证与权限

正式环境使用 `https://api.humanbased.ai` 和该环境签发的 Key，测试环境使用 `https://api.staging.humanbased.ai` 和测试环境的 Key。所有操作都在 `/api/v1/campaignapi/` 下，请求头为 `Authorization: Bearer <API_KEY>`。

Key 由组织成员在门户创建，创建时选择活动权限，表单见[创建和使用 API Key](/zh/api/api-auth)。服务端对每项权限单独检查。

| 权限 | 这把 Key 可以 | 范围 |
| --- | --- | --- |
| `campaign:read` | 列出并读取活动、版本和模板。 | 只读 Key 可读本组织的所有活动。 |
| `campaign:author` | 验证和规划候选文件、创建活动、把更改应用到草稿、读取应用回执。 | Key 的 `managed_campaign_ids` 列出的活动：创建 Key 时创建者可以编辑的那些，加上这把 Key 自己创建的活动。 |
| `campaign:launch` | 提出发布提案、读回自己的提案，并在人确认后执行。 | 同一份名单，且在 Key 记录的支出上限之内。 |

活动在 Key 的组织或名单之外时，返回 `404` 和 `CAMPAIGN_NOT_FOUND`，与活动根本不存在时的回答相同。

创建 Key 的成员要在本组织至少一个活动上是编辑者或所有者，表单才会签发 `campaign:author` 和 `campaign:launch`。组织的第一个活动请在 Developer 门户创建。

## 确认身份和可读范围

先执行只读身份检查：

```sh
curl --silent --show-error \
  'https://api.humanbased.ai/api/v1/auth/whoami' \
  --header "Authorization: Bearer $HB_API_KEY"
```

继续之前，核对 `data.org_id`、`data.scopes` 和 `data.managed_campaign_ids`。Key 放在环境变量或密钥管理工具中。然后读取当前身份可见的活动：

```sh
curl --silent --show-error \
  'https://api.humanbased.ai/api/v1/campaignapi/campaigns' \
  --header "Authorization: Bearer $HB_API_KEY"
```

每个响应都带 `success`、`errorCode`、`errorMessage` 和 `data`。活动列表可传 `status`、`limit`（1–200）和不透明的 `cursor`。读取 `data.items`；当 `data.has_more` 为 true 时，原样把 `data.next_cursor` 传入下一次请求。单页为空时继续查看 `has_more` 和 `next_cursor`，再判断分页是否结束；标识符按字符串处理。

## 按目的选择接口

| 目的 | 操作 | 所需权限 |
| --- | --- | --- |
| 查找 Campaign 文件可以引用的模板 | `GET /api/v1/campaignapi/templates`、`template`、`catalog` | `campaign:read` |
| 查看活动和已保存的版本 | `GET /api/v1/campaignapi/campaigns`、`campaign`、`versions`、`version`、`versiondiff` | `campaign:read` |
| 只检查候选文件 | `POST /api/v1/campaignapi/candidatevalidate`、`candidateplan` | `campaign:author` |
| 创建活动，或把更改应用到草稿 | `POST /api/v1/campaignapi/candidateapply` | `campaign:author` |
| 读取此前一次应用的回执 | `GET /api/v1/campaignapi/actionreceipt` | `campaign:author` |
| 提出发布提案，并读回提案 | `POST /api/v1/campaignapi/launchpropose`、`GET /api/v1/campaignapi/launchproposal` | `campaign:launch` |
| 确认发布提案 | `POST /api/v1/campaignapi/launchconfirm` | 登录门户的人；Key 会被拒绝 |
| 执行已确认的发布 | `POST /api/v1/campaignapi/launch` | `campaign:launch` |

标识符在 `GET` 请求中放查询参数，在 `POST` 请求中放 JSON 请求体。[CLI](/zh/api/cli#写入与发布命令) 把同样的操作封装成命令。

## 编写 Campaign 文件

候选文件是一份 Campaign 文件：一个 JSON 对象，装着整个活动的定义。每次写入都发送完整文件。

| 部分 | 内容 |
| --- | --- |
| `spec_version` | 字符串 `"1.0"`。 |
| `campaign` | 活动的 `name`。`campaign_id`、`owner` 和 `created_at` 由服务端填写。 |
| `eligibility` | 谁可以参与：`gates`、`kyc_tier`（`none`、`light` 或 `full`）和 `reputation`。 |
| `workflow` | 各个步骤 `nodes`，以及步骤之间的先后 `next`。 |
| `reward_policy` | 基础奖励 `base`、调整项 `factors` 和上限 `caps`。 |
| `quotas` | 活动的总预算 `budget_usd`，以及各步骤的限额 `per_node`。 |
| `data_contract` | `scarcity_strata` 和 `lineage`。 |
| `audit` | 一句话的 `intent`，以及 `versions`。 |

八个部分都必填。平台没有为这份文件单独发布 JSON Schema，`candidatevalidate` 的检查结果就是准绳，每个问题都带字段路径。

修改已有活动时，读回它的最新版本再编辑，做法见下一节。新建时可以从下面这份文件开始：一个参与者步骤，前面有准入检查，每条提交由人验收。名称、金额和数量只是示意，请填你自己的决定。`template_id` 从 `GET /api/v1/campaignapi/templates` 取得。

```json
{
  "spec_version": "1.0",
  "campaign": { "name": "退货分诊第一轮" },
  "eligibility": { "gates": [], "kyc_tier": "none", "reputation": {} },
  "workflow": {
    "nodes": [
      { "key": "gate", "node_key": "access_gate", "params": { "spec_version": 1 } },
      {
        "key": "collect",
        "node_key": "human_supply",
        "params": {
          "form_template_ref": { "template_id": "784210002001" },
          "task": { "totalCap": 200, "perUserQuota": 20 },
          "reward": { "asset_code": "USDC", "amount": 0.5 }
        }
      },
      { "key": "accept", "node_key": "data_accept", "params": { "acceptance": { "mode": "manual" } } }
    ],
    "next": [
      { "from": "gate", "to": "collect" },
      { "from": "collect", "to": "accept" },
      { "from": "accept", "to": "done" }
    ]
  },
  "reward_policy": {
    "base": { "asset_code": "USDC", "amount": 0.5 },
    "factors": [],
    "caps": {}
  },
  "quotas": { "per_node": {}, "budget_usd": 100 },
  "data_contract": { "scarcity_strata": [], "lineage": {} },
  "audit": {
    "intent": "收集退货案例的分诊结论。",
    "versions": []
  }
}
```

在 `collect` 步骤里，`task.totalCap` 是这一步向全体参与者收取的提交总数，`task.perUserQuota` 是一位参与者最多可以提交的条数。两者都把进行中的任务和等待验收的提交计算在内。提交被驳回或判为重复后不再计入，名额随之空出。`reward.amount` 是一条已接受提交的奖励。

在 `accept` 步骤里，`acceptance.mode` 设为 `manual` 后，每条提交都要等人验收。不写这一项，提交一到就自动接受。

`hb campaign init --template` 写出的文件有同样的部分和步骤，包括 `task` 和 `acceptance`；需要你决定的每个值都留成 `<DECIDE: …>` 占位符。

## 验证并规划候选文件

这两个操作只读取。把上面的文件存为私有工作目录里的 `candidate.json`，再生成新建活动所需的两个请求体：

```sh
python3 - <<'PY'
import json
from pathlib import Path

candidate = json.loads(Path('candidate.json').read_text())
Path('validate.json').write_text(json.dumps({'candidate': candidate}))
Path('plan.json').write_text(json.dumps({'mode': 'create', 'candidate': candidate}))
PY
```

要规划一次修改，就把 `HB_CAMPAIGN_ID` 设为这把 Key 可以编辑的活动，读取它的最新版本：

```sh
curl --fail-with-body --silent --show-error --get \
  'https://api.humanbased.ai/api/v1/campaignapi/version' \
  --header "Authorization: Bearer $HB_API_KEY" \
  --data-urlencode "campaign_id=$HB_CAMPAIGN_ID" \
  --output version.json
```

`data.version.version_number` 与 `data.version.spec_hash` 确定本次修改的原版本。`data.campaign_file` 是完整文件，`data.server_assigned_fields` 列出 `campaign` 下候选文件要去掉的字段。下面只示范修改名称：

```sh
python3 - <<'PY'
import json
from pathlib import Path

response = json.loads(Path('version.json').read_text())
assert response['success'] and response['data']['campaign_file']
data = response['data']
candidate = data['campaign_file']
for field in data.get('server_assigned_fields', []):
    candidate['campaign'].pop(field, None)
candidate['campaign']['name'] = '已复核的示例名称'
Path('validate.json').write_text(json.dumps({'candidate': candidate}))
Path('plan.json').write_text(json.dumps({
    'mode': 'update', 'campaign_id': data['campaign_id'],
    'candidate': candidate,
    'expected_version_number': data['version']['version_number'],
    'expected_spec_hash': data['version']['spec_hash'],
}))
PY
```

两种情况的请求体用同样的方式发送：

```sh
curl --fail-with-body --silent --show-error \
  'https://api.humanbased.ai/api/v1/campaignapi/candidatevalidate' \
  --header "Authorization: Bearer $HB_API_KEY" \
  --header 'Content-Type: application/json' --data-binary @validate.json \
  --output validation.json
curl --fail-with-body --silent --show-error \
  'https://api.humanbased.ai/api/v1/campaignapi/candidateplan' \
  --header "Authorization: Bearer $HB_API_KEY" \
  --header 'Content-Type: application/json' --data-binary @plan.json \
  --output plan-result.json
```

| 文件 | 查看 | 含义 |
| --- | --- | --- |
| `validation.json` | `data.valid` | 所有检查都通过时为 `true`。 |
| `validation.json` | `data.checks` | `schema`、`compile`、`dependencies` 各一项，带 `ran`、`passed` 和 `violations`。每条问题有 `path`、稳定的 `code` 和 `message`。 |
| `validation.json` | `data.candidate_hash` | 你发送的这份文件的标识。 |
| `plan-result.json` | `data.applicable` | 候选文件有效，且修改时原版本仍是草稿，才为 `true`。 |
| `plan-result.json` | `data.base` | 计划对照的版本；新建时为 `null`。 |
| `plan-result.json` | `data.changes`、`data.change_count` | 每项更改的 `op`（`add`、`remove` 或 `replace`）、`path`，以及 `before` 或 `after`。 |
| `plan-result.json` | `data.findings` | 同一份验证结果，随这次计划再给一遍。 |

`applicable: false` 的计划还不能应用。原版本或哈希变化后，规划会以 `BASE_VERSION_STALE` 被拒绝：重新读取版本，再复核新的候选文件和计划。

## 创建活动

负责人批准这份文件后，在新建计划的请求体里加上 `request_key`，发送给 `candidateapply`。请求键由你自己选：一个稳定的字符串，最长 200 个字符，只代表这一次写入。

```sh
python3 - <<'PY'
import json
from pathlib import Path

body = json.loads(Path('plan.json').read_text())
assert body['mode'] == 'create'
body['request_key'] = 'create-returns-triage-2026-10-10'
Path('apply.json').write_text(json.dumps(body))
PY
curl --fail-with-body --silent --show-error \
  'https://api.humanbased.ai/api/v1/campaignapi/candidateapply' \
  --header "Authorization: Bearer $HB_API_KEY" \
  --header 'Content-Type: application/json' --data-binary @apply.json \
  --output receipt.json
```

新建请求体只有 `mode`、`candidate` 和 `request_key`，其中没有 `campaign_id`，也没有原版本字段。

一次新建在同一个 `request_key` 下依次完成这些步骤：

| 步骤 | 结果 |
| --- | --- |
| 分配 | 在 Key 所属组织建立一个草稿活动，归创建这把 Key 的人所有，预算取自 `quotas.budget_usd`。 |
| 开通 | 这个活动的 Pipeline，此时只有一个准入检查步骤。发布时，平台才把草稿里的步骤写进去。 |
| 保存 | 把文件存为第 1 版，状态为 `draft`。 |
| 授权 | 把新活动加入 Key 的 `managed_campaign_ids`，同一把 Key 之后可以修改并发布它。`granted_to_key` 说明这一步是否完成。 |

响应是一份回执。示例中的哈希做了缩写：

```json
{
  "data": {
    "replayed": false,
    "action_id": "784210008001",
    "action": "campaignfile.apply",
    "status": "succeeded",
    "request_key": "create-returns-triage-2026-10-10",
    "request_hash": "5d0c…a917",
    "campaign_id": "784210001001",
    "result": {
      "campaign_id": "784210001001",
      "external_pipeline_id": "784210011001",
      "file_version_id": "784210007001",
      "version_number": 1,
      "persisted_file_hash": "8a41…c2f0",
      "state": "draft",
      "granted_to_key": true
    }
  },
  "success": true,
  "errorCode": 0,
  "errorMessage": "SUCCESS"
}
```

保存 `data.campaign_id`，之后每个操作都要用它。`data.result.granted_to_key` 为 `false` 时，活动已经建立，但这把 Key 没有拿到它；请在 Developer 门户修改，或换一把名单中包含它的 Key。要确认创建结果，读取 `GET /api/v1/campaignapi/campaign?campaign_id=<campaign_id>`，`data.campaign.status` 应为 `draft`。

`persisted_file_hash` 与验证时的 `candidate_hash` 不同，因为保存的文件多了服务端填写的字段。

## 把更改应用到草稿

在修改计划的请求体里加上 `request_key`，发送给同一个操作。修改请求体包含 `mode`、`campaign_id`、`candidate`、`expected_version_number`、`expected_spec_hash` 和 `request_key`。

修改成功后，候选文件成为这个活动最新的草稿版本，其他内容保持原样。回执结构与上例相同：`data.result.version_number` 是新版本号，`data.result` 中没有 `granted_to_key`。

写入的那一刻，服务端会再比对一次版本号和哈希。别人先保存时，请求以 `409` 和 `BASE_VERSION_STALE` 被拒绝，什么都没有写入：重新读取版本、重做候选文件，再规划一次。

## 读取回执并恢复

`candidateapply` 返回 `200` 时，一定是 `status: succeeded` 的回执。其他结果都以错误响应返回；另外三种状态通过下面的回执查询看到。

| 回执字段 | 含义 |
| --- | --- |
| `replayed` | 为 `true` 时，这份响应描述的是同一 `request_key` 下此前的尝试，本次没有新的写入。查询回执时它总是 `true`，与那次尝试的 `status` 无关。 |
| `action_id` | 服务端给这次写入的标识。 |
| `action` | 新建和修改都是 `campaignfile.apply`。 |
| `status` | `succeeded`、`in_progress`、`failed` 或 `uncertain`。 |
| `request_key`、`request_hash` | 你的请求键，以及服务端对整个请求计算的哈希。 |
| `campaign_id`、`result` | 被写入的活动，以及这次写入的产出。 |
| `steps`、`uncertain_reason` | 写入尚未了结或已失败时出现：进行到哪一步，以及结果为何未知。 |
| `in_progress_since` | `status` 为 `in_progress` 时出现：这次写入最近一次有进展的时间。 |

`request_key` 让重试变得安全。把请求体和它的键保存在一起，再按重试的返回决定：

| 你看到 | 发生了什么 | 你现在做什么 |
| --- | --- | --- |
| 完全没有响应 | 写入可能成功，也可能没有。 | 读取 `GET /api/v1/campaignapi/actionreceipt?request_key=<your-key>`，或用同一个键重发相同的请求体。 |
| 回执 `replayed: true` 且 `status: succeeded` | 首次尝试已成功，这次没有重复写入。 | 以这份回执为准。 |
| `409` `ACTION_IN_FLIGHT`，或回执 `status: in_progress` | 仍有请求在用这个键写入。 | 稍等后读取回执。`in_progress_since` 过去十分钟仍是 `in_progress` 时，用同一个键重发相同的请求体。 |
| `409` `REQUEST_KEY_SPENT`，或回执 `status: failed` | 此前的尝试失败，什么都没有写入。 | 处理原因后换新键再发。 |
| `409` `REQUEST_KEY_REUSED` | 这个键已经用于另一个请求。 | 重试原请求就发原请求体；新请求换新键。 |
| `409` `ACTION_UNCERTAIN`，或回执 `status: uncertain` | 写入已经尝试，结果未知。 | 停下。由人对照回执的 `steps` 和活动当前版本后再决定；新建时，`steps` 会写明已经分配的活动。换新键等于第二次写入。 |

`actionreceipt` 只接受 `request_key` 或 `action_id` 其中一个。按 `request_key` 查询时，默认查 Campaign 文件的应用；查模板的应用要加 `action=template.apply`。发布的回执要按下一节的方式找回。

## 发布活动

发布把草稿活动变为 `live`，并从组织余额中锁定它的预算。这件事由三方动作完成：Key 提出提案，人确认这份提案，Key 再执行。

提出提案前先核对四项：

| 前提 | 在哪里决定 |
| --- | --- |
| Key 有 `campaign:launch`，且活动在它的 `managed_campaign_ids` 中。 | Key 的授权，用 `whoami` 查看。 |
| 最新存储的版本有工作进入的步骤，每个参与者步骤都有表单。 | Campaign 文件。 |
| 活动设有预算，组织可用余额足以覆盖。 | 提案的 `amount_usd`，以及组织的 Billing 页面。 |
| 预算在 Key 支出上限的剩余额度之内。 | 创建 Key 时设定的上限。 |

### 1. 提出提案

```sh
curl --fail-with-body --silent --show-error \
  'https://api.humanbased.ai/api/v1/campaignapi/launchpropose' \
  --header "Authorization: Bearer $HB_API_KEY" \
  --header 'Content-Type: application/json' \
  --data "{\"campaign_id\": \"$HB_CAMPAIGN_ID\"}" \
  --output proposal.json
```

提案只写入一条记录，资金和活动都保持原样。请保存响应，`proposal_id` 只在这里返回。

```json
{
  "data": {
    "proposal_id": "784210010001",
    "campaign_id": "784210001001",
    "operation": "launch",
    "state": "proposed",
    "amount_usd": "100.0000",
    "base_version_number": 1,
    "base_spec_hash": "8a41…c2f0",
    "proposal_hash": "clp1_77be…09d3",
    "expires_at": "2026-10-10T10:30:00+00:00",
    "confirmed_by": null,
    "confirmed_at": null,
    "executed_action_id": null
  },
  "success": true,
  "errorCode": 0,
  "errorMessage": "SUCCESS"
}
```

`amount_usd` 是这次发布将锁定的预算，以保留四位小数的字符串给出。`base_version_number` 和 `base_spec_hash` 指明它将运行的草稿。`proposal_hash` 把这些内容绑在一起。`expires_at` 是提出提案后一小时，确认和执行都要在这之前完成。

一把 Key 对一个活动只保留一份未了结的提案。第一份执行完或过期之前，对同一活动再次调用 `launchpropose` 会以 `PROPOSAL_OPEN_ALREADY` 被拒绝。

### 2. 由人确认

把负责这项决定的人引到 Developer 门户的确认页面：

```text
https://developer.humanbased.ai/campaigns/<campaign_id>/launch/confirm?proposal=<proposal_id>
```

对方以该活动的编辑者或所有者身份登录。页面显示服务端保存的活动、金额、草稿版本、过期时间和提案哈希。选择 **Confirm this launch**（确认这次发布）只记录对这个哈希的同意，发布留到下一步执行。

用提出提案的那把 Key 读取 `GET /api/v1/campaignapi/launchproposal?proposal_id=<proposal_id>`。`data.state` 变为 `confirmed` 后继续。

### 3. 执行

```sh
python3 - <<'PY'
import json
from pathlib import Path

proposal = json.loads(Path('proposal.json').read_text())['data']
Path('launch.json').write_text(json.dumps({
    'proposal_id': proposal['proposal_id'],
    'request_key': 'launch-returns-triage-2026-10-10',
}))
PY
curl --fail-with-body --silent --show-error \
  'https://api.humanbased.ai/api/v1/campaignapi/launch' \
  --header "Authorization: Bearer $HB_API_KEY" \
  --header 'Content-Type: application/json' --data-binary @launch.json \
  --output launch-receipt.json
```

请求体包含 `proposal_id` 和你选定的 `request_key`，并且只能由提出提案的那把 Key 发送。资金变动之前，服务端按活动此刻的状态重新计算提案哈希。预算、草稿或付款条款在确认之后改过，就成了另一个动作，请求会以 `PROPOSAL_STALE` 被拒绝。

发布成功时返回 `action: campaign.launch`、`status: succeeded` 的回执。`data.result` 中有 `campaign_id`、活动的新 `status`、`proposal_id` 和 `confirmed_by`。

### 确认结果或找回发布

| 你看到 | 发生了什么 | 你现在做什么 |
| --- | --- | --- |
| 回执 `status: succeeded` | 活动已发布，预算已锁定。 | 用 `GET /api/v1/campaignapi/campaign` 确认 `data.campaign.status` 为 `live`。提案的 `state` 为 `executed`。 |
| 完全没有响应 | 发布可能已执行，也可能没有。 | 用同一个 `request_key` 重发相同的请求体。服务端返回首次的回执，发布只发生一次。`actionreceipt?request_key=` 查不到发布。 |
| `PROPOSAL_NOT_CONFIRMED` | 还没有人确认。 | 请对方确认，再发送同一请求体。 |
| `PROPOSAL_EXPIRED` 或 `PROPOSAL_REVOKED` | 这份提案已经关闭。 | 重新提出提案，并请人确认新提案。 |
| `PROPOSAL_STALE` | 活动在确认之后发生了变化。 | 等这份提案的 `expires_at` 过去后重新提出；在那之前，新提案会以 `PROPOSAL_OPEN_ALREADY` 被拒绝。 |
| `SPEND_CAP_EXCEEDED` | 这次发布会超出 Key 的支出上限，资金没有变动。 | 请人在 Developer 门户发布，或用一把上限有余量的 Key 重新提出提案。 |
| `CAMPAIGN_GRAPH_MALFORMED` | 草稿无法按存储的内容运行：Pipeline 拒绝了其中的步骤，或它引用的表单模板不存在或为空。资金没有变动。 | 提交更正后的草稿，等这份提案的 `expires_at` 过去后重新提出提案。 |
| `503` `CAMPAIGN_GRAPH_UNAVAILABLE` | Pipeline 没有接收这份草稿。资金没有变动，活动也没有变为 `live`。 | 换一个新的 `request_key` 再次执行发布。这份提案仍处于已确认状态。 |
| `PROPOSAL_ALREADY_EXECUTED` | 这份提案已经执行，或正在执行。 | 读取提案和活动的状态，保持现状。 |
| `409` `ACTION_UNCERTAIN` | 发布已经尝试，结果未知，预算可能已被锁定。 | 停下。读取 `launchproposal`：`executed` 表示发布已完成，`executing` 表示尚未了结。由人核对活动状态和组织的 Billing 页面，再带上 `proposal_id` 联系 [hello@humanbased.ai](mailto:hello@humanbased.ai)。换新键等于第二次发布。 |

## 错误

拒绝响应的 `success` 为 `false`，`data` 为 `null`，`errorCode` 等于 HTTP 状态，并带有 `errorCodeName`。程序按 `errorCodeName` 分支。`errorMessage` 通常是给人读的文字，措辞可能调整；`CANDIDATE_INVALID` 的 `errorMessage` 则是一个对象，带有校验结果：

```json
{
  "data": null,
  "success": false,
  "errorCode": 400,
  "errorCodeName": "CANDIDATE_INVALID",
  "errorMessage": {
    "error": "CANDIDATE_INVALID",
    "message": "the candidate did not pass validation, so nothing was applied",
    "findings": {
      "valid": false,
      "checks": [
        {"check": "schema", "ran": true, "passed": false, "violations": [{"path": "…", "code": "…", "message": "…"}]}
      ],
      "candidate_hash": "…"
    }
  }
}
```

`findings` 就是 `candidatevalidate` 在 `data` 中返回的对象，示例做了删节。打印 `errorMessage` 前，先判断它是不是字符串。

| `errorCodeName` | 发生了什么 | 你现在做什么 |
| --- | --- | --- |
| `KEY_CAPABILITY_MISSING` | Key 缺少这个操作需要的权限。 | 创建带有该权限的 Key。 |
| `KEY_HAS_NO_CAMPAIGN_ALLOWLIST` | Key 可以读取组织的活动，但名单里没有它可以修改的活动。 | 创建一把带写入权限的替代 Key。 |
| `KEY_HAS_NO_ACCOUNTABLE_PERSON` | Key 没有记录创建它的人，它建立的活动会没有所有者。 | 创建一把替代 Key。 |
| `CAMPAIGN_NOT_FOUND` | 活动不存在，或这把 Key 看不到它。 | 对照 `campaigns` 列表和 `managed_campaign_ids` 核对 ID。 |
| `CANDIDATE_MALFORMED` | 候选文件不是 Campaign 文件结构的 JSON 对象。 | 修正文件结构。 |
| `CANDIDATE_TOO_LARGE` | 候选文件超过规模限制：1,048,576 字节、20,000 个 JSON 值、64 层嵌套、256 个工作流节点或 1,024 条连线。 | 精简文件。 |
| `CANDIDATE_INVALID` | 候选文件经过检查但未通过。什么都没有应用，`request_key` 仍未使用。 | 读取 `errorMessage.findings`，逐条修正后再发送。 |
| `EXPECTED_VERSION_REQUIRED`、`EXPECTED_SPEC_HASH_REQUIRED` | 修改请求没有指明原版本。 | 把读到的版本号和哈希都带上。 |
| `EXPECTED_BASE_ON_CREATE` | 新建请求带了 `campaign_id` 或原版本字段。 | 去掉这些字段。 |
| `REQUEST_KEY_REQUIRED` | 写入请求没有 `request_key`，或键是空白。 | 选定一个键再发送。 |
| `CAMPAIGN_HAS_NO_VERSION` | 修改请求指向的活动还没有可对照的版本。 | 规划前先读取该活动的版本列表。 |
| `BASE_VERSION_STALE` | 原版本在你读取之后发生了变化。 | 重新读取版本、重做候选文件，再规划一次。 |
| `CONTENT_IS_HISTORICAL` | 这份内容与某个较早的版本完全相同，而当前已有更新的版本。什么都没有改变。 | 发送一份与那个旧版本有差别的文件。 |
| `PLATFORM_NOT_CONFIGURED` | 平台缺少新建所需的配置，什么都没有创建。 | 联系 [hello@humanbased.ai](mailto:hello@humanbased.ai)；重试没有帮助。 |
| `DEVELOPER_SESSION_REQUIRED` | 用 Key 调用了 `launchconfirm`。 | 请人在 Developer 门户确认。 |
| `PROPOSAL_NOT_FOUND` | 没有这份提案，或它由另一把 Key 提出。 | 使用提出提案的那把 Key 和它收到的 `proposal_id`。 |
| `PROPOSAL_HASH_MISMATCH` | 确认时发送的哈希与保存的提案不一致。 | 重新读取提案，按它显示的哈希确认。 |
| `SPEND_CAP_UNAVAILABLE` | Key 可以发布，但它的支出上限没有读到，发布没有执行。 | 再发送一次同样的请求；持续出现时创建替代 Key。 |
| `ACTION_NOT_FOUND` | 这把 Key 名下没有与该 `action_id` 或 `request_key` 对应的回执。 | 核对标识，以及它所属的 `action`。 |
| `ACTION_LOOKUP_REQUIRED` | 查询回执时没有提供，或同时提供了 `action_id` 和 `request_key`。 | 只发送其中一个。 |
| `CAMPAIGN_GRAPH_NO_WORK_ORIGIN` | 流程里没有让工作进入的任务或导入节点。 | 添加一个，保存新版本后再发布。 |
| `CAMPAIGN_ID_REQUIRED` | 该操作针对一个活动，但请求没有指明。 | 发送 `campaign_id`。 |
| `CAMPAIGN_ROLE_INSUFFICIENT` | 你在该活动上的角色低于该操作的要求。 | 请活动所有者授予可执行该操作的角色。 |
| `CAMPAIGN_VERSION_NOT_FOUND` | 该活动没有这个版本。 | 先读取 `versions`，使用列出的版本。 |
| `CLI_SESSION_NOT_PERMITTED` | 已登录的 CLI 不能执行该操作。 | 改用组织 API Key；重新登录也不会改变结果。 |
| `CURSOR_INVALID` | 这个 cursor 不是该接口签发的。 | 不带 cursor 重新开始，再沿用返回的 cursor。 |
| `KEY_PRINCIPAL_INCOMPLETE` | 网关转发的 Key 身份不完整。这是平台故障，不是 Key 有误。 | 重试；持续出现时联系 [hello@humanbased.ai](mailto:hello@humanbased.ai)。 |
| `LIMIT_OUT_OF_RANGE` | `limit` 超出该接口接受的范围。 | 使用范围内的值，例如 `campaigns` 为 1–200。 |
| `MODE_UNSUPPORTED` | 该操作不提供这种模式。 | 使用该操作列出的模式。 |
| `OBSERVATION_OWNER_DISAGREES` | 该活动指向的流程在其所属服务中不存在。这是配置故障，不是请求有误。 | 带上 Campaign ID 联系支持。 |
| `OBSERVATION_OWNER_UNAVAILABLE` | 负责这些计数的服务没有响应，请求并未被拒绝。 | 重新读取。 |
| `OBSERVATION_REFUSED_BY_OWNER` | 负责这些计数的服务拒绝了请求，没有说明原因。 | 带上 Campaign ID 和时间联系支持。 |
| `OBSERVATION_WINDOW_INVALID` | 统计窗口或流程版本被拒绝；`errorMessage` 会引用该值。 | 修正该值后重新读取。 |
| `ORG_ID_REQUIRED` | 调用方没有对应的组织，因此没有可读取的范围。 | 使用属于某个组织的 Key 或登录身份。 |
| `ORG_MISMATCH` | 该 CLI 会话登录的组织与请求指定的组织不同。 | 登录到该组织，或去掉请求中显式指定的组织。 |
| `PIPELINE_VERSION_NOT_FOUND` | 该流程版本不属于这个活动的流程。 | 使用该活动的版本。 |
| `PORTAL_SESSION_NOT_PERMITTED` | 通过此 API 的发布绑定到提出它的组织 API Key 或已登录的 CLI，Developer 门户的登录会话两者都不是。 | 在 Developer 门户的活动页面发布，或用同一把 Key 或同一个 CLI 会话提出并执行发布。 |
| `REQUEST_MALFORMED` | 请求无法解析：缺少字段、类型错误或超出范围。 | 阅读 `errorMessage`，修正字段后重新发送。 |
| `TEMPLATE_INVALID` | 模板经过检查，其中有错误。 | 按每条带路径的结果修正（与 `templatevalidate` 返回的相同），再重新发送。 |
| `TEMPLATE_KIND_IMMUTABLE` | 模板类型在创建时已固定。 | 改为新建一个模板。 |
| `TEMPLATE_MALFORMED` | 请求体根本不是模板文档。 | 修正请求结构。 |
| `TEMPLATE_NOT_EDITABLE` | 本组织可以读取该模板，但不能修改。 | 先采用到你的组织，再编辑副本。 |
| `TEMPLATE_NOT_FOUND` | 没有该模板，或本组织无权使用。 | 核对模板 ID。 |
| `TEMPLATE_STALE` | 读取之后模板已被修改。 | 重新读取，把你的修改并入最新内容，并带上新的哈希发送。 |
| `ACTION_IN_FLIGHT`、`ACTION_UNCERTAIN`、`REQUEST_KEY_REUSED`、`REQUEST_KEY_SPENT` | 用 `request_key` 的写入仍在进行、已经失败、结果未知，或这个键已用于另一个请求。 | 按[读取回执并恢复](#读取回执并恢复)中的表处理。遇到 `ACTION_UNCERTAIN` 时先停下，由人核对后再决定是否重发。 |
| `PROPOSAL_NOT_CONFIRMED`、`PROPOSAL_EXPIRED`、`PROPOSAL_REVOKED`、`PROPOSAL_STALE`、`PROPOSAL_OPEN_ALREADY`、`PROPOSAL_ALREADY_EXECUTED`、`SPEND_CAP_EXCEEDED`、`CAMPAIGN_GRAPH_MALFORMED`、`CAMPAIGN_GRAPH_UNAVAILABLE` | 发布提案或执行发布被拒绝。 | 按[发布活动](#发布活动)中的拒绝原因表处理。 |

请求到达本接口之前被拒绝时，例如 Key 缺失或无效的 `401`，响应可能没有 `errorCodeName`，程序要为这种情况保留默认分支。网关返回的 `429` `Too many requests` 也是如此：按[调用频率限制](/zh/api/api-fields#rate-limits)等待 `Retry-After` 给出的秒数。

在 Developer 门户发布的做法见[测试与发布](/zh/tasks/launch)。
