# Raise App API

版本 `1.2.0`。[下载 OpenAPI 文件（YAML）](/openapi/app-v1.yaml)

## 概述

给已安装的应用用的接口（主版本 v1）：客户中心、站点访客、经营记账、审计链、当前人员的权限与部门、成员、安装了本应用的企业、其他应用的服务、本应用的开放事件、记录链接。调用根地址是 `https://api.{域名}/app/v1`（`https://app.{域名}/app/v1` 同样可用）。
本文件里的路径已经去掉了这个前缀：`GET /access` 就是 `GET https://api.{域名}/app/v1/access`。它与 `/open/v1`（`open-v1.openapi.yaml`）是平台的两个接口面：`/open/v1` 给组织授权的集成读写组织数据，`/app/v1` 给已安装的应用调用平台能力；同一个生成器 `tools/sdkgen` 生成两套 SDK 客户端。

### 令牌与范围

* **令牌** 应用自己的租户令牌（`POST /open/v1/oauth/token`，`grant_type=client_credentials`，见 open-v1 文档），或使用这个应用页面的人的页面会话。客户令牌不能调用这里（403 `forbidden`）。
* **范围** 每个操作需要一个 `app.…` 范围（操作的 `x-raise-scope`，描述里也写着）。范围必须是应用在清单（manifest）`uses` 里声明过的：令牌里带着它们，页面会话也按清单给。没声明的范围是 403 `scope_required`。
* **租户** 永远是令牌的租户。响应头 `X-K3-Tenant` 写出令牌实际指向的组织。
* 有些操作只收应用自己的令牌（没有人在背后），页面会话是 403 `forbidden`，操作描述里会写明。

### 约定

与 `/open/v1` 相同的约定：

* **错误** 状态 ≥ 400 的正文都是 `{"error":{"code","message","requestId","docUrl","details"}}`。`code` 稳定，用它分支；`message` 给人看。`details` 没内容时是 `{}`。`requestId` 与响应头 `X-Request-Id` 相同，报问题时带上。
* **幂等** 每个 `POST` 收 `Idempotency-Key`（1–255 字符）。范围是应用 + 组织。保存 24 小时。同键同方法同路径同正文 → 重放第一次的回答，并带 `Idempotent-Replayed: true`。同键不同正文 → 409 `idempotency_conflict`。第一次还在处理 → 409 `idempotency_in_progress`。状态 ≥ 500 的回答不保存。SDK 会在 POST 上自动生成 uuid v4。业务上的去重另有自己的键（记账的 `sourceApp + sourceRef + kind`、接触的 `key`、事件的 `key`、服务调用的 `idempotencyKey`），它们在正文里，与这个头无关。
* **限流** 认证过的调用都带 `X-RateLimit-Limit`、`X-RateLimit-Remaining`、`X-RateLimit-Reset`。429 另带 `Retry-After`（秒）。`Raise-Environment: test|live` 说明这把令牌属于哪边。
* **时间** RFC 3339 UTC。**金额** 整数最小货币单位。JSON 字段 camelCase（`GET /access` 的正文例外，见该操作）。
* 正文里的未知字段会被拒绝（422）。

### 实时文档的同步连接（不在这个文件的路径里）

`realtime` 接口管文档的建立、服务端改动和读取；人在页面里的协同编辑走 WebSocket，由平台在**应用自己的域名**上提供（经应用网关，不在 `api.{域名}`）：`GET /app/v1/realtime/sync?codec=&kind=`（一条连接服务这个编码、这个种类的所有文档）和 `GET /app/v1/realtime/docs/{id}/sync`（同一条连接，编码与种类从文档读）。网关先检查应用令牌、安装、Origin 和连接数，再交给平台；页面一侧用 `@21stware/raise-design` 的实时文档组件或应用 SDK（`packages/app-sdk/go/realtime`）连接，不要自己拼协议。每份文档的权限是调用者在它所属资源上的级别（`resources` 接口）。

清单 `realtime.docs[]` 的每个种类还可以写三项。`admit`：本应用的一个权限点（`permissions[].id`），开同步连接的人必须持有，否则 403；不写则网关放进来的人都可以。`topics`：本应用声明的资源类型（`resources[].type`）；同步连接可以订阅 `<类型>:<id>`，平台在调用者对这个资源至少有查看级别时放行；`<类型>:*` 是这个类型的列表（有一个被新建、改名、归档或分享给人），持有 `admit` 权限点的人可以订阅（没写 `admit` 的种类没有列表主题）；其余主题一律拒绝；应用用 `POST /realtime/topics/{topic}/publish` 告诉订阅者有变化。`store`：这个种类的文档放在哪个命名空间，默认本应用 id；写别的应用 id 就是与它共用文档，平台只在对方已安装的清单里也声明了同一种类、同一 `store`、同一编码（双方都同意），且两个应用属于同一开发者组织时接受（提交版本时检查一次，每次使用时再检查）。共用命名空间不扩大权限：每个应用的人仍只按它自己声明的资源类型判断。English: each kind may also set `admit` (a permission point a person must hold to open a sync connection), `topics` (declared resource types whose `<type>:<id>` topics a client may subscribe to at view level or above, and `<type>:*`, the list of that type, for whoever holds the admit point) and `store` (the namespace, default the app id; another app's id shares that app's documents only when both declare the same kind, store and codec and both apps belong to one developer organisation).

用 Go 模块 `github.com/21stware/raise-go`（`client.App.…`）或 npm 包 `@tokiter/raise-sdk`（`raise.app.…`），不要自己拼重试。SDK 的基地址是 `/open/v1` 根，应用接口的根由它换成同级的 `/app/v1`。

## 地址

- `/app/v1`

## 认证方式

| 名称 | 类型 | 说明 |
|---|---|---|
| `bearer` | http bearer | 应用自己的租户令牌（POST /open/v1/oauth/token 换到），或使用这个应用页面的人的页面会话。Authorization: Bearer &lt;token> |

## customers

客户中心：按手机号等身份找到或建立客户、读遮蔽后的资料、记录一次接触

### `POST /customers/resolve`

按身份（手机号等）找到或建立客户，返回主客户的 id

需要范围 `app.customers:resolve`。English: finds the customer that owns one of the identities, or creates one, and answers the primary customer's id and whether this call created it.

`identities` 是这个人的身份（`kind`、`value`，可带 `scope`、`verified`）。`name`、`owner`、`ownerUnit` 在新建客户时使用。`channel` 与 `sourceRef` 是这位客户第一次从哪里来（来源应用是调用的应用，不能自己填）。`public` 标记匿名访客发起的调用。`anonymous` 要求新建一位没有任何身份的客户，此时 `identities` 必须为空，也不做任何匹配，所以一定新建。

查询 `dryRun=1`：只回答“会得到什么”，什么也不写（不建客户）；新建的客户 id 不会被任何别的调用认识。

数据范围：页面会话里的人找到的客户如果不在他「查看客户」（customer.read）的数据范围内，回答只有 `id` 与 `created`，没有 `name`；应用自己的调用（企业令牌）不受影响。

正文里的字段不合规是 422。

- 认证：`bearer`
- 范围：`app.customers:resolve`
- operationId：`resolveCustomer`

**参数**

| 名称 | 位置 | 类型 | 必填 | 说明 |
|---|---|---|---|---|
| `dryRun` | query | string |  | 传 1 时只预演，不写入<br>可选值：`1` |
| `Idempotency-Key` | header | string |  | 每个 POST 都收。SDK 未传入时会生成 uuid v4。24 小时内同键同正文重放第一次的回答<br>最短 1，最长 255 |

**请求正文（必填）**（`application/json`）

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `identities` | CustomerIdentity[] | 是 | 这个人的身份。anonymous 为 true 时必须为空 |
| `identities[].kind` | string | 是 | 身份的种类 |
| `identities[].scope` | string |  | 身份所在的范围。可省略 |
| `identities[].value` | string | 是 | 身份的值 |
| `identities[].verified` | boolean |  | 这个身份已被证明 |
| `name` | string |  | 新建客户时使用的姓名 |
| `channel` | string |  | 第一次来源的渠道 |
| `sourceRef` | string |  | 第一次来源：指向那次活动的 k3:// 引用 |
| `owner` | string |  | 新建客户时的负责人 |
| `ownerUnit` | string |  | 新建客户时的归属部门 |
| `public` | boolean |  | 标记这是匿名访客发起的调用 |
| `anonymous` | boolean |  | 要求新建一位没有任何身份的客户；此时 identities 必须为空，不做匹配，一定新建 |

**响应**

| 状态 | 说明 |
|---|---|
| 200 | 主客户。`created` 说明这次调用是否新建了它 |
| 401 | 见 [Unauthorized](#resp-unauthorized) |
| 403 | 见 [Forbidden](#resp-forbidden) |
| 413 | `payload_too_large`：正文太大。见 [PayloadTooLarge](#resp-payloadtoolarge) |
| 422 | 字段或参数不被接受（正文读不出来、有未知字段、缺必填、取值越界）。见 [Unprocessable](#resp-unprocessable) |
| 429 | 见 [RateLimited](#resp-ratelimited) |
| 500 | 见 [Internal](#resp-internal) |
| 501 | `capability_off`：这个部署（或这个宿主）不提供这项能力，正文的 `capability` 是它的范围。见 [CapabilityOff](#resp-capabilityoff) |
| 503 | `unavailable`：暂时不可用（这个部署不提供的能力是 501 `capability_off`）。见 [Unavailable](#resp-unavailable) |

**200 响应正文**（`application/json`）

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `id` | string | 是 | 主客户的 id |
| `created` | boolean | 是 | 这次调用是否新建了它 |
| `name` | string |  | 客户中心保存的姓名：新建时或原来没有姓名时是这次给的，否则是原有的 |

### `GET /customers/{id}`

读一位客户的遮蔽资料

需要范围 `app.customers:read`。English: the customer's profile with phone and e-mail masked.

`id` 是主客户的 id。若这个 id 的客户已被合并，返回的 `id` 是合并后的主客户（与你问的不同），`mergedFrom` 列出并入它的客户。客户中心没有这个 id 是 404 `not_found`。

数据范围：应用自己的调用（企业令牌）可读本企业任何客户；页面会话里的人只能读他在客户应用里「查看客户」（customer.read）数据范围内的客户，范围外的与不存在一样返回 404。

- 认证：`bearer`
- 范围：`app.customers:read`
- operationId：`getCustomerProfile`

**参数**

| 名称 | 位置 | 类型 | 必填 | 说明 |
|---|---|---|---|---|
| `id` | path | string | 是 | 客户 id |

**响应**

| 状态 | 说明 |
|---|---|
| 200 | 客户资料 |
| 401 | 见 [Unauthorized](#resp-unauthorized) |
| 403 | 见 [Forbidden](#resp-forbidden) |
| 404 | `not_found`：对应的记录不存在（客户中心没有这个客户，访客、服务或所指的东西不存在）。见 [NotFound](#resp-notfound) |
| 429 | 见 [RateLimited](#resp-ratelimited) |
| 500 | 见 [Internal](#resp-internal) |
| 501 | `capability_off`：这个部署（或这个宿主）不提供这项能力，正文的 `capability` 是它的范围。见 [CapabilityOff](#resp-capabilityoff) |
| 503 | `unavailable`：暂时不可用（这个部署不提供的能力是 501 `capability_off`）。见 [Unavailable](#resp-unavailable) |

**200 响应正文**（`application/json`）

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `id` | string | 是 | 主客户的 id；被合并过时与所问的 id 不同 |
| `name` | string | 是 |  |
| `stage` | string | 是 | 生效的阶段：lead、customer 或 dormant（长期没有接触的客户是 dormant） |
| `owner` | string |  | 负责人（员工 subject） |
| `ownerUnit` | string |  |  |
| `sourceApp` | string |  | 第一次带来这位客户的应用 |
| `sourceChannel` | string |  |  |
| `phone` | string |  | 打码后的第一个电话 |
| `tags` | string[] |  | 标签 id |
| `lastTouchAt` | string(date-time) | 是 |  |
| `createdAt` | string(date-time) | 是 |  |
| `wecomBasis` | string |  | 企业微信外部联系人与客户的关联依据，取最弱的一个：none、phone 或 unionid；没有外部联系人时为空 |
| `wecomPending` | boolean |  | 即 wecomBasis 为 none（待关联） |
| `sourceRef` | string |  |  |
| `dealtAt` | string(date-time) |  |  |
| `identities` | CustomerIdentityView[] | 是 |  |
| `identities[].kind` | string | 是 |  |
| `identities[].scope` | string |  |  |
| `identities[].masked` | string | 是 | 打码后的值 |
| `identities[].value` | string |  | 明文。只在请求了明文时出现 |
| `identities[].verified` | boolean | 是 |  |
| `identities[].firstSeen` | string(date-time) | 是 |  |
| `identities[].source` | string |  |  |
| `links` | string[] | 是 |  |
| `mergedFrom` | string[] |  | 并入它的客户 |
| `touchCount` | integer | 是 | 这位客户及并入它的客户的接触总数 |

### `POST /customers/touch`

给客户的时间线记一次接触（同一个 key 只记一次）

需要范围 `app.customers:resolve`。English: appends one entry to the customer's timeline, once per key.

`kind` 写成 `{应用}.{动作}`（例如 `coupons.redeem`），`ref` 是指向所发生之事的 `k3://` 引用，`key` 是幂等键（租户内唯一，用来源事件派生，例如 `coupons:claim:cl_17`），`at` 省略为现在。同一个 `key` 再来，`written` 为 false。

- 认证：`bearer`
- 范围：`app.customers:resolve`
- operationId：`touchCustomer`

**参数**

| 名称 | 位置 | 类型 | 必填 | 说明 |
|---|---|---|---|---|
| `Idempotency-Key` | header | string |  | 每个 POST 都收。SDK 未传入时会生成 uuid v4。24 小时内同键同正文重放第一次的回答<br>最短 1，最长 255 |

**请求正文（必填）**（`application/json`）

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `customer` | string | 是 | 客户 id |
| `key` | string | 是 | 幂等键，租户内唯一，用来源事件派生 |
| `kind` | string | 是 | {应用}.{动作}，例如 coupons.redeem |
| `ref` | string |  | 指向所发生之事的 k3:// 引用 |
| `attrs` | object |  |  |
| `at` | string(date-time) |  | 省略为现在 |

**响应**

| 状态 | 说明 |
|---|---|
| 200 | `written` 为 true 表示这次写入了；false 表示这个 key 以前就有 |
| 401 | 见 [Unauthorized](#resp-unauthorized) |
| 403 | 见 [Forbidden](#resp-forbidden) |
| 404 | `not_found`：对应的记录不存在（客户中心没有这个客户，访客、服务或所指的东西不存在）。见 [NotFound](#resp-notfound) |
| 413 | `payload_too_large`：正文太大。见 [PayloadTooLarge](#resp-payloadtoolarge) |
| 422 | 字段或参数不被接受（正文读不出来、有未知字段、缺必填、取值越界）。见 [Unprocessable](#resp-unprocessable) |
| 429 | 见 [RateLimited](#resp-ratelimited) |
| 500 | 见 [Internal](#resp-internal) |
| 501 | `capability_off`：这个部署（或这个宿主）不提供这项能力，正文的 `capability` 是它的范围。见 [CapabilityOff](#resp-capabilityoff) |
| 503 | `unavailable`：暂时不可用（这个部署不提供的能力是 501 `capability_off`）。见 [Unavailable](#resp-unavailable) |

**200 响应正文**（`application/json`）

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `written` | boolean | 是 | false 表示这个 key 以前就有 |

### `GET /customers`

列出调用者（这个人）有权看的客户

需要范围 `app.customers:list`。English: a page of the customers inside the calling person's `customer.read` grant on the customer app.

数据范围是**这个人**在客户应用上的 `customer.read` 授权，不是应用自己的权限：应用用自己的令牌调用，或这个人没有 `customer.read`，都是 403。`stage` 是 `lead`、`customer` 或 `dormant`；`limit` 为 1 到 200；`hint` 只留下处在待处理合并建议一端的客户；`wecomPending` 只留下企业微信外部联系人尚未关联的客户。字段不合规是 422。

- 认证：`bearer`
- 范围：`app.customers:list`
- operationId：`listCustomerPage`

**参数**

| 名称 | 位置 | 类型 | 必填 | 说明 |
|---|---|---|---|---|
| `q` | query | string |  | 按姓名或身份搜索 |
| `stage` | query | string |  | 阶段<br>可选值：`lead`、`customer`、`dormant` |
| `tag` | query | string |  | 标签 id |
| `owner` | query | string |  | 负责人（员工 subject） |
| `source` | query | string |  | 来源应用 |
| `hint` | query | boolean |  | 只留下有合并建议的客户 |
| `wecomPending` | query | boolean |  | 只留下企业微信待关联的客户 |
| `limit` | query | integer |  | 一页多少条<br>最小 1，最大 200 |
| `offset` | query | integer |  | 跳过多少条<br>最小 0 |

**响应**

| 状态 | 说明 |
|---|---|
| 200 | 一页客户和同范围同筛选下的总数 |
| 401 | 见 [Unauthorized](#resp-unauthorized) |
| 403 | 见 [Forbidden](#resp-forbidden) |
| 422 | 字段或参数不被接受（正文读不出来、有未知字段、缺必填、取值越界）。见 [Unprocessable](#resp-unprocessable) |
| 429 | 见 [RateLimited](#resp-ratelimited) |
| 500 | 见 [Internal](#resp-internal) |
| 501 | `capability_off`：这个部署（或这个宿主）不提供这项能力，正文的 `capability` 是它的范围。见 [CapabilityOff](#resp-capabilityoff) |
| 503 | `unavailable`：暂时不可用（这个部署不提供的能力是 501 `capability_off`）。见 [Unavailable](#resp-unavailable) |

**200 响应正文**（`application/json`）

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `items` | CustomerSummary[] | 是 |  |
| `items[].id` | string | 是 | 主客户的 id |
| `items[].name` | string | 是 |  |
| `items[].stage` | string | 是 | 生效的阶段：lead、customer 或 dormant |
| `items[].owner` | string |  | 负责人（员工 subject） |
| `items[].ownerUnit` | string |  |  |
| `items[].sourceApp` | string |  | 第一次带来这位客户的应用 |
| `items[].sourceChannel` | string |  |  |
| `items[].phone` | string |  | 打码后的第一个电话 |
| `items[].tags` | string[] |  | 标签 id |
| `items[].lastTouchAt` | string(date-time) | 是 |  |
| `items[].createdAt` | string(date-time) | 是 |  |
| `items[].wecomBasis` | string |  | 企业微信外部联系人与客户的关联依据，取最弱的一个：none、phone 或 unionid；没有外部联系人时省略 |
| `items[].wecomPending` | boolean |  | 即 wecomBasis 为 none（待关联） |
| `total` | integer | 是 | 同一范围、同一筛选下的客户总数 |

### `GET /customers/find`

按手机号找到一位客户

需要范围 `app.customers:read`。English: finds the customer that owns the phone and answers the masked profile.

`kind` 目前只有 `phone`，`value` 是号码。没有这位客户是 404 `not_found`；`kind` 不对或 `value` 为空是 422。

- 认证：`bearer`
- 范围：`app.customers:read`
- operationId：`findCustomer`

**参数**

| 名称 | 位置 | 类型 | 必填 | 说明 |
|---|---|---|---|---|
| `kind` | query | string | 是 | 身份的种类<br>可选值：`phone` |
| `value` | query | string | 是 | 身份的值 |

**响应**

| 状态 | 说明 |
|---|---|
| 200 | 客户资料 |
| 401 | 见 [Unauthorized](#resp-unauthorized) |
| 403 | 见 [Forbidden](#resp-forbidden) |
| 404 | `not_found`：对应的记录不存在（客户中心没有这个客户，访客、服务或所指的东西不存在）。见 [NotFound](#resp-notfound) |
| 422 | 字段或参数不被接受（正文读不出来、有未知字段、缺必填、取值越界）。见 [Unprocessable](#resp-unprocessable) |
| 429 | 见 [RateLimited](#resp-ratelimited) |
| 500 | 见 [Internal](#resp-internal) |
| 501 | `capability_off`：这个部署（或这个宿主）不提供这项能力，正文的 `capability` 是它的范围。见 [CapabilityOff](#resp-capabilityoff) |
| 503 | `unavailable`：暂时不可用（这个部署不提供的能力是 501 `capability_off`）。见 [Unavailable](#resp-unavailable) |

**200 响应正文**（`application/json`）

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `id` | string | 是 | 主客户的 id；被合并过时与所问的 id 不同 |
| `name` | string | 是 |  |
| `stage` | string | 是 | 生效的阶段：lead、customer 或 dormant（长期没有接触的客户是 dormant） |
| `owner` | string |  | 负责人（员工 subject） |
| `ownerUnit` | string |  |  |
| `sourceApp` | string |  | 第一次带来这位客户的应用 |
| `sourceChannel` | string |  |  |
| `phone` | string |  | 打码后的第一个电话 |
| `tags` | string[] |  | 标签 id |
| `lastTouchAt` | string(date-time) | 是 |  |
| `createdAt` | string(date-time) | 是 |  |
| `wecomBasis` | string |  | 企业微信外部联系人与客户的关联依据，取最弱的一个：none、phone 或 unionid；没有外部联系人时为空 |
| `wecomPending` | boolean |  | 即 wecomBasis 为 none（待关联） |
| `sourceRef` | string |  |  |
| `dealtAt` | string(date-time) |  |  |
| `identities` | CustomerIdentityView[] | 是 |  |
| `identities[].kind` | string | 是 |  |
| `identities[].scope` | string |  |  |
| `identities[].masked` | string | 是 | 打码后的值 |
| `identities[].value` | string |  | 明文。只在请求了明文时出现 |
| `identities[].verified` | boolean | 是 |  |
| `identities[].firstSeen` | string(date-time) | 是 |  |
| `identities[].source` | string |  |  |
| `links` | string[] | 是 |  |
| `mergedFrom` | string[] |  | 并入它的客户 |
| `touchCount` | integer | 是 | 这位客户及并入它的客户的接触总数 |

### `GET /customers/{id}/timeline`

读一位客户的时间线（新的在前）

需要范围 `app.customers:read`。English: the customer's timeline entries, newest first, paged by the id of the last one seen.

`limit` 为 1 到 200；`before` 是上一页最后一条的 `id`，只取比它早的。没有这个客户是 404 `not_found`。

- 认证：`bearer`
- 范围：`app.customers:read`
- operationId：`listCustomerTimeline`

**参数**

| 名称 | 位置 | 类型 | 必填 | 说明 |
|---|---|---|---|---|
| `id` | path | string | 是 | 客户 id（主客户；被合并过的 id 会指到合并后的客户） |
| `limit` | query | integer |  | 最多几条<br>最小 1，最大 200 |
| `before` | query | integer(int64) |  | 只取 id 比它小的接触<br>最小 0 |

**响应**

| 状态 | 说明 |
|---|---|
| 200 | 接触，新的在前 |
| 401 | 见 [Unauthorized](#resp-unauthorized) |
| 403 | 见 [Forbidden](#resp-forbidden) |
| 404 | `not_found`：对应的记录不存在（客户中心没有这个客户，访客、服务或所指的东西不存在）。见 [NotFound](#resp-notfound) |
| 422 | 字段或参数不被接受（正文读不出来、有未知字段、缺必填、取值越界）。见 [Unprocessable](#resp-unprocessable) |
| 429 | 见 [RateLimited](#resp-ratelimited) |
| 500 | 见 [Internal](#resp-internal) |
| 501 | `capability_off`：这个部署（或这个宿主）不提供这项能力，正文的 `capability` 是它的范围。见 [CapabilityOff](#resp-capabilityoff) |
| 503 | `unavailable`：暂时不可用（这个部署不提供的能力是 501 `capability_off`）。见 [Unavailable](#resp-unavailable) |

**200 响应正文**（`application/json`）

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `items` | CustomerTouchView[] | 是 |  |
| `items[].id` | integer(int64) | 是 | 接触的 id，翻页用 |
| `items[].customer` | string | 是 | 写给哪位客户（可能是已并入的） |
| `items[].kind` | string | 是 | {应用}.{动作} |
| `items[].app` | string |  |  |
| `items[].ref` | string |  | 指向所发生之事的 k3:// 引用 |
| `items[].attrs` | object | 是 |  |
| `items[].at` | string(date-time) | 是 |  |

### `POST /customers/{id}/rename`

改客户的姓名

需要范围 `app.customers:write`。English: renames the customer. 没有这个客户是 404 `not_found`。

- 认证：`bearer`
- 范围：`app.customers:write`
- operationId：`renameCustomer`

**参数**

| 名称 | 位置 | 类型 | 必填 | 说明 |
|---|---|---|---|---|
| `id` | path | string | 是 | 客户 id（主客户；被合并过的 id 会指到合并后的客户） |
| `Idempotency-Key` | header | string |  | 每个 POST 都收。SDK 未传入时会生成 uuid v4。24 小时内同键同正文重放第一次的回答<br>最短 1，最长 255 |

**请求正文（必填）**（`application/json`）

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `name` | string | 是 |  |

**响应**

| 状态 | 说明 |
|---|---|
| 204 | 已办。没有正文 |
| 401 | 见 [Unauthorized](#resp-unauthorized) |
| 403 | 见 [Forbidden](#resp-forbidden) |
| 404 | `not_found`：对应的记录不存在（客户中心没有这个客户，访客、服务或所指的东西不存在）。见 [NotFound](#resp-notfound) |
| 413 | `payload_too_large`：正文太大。见 [PayloadTooLarge](#resp-payloadtoolarge) |
| 422 | 字段或参数不被接受（正文读不出来、有未知字段、缺必填、取值越界）。见 [Unprocessable](#resp-unprocessable) |
| 429 | 见 [RateLimited](#resp-ratelimited) |
| 500 | 见 [Internal](#resp-internal) |
| 501 | `capability_off`：这个部署（或这个宿主）不提供这项能力，正文的 `capability` 是它的范围。见 [CapabilityOff](#resp-capabilityoff) |
| 503 | `unavailable`：暂时不可用（这个部署不提供的能力是 501 `capability_off`）。见 [Unavailable](#resp-unavailable) |

### `POST /customers/{id}/links`

把本应用的一条记录挂到客户上

需要范围 `app.customers:write`。English: links one of the calling app's own records to the customer.

`ref` 必须是本应用自己的记录（`k3://<调用的应用>/…`），别的应用的引用是 422。

- 认证：`bearer`
- 范围：`app.customers:write`
- operationId：`linkCustomer`

**参数**

| 名称 | 位置 | 类型 | 必填 | 说明 |
|---|---|---|---|---|
| `id` | path | string | 是 | 客户 id（主客户；被合并过的 id 会指到合并后的客户） |
| `Idempotency-Key` | header | string |  | 每个 POST 都收。SDK 未传入时会生成 uuid v4。24 小时内同键同正文重放第一次的回答<br>最短 1，最长 255 |

**请求正文（必填）**（`application/json`）

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `ref` | string | 是 | 本应用自己的记录：k3://&lt;调用的应用>/… |

**响应**

| 状态 | 说明 |
|---|---|
| 204 | 已办。没有正文 |
| 401 | 见 [Unauthorized](#resp-unauthorized) |
| 403 | 见 [Forbidden](#resp-forbidden) |
| 404 | `not_found`：对应的记录不存在（客户中心没有这个客户，访客、服务或所指的东西不存在）。见 [NotFound](#resp-notfound) |
| 413 | `payload_too_large`：正文太大。见 [PayloadTooLarge](#resp-payloadtoolarge) |
| 422 | 字段或参数不被接受（正文读不出来、有未知字段、缺必填、取值越界）。见 [Unprocessable](#resp-unprocessable) |
| 429 | 见 [RateLimited](#resp-ratelimited) |
| 500 | 见 [Internal](#resp-internal) |
| 501 | `capability_off`：这个部署（或这个宿主）不提供这项能力，正文的 `capability` 是它的范围。见 [CapabilityOff](#resp-capabilityoff) |
| 503 | `unavailable`：暂时不可用（这个部署不提供的能力是 501 `capability_off`）。见 [Unavailable](#resp-unavailable) |

### `POST /customers/{id}/owner`

改客户的负责人和归属部门

需要范围 `app.customers:write`。English: sets the customer's owner and owning unit.

`owner` 是员工 subject，`ownerUnit` 是部门 id；两个都可以是空串，表示清空。

- 认证：`bearer`
- 范围：`app.customers:write`
- operationId：`setCustomerOwner`

**参数**

| 名称 | 位置 | 类型 | 必填 | 说明 |
|---|---|---|---|---|
| `id` | path | string | 是 | 客户 id（主客户；被合并过的 id 会指到合并后的客户） |
| `Idempotency-Key` | header | string |  | 每个 POST 都收。SDK 未传入时会生成 uuid v4。24 小时内同键同正文重放第一次的回答<br>最短 1，最长 255 |

**请求正文（必填）**（`application/json`）

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `owner` | string | 是 | 员工 subject；空串清空 |
| `ownerUnit` | string | 是 | 部门 id；空串清空 |

**响应**

| 状态 | 说明 |
|---|---|
| 204 | 已办。没有正文 |
| 401 | 见 [Unauthorized](#resp-unauthorized) |
| 403 | 见 [Forbidden](#resp-forbidden) |
| 404 | `not_found`：对应的记录不存在（客户中心没有这个客户，访客、服务或所指的东西不存在）。见 [NotFound](#resp-notfound) |
| 413 | `payload_too_large`：正文太大。见 [PayloadTooLarge](#resp-payloadtoolarge) |
| 422 | 字段或参数不被接受（正文读不出来、有未知字段、缺必填、取值越界）。见 [Unprocessable](#resp-unprocessable) |
| 429 | 见 [RateLimited](#resp-ratelimited) |
| 500 | 见 [Internal](#resp-internal) |
| 501 | `capability_off`：这个部署（或这个宿主）不提供这项能力，正文的 `capability` 是它的范围。见 [CapabilityOff](#resp-capabilityoff) |
| 503 | `unavailable`：暂时不可用（这个部署不提供的能力是 501 `capability_off`）。见 [Unavailable](#resp-unavailable) |

### `POST /customers/{id}/dealt`

记这位客户成交了

需要范围 `app.customers:write`。English: marks the customer as having dealt, at the given time or now.

`at` 省略为现在。

- 认证：`bearer`
- 范围：`app.customers:write`
- operationId：`markCustomerDealt`

**参数**

| 名称 | 位置 | 类型 | 必填 | 说明 |
|---|---|---|---|---|
| `id` | path | string | 是 | 客户 id（主客户；被合并过的 id 会指到合并后的客户） |
| `Idempotency-Key` | header | string |  | 每个 POST 都收。SDK 未传入时会生成 uuid v4。24 小时内同键同正文重放第一次的回答<br>最短 1，最长 255 |

**请求正文（必填）**（`application/json`）

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `at` | string(date-time) |  | 省略为现在 |

**响应**

| 状态 | 说明 |
|---|---|
| 204 | 已办。没有正文 |
| 401 | 见 [Unauthorized](#resp-unauthorized) |
| 403 | 见 [Forbidden](#resp-forbidden) |
| 404 | `not_found`：对应的记录不存在（客户中心没有这个客户，访客、服务或所指的东西不存在）。见 [NotFound](#resp-notfound) |
| 413 | `payload_too_large`：正文太大。见 [PayloadTooLarge](#resp-payloadtoolarge) |
| 422 | 字段或参数不被接受（正文读不出来、有未知字段、缺必填、取值越界）。见 [Unprocessable](#resp-unprocessable) |
| 429 | 见 [RateLimited](#resp-ratelimited) |
| 500 | 见 [Internal](#resp-internal) |
| 501 | `capability_off`：这个部署（或这个宿主）不提供这项能力，正文的 `capability` 是它的范围。见 [CapabilityOff](#resp-capabilityoff) |
| 503 | `unavailable`：暂时不可用（这个部署不提供的能力是 501 `capability_off`）。见 [Unavailable](#resp-unavailable) |

### `POST /customers/{id}/merge`

把另一位客户并入这一位

需要范围 `app.customers:write`。English: merges the other customer into this one and answers the primary customer's id.

`other` 必填。两个 id 都可以是已被合并过的，返回的 `primary` 是合并后的主客户。

- 认证：`bearer`
- 范围：`app.customers:write`
- operationId：`mergeCustomers`

**参数**

| 名称 | 位置 | 类型 | 必填 | 说明 |
|---|---|---|---|---|
| `id` | path | string | 是 | 客户 id（主客户；被合并过的 id 会指到合并后的客户） |
| `Idempotency-Key` | header | string |  | 每个 POST 都收。SDK 未传入时会生成 uuid v4。24 小时内同键同正文重放第一次的回答<br>最短 1，最长 255 |

**请求正文（必填）**（`application/json`）

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `other` | string | 是 | 并入这一位的客户 id |

**响应**

| 状态 | 说明 |
|---|---|
| 200 | 合并后的主客户 |
| 401 | 见 [Unauthorized](#resp-unauthorized) |
| 403 | 见 [Forbidden](#resp-forbidden) |
| 404 | `not_found`：对应的记录不存在（客户中心没有这个客户，访客、服务或所指的东西不存在）。见 [NotFound](#resp-notfound) |
| 413 | `payload_too_large`：正文太大。见 [PayloadTooLarge](#resp-payloadtoolarge) |
| 422 | 字段或参数不被接受（正文读不出来、有未知字段、缺必填、取值越界）。见 [Unprocessable](#resp-unprocessable) |
| 429 | 见 [RateLimited](#resp-ratelimited) |
| 500 | 见 [Internal](#resp-internal) |
| 501 | `capability_off`：这个部署（或这个宿主）不提供这项能力，正文的 `capability` 是它的范围。见 [CapabilityOff](#resp-capabilityoff) |
| 503 | `unavailable`：暂时不可用（这个部署不提供的能力是 501 `capability_off`）。见 [Unavailable](#resp-unavailable) |

**200 响应正文**（`application/json`）

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `primary` | string | 是 | 合并后的主客户 id |

### `POST /customers/{id}/erase`

抹除一位客户的个人数据（不可撤销）

需要范围 `app.customers:write`。English: erases the customer's personal data; final.

正文是 `{}`。抹除之后不能恢复。

- 认证：`bearer`
- 范围：`app.customers:write`
- operationId：`eraseCustomer`

**参数**

| 名称 | 位置 | 类型 | 必填 | 说明 |
|---|---|---|---|---|
| `id` | path | string | 是 | 客户 id（主客户；被合并过的 id 会指到合并后的客户） |
| `Idempotency-Key` | header | string |  | 每个 POST 都收。SDK 未传入时会生成 uuid v4。24 小时内同键同正文重放第一次的回答<br>最短 1，最长 255 |

**请求正文（必填）**（`application/json`）

类型 `CustomerEraseRequest`。空对象 {}

**响应**

| 状态 | 说明 |
|---|---|
| 204 | 已办。没有正文 |
| 401 | 见 [Unauthorized](#resp-unauthorized) |
| 403 | 见 [Forbidden](#resp-forbidden) |
| 404 | `not_found`：对应的记录不存在（客户中心没有这个客户，访客、服务或所指的东西不存在）。见 [NotFound](#resp-notfound) |
| 413 | `payload_too_large`：正文太大。见 [PayloadTooLarge](#resp-payloadtoolarge) |
| 422 | 字段或参数不被接受（正文读不出来、有未知字段、缺必填、取值越界）。见 [Unprocessable](#resp-unprocessable) |
| 429 | 见 [RateLimited](#resp-ratelimited) |
| 500 | 见 [Internal](#resp-internal) |
| 501 | `capability_off`：这个部署（或这个宿主）不提供这项能力，正文的 `capability` 是它的范围。见 [CapabilityOff](#resp-capabilityoff) |
| 503 | `unavailable`：暂时不可用（这个部署不提供的能力是 501 `capability_off`）。见 [Unavailable](#resp-unavailable) |

### `GET /customers/tags`

列出企业的客户标签库（opv2 J2 ④）

需要范围 `app.customers:read`。English: the tenant's tag dictionary; `writable` says which tags an app may put on and take off.

不含每个标签有多少客户。

- 认证：`bearer`
- 范围：`app.customers:read`
- operationId：`listCustomerTags`

**响应**

| 状态 | 说明 |
|---|---|
| 200 | 标签库 |
| 401 | 见 [Unauthorized](#resp-unauthorized) |
| 403 | 见 [Forbidden](#resp-forbidden) |
| 429 | 见 [RateLimited](#resp-ratelimited) |
| 500 | 见 [Internal](#resp-internal) |
| 501 | `capability_off`：这个部署（或这个宿主）不提供这项能力，正文的 `capability` 是它的范围。见 [CapabilityOff](#resp-capabilityoff) |
| 503 | `unavailable`：暂时不可用（这个部署不提供的能力是 501 `capability_off`）。见 [Unavailable](#resp-unavailable) |

**200 响应正文**（`application/json`）

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `items` | AppCustomerTag[] | 是 |  |
| `items[].id` | string | 是 | 标签 id |
| `items[].name` | string | 是 | 名称 |
| `items[].color` | string | 是 | 颜色 |
| `items[].source` | string | 是 | 来源：manual、wecom 或 rule |
| `items[].writable` | boolean | 是 | 应用能不能打上、去掉（手动标签） |

### `PUT /customers/{id}/tags/{tag}`

给本企业的一位客户打上标签（opv2 J2 ④）

需要范围 `app.customers:tag`。English: puts a manual tag on a customer of the tenant.

只能是标签库里的手动标签：别的来源（企业微信）的标签只读，返回 422 `validation_failed`。客户或标签不存在是 404 `not_found`；客户被合并时作用在主客户上。重复调用结果相同。每次改动在企业的审计链里记一条（`customer.tag` / `customer.untag`，操作者是页面会话的人或应用自己）；一个应用在一个企业里每 10 分钟最多改 3000 次，超过返回 429。只限本企业的客户：应用自己的调用（企业令牌）可改本企业任何客户；页面会话里的人只能改他在客户应用里「编辑客户」（customer.write）的数据范围内的客户，范围外的客户与不存在一样返回 404。

- 认证：`bearer`
- 范围：`app.customers:tag`
- operationId：`addCustomerTag`

**参数**

| 名称 | 位置 | 类型 | 必填 | 说明 |
|---|---|---|---|---|
| `Idempotency-Key` | header | string |  | 每个 POST 都收。SDK 未传入时会生成 uuid v4。24 小时内同键同正文重放第一次的回答<br>最短 1，最长 255 |
| `id` | path | string | 是 | 客户 id |
| `tag` | path | string | 是 | 标签 id（GET /customers/tags） |

**响应**

| 状态 | 说明 |
|---|---|
| 200 | 已改 |
| 401 | 见 [Unauthorized](#resp-unauthorized) |
| 403 | 见 [Forbidden](#resp-forbidden) |
| 404 | `not_found`：对应的记录不存在（客户中心没有这个客户，访客、服务或所指的东西不存在）。见 [NotFound](#resp-notfound) |
| 422 | 字段或参数不被接受（正文读不出来、有未知字段、缺必填、取值越界）。见 [Unprocessable](#resp-unprocessable) |
| 429 | 见 [RateLimited](#resp-ratelimited) |
| 500 | 见 [Internal](#resp-internal) |
| 501 | `capability_off`：这个部署（或这个宿主）不提供这项能力，正文的 `capability` 是它的范围。见 [CapabilityOff](#resp-capabilityoff) |
| 503 | `unavailable`：暂时不可用（这个部署不提供的能力是 501 `capability_off`）。见 [Unavailable](#resp-unavailable) |

**200 响应正文**（`application/json`）

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `customer` | string | 是 | 客户 id |
| `tag` | string | 是 | 标签 id |
| `on` | boolean | 是 | true 打上，false 去掉 |

### `DELETE /customers/{id}/tags/{tag}`

去掉本企业一位客户的标签（opv2 J2 ④）

需要范围 `app.customers:tag`。English: takes a manual tag off a customer of the tenant.

只能是标签库里的手动标签：别的来源（企业微信）的标签只读，返回 422 `validation_failed`。客户或标签不存在是 404 `not_found`；客户被合并时作用在主客户上。重复调用结果相同。每次改动在企业的审计链里记一条（`customer.tag` / `customer.untag`，操作者是页面会话的人或应用自己）；一个应用在一个企业里每 10 分钟最多改 3000 次，超过返回 429。只限本企业的客户：应用自己的调用（企业令牌）可改本企业任何客户；页面会话里的人只能改他在客户应用里「编辑客户」（customer.write）的数据范围内的客户，范围外的客户与不存在一样返回 404。

- 认证：`bearer`
- 范围：`app.customers:tag`
- operationId：`removeCustomerTag`

**参数**

| 名称 | 位置 | 类型 | 必填 | 说明 |
|---|---|---|---|---|
| `Idempotency-Key` | header | string |  | 每个 POST 都收。SDK 未传入时会生成 uuid v4。24 小时内同键同正文重放第一次的回答<br>最短 1，最长 255 |
| `id` | path | string | 是 | 客户 id |
| `tag` | path | string | 是 | 标签 id（GET /customers/tags） |

**响应**

| 状态 | 说明 |
|---|---|
| 200 | 已改 |
| 401 | 见 [Unauthorized](#resp-unauthorized) |
| 403 | 见 [Forbidden](#resp-forbidden) |
| 404 | `not_found`：对应的记录不存在（客户中心没有这个客户，访客、服务或所指的东西不存在）。见 [NotFound](#resp-notfound) |
| 422 | 字段或参数不被接受（正文读不出来、有未知字段、缺必填、取值越界）。见 [Unprocessable](#resp-unprocessable) |
| 429 | 见 [RateLimited](#resp-ratelimited) |
| 500 | 见 [Internal](#resp-internal) |
| 501 | `capability_off`：这个部署（或这个宿主）不提供这项能力，正文的 `capability` 是它的范围。见 [CapabilityOff](#resp-capabilityoff) |
| 503 | `unavailable`：暂时不可用（这个部署不提供的能力是 501 `capability_off`）。见 [Unavailable](#resp-unavailable) |

**200 响应正文**（`application/json`）

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `customer` | string | 是 | 客户 id |
| `tag` | string | 是 | 标签 id |
| `on` | boolean | 是 | true 打上，false 去掉 |

## visitors

站点访客与客户的对应

### `GET /site-visitors/{site}/{visitor}`

一位站点访客对应的客户，以及访客是否证明了自己是谁

需要范围 `app.visitors:read`。English: the customer a site visitor is tied to, and whether the visitor proved the tie.

没有对应的客户时 `customer` 为空字符串，`verified` 为 false。`verified` 为 true 表示访客自己证明过身份（微信、短信验证码）；表单或 Identify 写入的对应是 false。要授予访问权限请看 `verified`，只做归因看 `customer` 即可。

- 认证：`bearer`
- 范围：`app.visitors:read`
- operationId：`getSiteVisitor`

**参数**

| 名称 | 位置 | 类型 | 必填 | 说明 |
|---|---|---|---|---|
| `site` | path | string | 是 | 站点 id |
| `visitor` | path | string | 是 | 访客 id |

**响应**

| 状态 | 说明 |
|---|---|
| 200 | 对应的客户 |
| 401 | 见 [Unauthorized](#resp-unauthorized) |
| 403 | 见 [Forbidden](#resp-forbidden) |
| 404 | `not_found`：对应的记录不存在（客户中心没有这个客户，访客、服务或所指的东西不存在）。见 [NotFound](#resp-notfound) |
| 429 | 见 [RateLimited](#resp-ratelimited) |
| 500 | 见 [Internal](#resp-internal) |
| 501 | `capability_off`：这个部署（或这个宿主）不提供这项能力，正文的 `capability` 是它的范围。见 [CapabilityOff](#resp-capabilityoff) |
| 503 | `unavailable`：暂时不可用（这个部署不提供的能力是 501 `capability_off`）。见 [Unavailable](#resp-unavailable) |

**200 响应正文**（`application/json`）

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `customer` | string | 是 | 对应的客户 id；没有对应时为空字符串 |
| `verified` | boolean | 是 | 访客自己证明过身份（微信、短信验证码）；表单或 Identify 写入的对应是 false |

### `PUT /site-visitors/{site}/{visitor}`

把一位站点访客认定为某位客户

需要范围 `app.visitors:write`。English: ties a site visitor to a customer.

正文 `{customer}`。返回写入后访客对应的客户 id。

- 认证：`bearer`
- 范围：`app.visitors:write`
- operationId：`identifySiteVisitor`

**参数**

| 名称 | 位置 | 类型 | 必填 | 说明 |
|---|---|---|---|---|
| `site` | path | string | 是 | 站点 id |
| `visitor` | path | string | 是 | 访客 id |

**请求正文（必填）**（`application/json`）

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `customer` | string | 是 | 客户 id |

**响应**

| 状态 | 说明 |
|---|---|
| 200 | 访客现在对应的客户 |
| 401 | 见 [Unauthorized](#resp-unauthorized) |
| 403 | 见 [Forbidden](#resp-forbidden) |
| 404 | `not_found`：对应的记录不存在（客户中心没有这个客户，访客、服务或所指的东西不存在）。见 [NotFound](#resp-notfound) |
| 413 | `payload_too_large`：正文太大。见 [PayloadTooLarge](#resp-payloadtoolarge) |
| 422 | 字段或参数不被接受（正文读不出来、有未知字段、缺必填、取值越界）。见 [Unprocessable](#resp-unprocessable) |
| 429 | 见 [RateLimited](#resp-ratelimited) |
| 500 | 见 [Internal](#resp-internal) |
| 501 | `capability_off`：这个部署（或这个宿主）不提供这项能力，正文的 `capability` 是它的范围。见 [CapabilityOff](#resp-capabilityoff) |
| 503 | `unavailable`：暂时不可用（这个部署不提供的能力是 501 `capability_off`）。见 [Unavailable](#resp-unavailable) |

**200 响应正文**（`application/json`）

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `customer` | string | 是 | 访客现在对应的客户 id |

### `GET /site-visitors`

按顺序号列出已被认定的站点访客（给订阅端续读）

需要范围 `app.visitors:read`。English: the identified visitors after a sequence number; a reader keeps the last `seq` it handled.

`after` 是已处理的最后一个 `seq`（默认 0，不能为负）。`limit` 必填，1 到 500。其他值是 422。没有结果时 `items` 是 `[]`。

- 认证：`bearer`
- 范围：`app.visitors:read`
- operationId：`listIdentifiedSiteVisitors`

**参数**

| 名称 | 位置 | 类型 | 必填 | 说明 |
|---|---|---|---|---|
| `after` | query | integer |  | 从这个顺序号之后开始读<br>默认 `0`<br>最小 0 |
| `limit` | query | integer |  | 条数，必填，1 到 500<br>最小 1，最大 500 |

**响应**

| 状态 | 说明 |
|---|---|
| 200 | 一批已认定的访客 |
| 401 | 见 [Unauthorized](#resp-unauthorized) |
| 403 | 见 [Forbidden](#resp-forbidden) |
| 422 | 字段或参数不被接受（正文读不出来、有未知字段、缺必填、取值越界）。见 [Unprocessable](#resp-unprocessable) |
| 429 | 见 [RateLimited](#resp-ratelimited) |
| 500 | 见 [Internal](#resp-internal) |
| 501 | `capability_off`：这个部署（或这个宿主）不提供这项能力，正文的 `capability` 是它的范围。见 [CapabilityOff](#resp-capabilityoff) |
| 503 | `unavailable`：暂时不可用（这个部署不提供的能力是 501 `capability_off`）。见 [Unavailable](#resp-unavailable) |

**200 响应正文**（`application/json`）

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `items` | IdentifiedVisitor[] | 是 |  |
| `items[].site` | string | 是 |  |
| `items[].visitor` | string | 是 |  |
| `items[].customer` | string | 是 |  |
| `items[].seq` | integer | 是 | 顺序号，续读时作为 after |
| `items[].at` | string(date-time) | 是 |  |
| `items[].verified` | boolean | 是 | 访客自己证明过身份；false 是表单或 Identify 写的对应 |

## access

当前人员在本应用里能做什么，以及所在部门

### `GET /access`

当前人员对本应用某个权限点能做什么

需要范围 `app.access:read`。English: what the caller (the person of the page session) may do with one permission of this app.

`point` 必填，必须以 `app.{本应用 id}.` 开头，否则 403 `forbidden`。`type` 是对象类型 id（读对象类型时用来取字段可见性）。

正文的字段名**不是** camelCase：它是权限求值器的结果原样输出（`Managed`、`Allowed`、`All`、`Self`、`Units`、`Subject`、`Personal`、`Fields`）。基础模式（组织没有启用权限模型）下，平台角色说了算，`Allowed` 与范围不适用。平台没有接通权限求值时，回答是只有 `{"managed": false}`（小写）。

- 认证：`bearer`
- 范围：`app.access:read`
- operationId：`checkAccess`

**参数**

| 名称 | 位置 | 类型 | 必填 | 说明 |
|---|---|---|---|---|
| `point` | query | string | 是 | 权限点，以 app.{本应用 id}. 开头 |
| `type` | query | string |  | 对象类型 id，可省略 |

**响应**

| 状态 | 说明 |
|---|---|
| 200 | 求值结果 |
| 401 | 见 [Unauthorized](#resp-unauthorized) |
| 403 | 见 [Forbidden](#resp-forbidden) |
| 429 | 见 [RateLimited](#resp-ratelimited) |
| 500 | 见 [Internal](#resp-internal) |
| 501 | `capability_off`：这个部署（或这个宿主）不提供这项能力，正文的 `capability` 是它的范围。见 [CapabilityOff](#resp-capabilityoff) |
| 503 | `unavailable`：暂时不可用（这个部署不提供的能力是 501 `capability_off`）。见 [Unavailable](#resp-unavailable) |

**200 响应正文**（`application/json`）

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `Managed` | boolean |  | 组织启用了权限模型。false 是基础模式：平台角色说了算，下面的字段不适用 |
| `Allowed` | boolean |  | 这个权限点是否允许 |
| `All` | boolean |  | 每一条记录 |
| `Self` | boolean |  | 自己创建的记录 |
| `Units` | string[]，可为 null |  | 在这些部门创建的记录（子树已展开） |
| `Subject` | string |  | 被求值的人 |
| `Personal` | boolean |  | 个人授权给了（一部分）权限 |
| `Fields` | map[string, string]，可为 null |  | 读对象类型时，调用者看到被打码或完全看不到的字段 |

### `POST /access/points`

当前人员对本应用多个权限点能做什么（一次问完）

需要范围 `app.access:read`。English: what the caller may do with several permissions of this app, in one call.

`points` 是 1 到 100 个 `{point, type}`，规则同 `GET /access`：每个 `point` 都必须以 `app.{本应用 id}.` 开头，有一个不是就整次 403 `forbidden`（什么都不问）；个数为 0 或超过 100 是 422。`results[i]` 就是 `GET /access` 对 `points[i]` 的回答（同一个求值器、同样不是 camelCase 的字段名；平台没有接通权限求值时每一项都是 `{"managed": false}`）。平台不记住答案：收回的权限在下一次调用就没有了。English: 1 to 100 `{point, type}`, each under the `app.{app id}.` prefix (one that is not refuses the whole call with 403 before anything is asked; none or over 100 is 422); `results[i]` is exactly GET /access's answer for `points[i]`, nothing cached.

- 认证：`bearer`
- 范围：`app.access:read`
- operationId：`checkAccessPoints`

**参数**

| 名称 | 位置 | 类型 | 必填 | 说明 |
|---|---|---|---|---|
| `Idempotency-Key` | header | string |  | 每个 POST 都收。SDK 未传入时会生成 uuid v4。24 小时内同键同正文重放第一次的回答<br>最短 1，最长 255 |

**请求正文（必填）**（`application/json`）

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `points` | AccessPoint[] | 是 | 最多 100 项 |
| `points[].point` | string | 是 | 权限点，以 app.{本应用 id}. 开头 |
| `points[].type` | string |  | 对象类型 id，可省略 |

**响应**

| 状态 | 说明 |
|---|---|
| 200 | 每个权限点的求值结果，与请求同序 |
| 401 | 见 [Unauthorized](#resp-unauthorized) |
| 403 | 见 [Forbidden](#resp-forbidden) |
| 413 | `payload_too_large`：正文太大。见 [PayloadTooLarge](#resp-payloadtoolarge) |
| 422 | 字段或参数不被接受（正文读不出来、有未知字段、缺必填、取值越界）。见 [Unprocessable](#resp-unprocessable) |
| 429 | 见 [RateLimited](#resp-ratelimited) |
| 500 | 见 [Internal](#resp-internal) |
| 501 | `capability_off`：这个部署（或这个宿主）不提供这项能力，正文的 `capability` 是它的范围。见 [CapabilityOff](#resp-capabilityoff) |
| 503 | `unavailable`：暂时不可用（这个部署不提供的能力是 501 `capability_off`）。见 [Unavailable](#resp-unavailable) |

**200 响应正文**（`application/json`）

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `results` | Access[] | 是 | 与 points 同序，每项同 GET /access 的回答 |
| `results[].Managed` | boolean |  | 组织启用了权限模型。false 是基础模式：平台角色说了算，下面的字段不适用 |
| `results[].Allowed` | boolean |  | 这个权限点是否允许 |
| `results[].All` | boolean |  | 每一条记录 |
| `results[].Self` | boolean |  | 自己创建的记录 |
| `results[].Units` | string[]，可为 null |  | 在这些部门创建的记录（子树已展开） |
| `results[].Subject` | string |  | 被求值的人 |
| `results[].Personal` | boolean |  | 个人授权给了（一部分）权限 |
| `results[].Fields` | map[string, string]，可为 null |  | 读对象类型时，调用者看到被打码或完全看不到的字段 |

### `GET /unit`

当前人员所在的部门

需要范围 `app.access:read`。English: the department of the caller. 平台没有接通权限求值时是 501 `capability_off`。

- 认证：`bearer`
- 范围：`app.access:read`
- operationId：`getUnit`

**响应**

| 状态 | 说明 |
|---|---|
| 200 | 部门 id（没有部门时为空字符串） |
| 401 | 见 [Unauthorized](#resp-unauthorized) |
| 403 | 见 [Forbidden](#resp-forbidden) |
| 429 | 见 [RateLimited](#resp-ratelimited) |
| 500 | 见 [Internal](#resp-internal) |
| 501 | `capability_off`：这个部署（或这个宿主）不提供这项能力，正文的 `capability` 是它的范围。见 [CapabilityOff](#resp-capabilityoff) |
| 503 | `unavailable`：暂时不可用（这个部署不提供的能力是 501 `capability_off`）。见 [Unavailable](#resp-unavailable) |

**200 响应正文**（`application/json`）

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `unit` | string | 是 | 部门 id；没有部门时为空字符串 |

## directory

企业的成员列表

### `GET /members`

企业的成员列表

需要范围 `app.directory:read`。English: the members of the tenant. 没有成员时 `items` 是 `[]`。

- 认证：`bearer`
- 范围：`app.directory:read`
- operationId：`listDirectoryMembers`

**响应**

| 状态 | 说明 |
|---|---|
| 200 | 成员 |
| 401 | 见 [Unauthorized](#resp-unauthorized) |
| 403 | 见 [Forbidden](#resp-forbidden) |
| 429 | 见 [RateLimited](#resp-ratelimited) |
| 500 | 见 [Internal](#resp-internal) |
| 501 | `capability_off`：这个部署（或这个宿主）不提供这项能力，正文的 `capability` 是它的范围。见 [CapabilityOff](#resp-capabilityoff) |
| 503 | `unavailable`：暂时不可用（这个部署不提供的能力是 501 `capability_off`）。见 [Unavailable](#resp-unavailable) |

**200 响应正文**（`application/json`）

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `items` | Member[] | 是 |  |
| `items[].userId` | string | 是 |  |
| `items[].email` | string | 是 |  |
| `items[].name` | string | 是 |  |
| `items[].state` | string | 是 | active、inactive 或 invited |
| `items[].roles` | string[] | 是 |  |
| `items[].machine` | boolean |  |  |
| `items[].avatarUrl` | string |  | 头像的绝对地址；没有头像时省略 |

## ledger

向经营记账（本应用来源的记账行）

### `POST /ledger/posts`

向经营记账记一笔（本应用来源）

需要范围 `app.ledger:post`。English: enqueues one posting in the platform's outbox and starts its delivery to the books; answers the outbox key.

`sourceApp` 必须是调用的应用，否则 403 `forbidden`；例外是本应用自己的网关收款按渠道记（与所有渠道记账一样）：`sourceApp` 为 `channel:wechat` 或 `channel:alipay`，`sourceRef` 为 `k3://payments/order/{id}`，那笔支付单是本应用在这个渠道建的、已付款，金额不超过它。English: `sourceApp` must be the calling app, or `channel:{wechat|alipay}` with `sourceRef` `k3://payments/order/{id}` of a paid payment order the app made on that channel, the amount within it. `sourceApp + sourceRef + kind` 是记账的去重键：同一组合只记一次。`direction` 是 `in` 或 `out`，`amountCents` 大于 0（分），`currency` 省略为组织的币种（不做换算，别的币种会被拒绝），`occurredOn` 省略为组织时区的今天。

`reverses` 写某个更早的同来源同引用的记账的 `kind`，本笔是它的冲销（金额必须相同、方向相反，不能同时是账单）。`settles` 写某笔开出账单的更早记账的 `kind`，本笔是对这张账单的付款。这两个字段互斥，也都不能与 `bill` 同用，违反是 422。

- 认证：`bearer`
- 范围：`app.ledger:post`
- operationId：`postLedger`

**参数**

| 名称 | 位置 | 类型 | 必填 | 说明 |
|---|---|---|---|---|
| `Idempotency-Key` | header | string |  | 每个 POST 都收。SDK 未传入时会生成 uuid v4。24 小时内同键同正文重放第一次的回答<br>最短 1，最长 255 |

**请求正文（必填）**（`application/json`）

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `sourceApp` | string | 是 | 必须是调用的应用；本应用已付款的网关支付单按渠道记时为 channel:wechat / channel:alipay |
| `sourceRef` | string | 是 | 来源记录的 k3:// 引用，例如 k3://membership/package/pk_1 |
| `kind` | string | 是 | purchase、refund、approval… |
| `direction` | string | 是 | 可选值：`in`、`out` |
| `amountCents` | integer | 是 | 大于 0，单位分 |
| `currency` | string |  | 省略为组织的币种；别的币种会被拒绝，不做换算 |
| `occurredOn` | string |  | YYYY-MM-DD；省略为组织时区的今天 |
| `account` | string |  | 账簿的账户 id，调用方知道时才给 |
| `method` | string |  | 用调用方的话说的付款方式（cash wechat alipay card transfer other），账簿据此映射账户 |
| `category` | string |  | 分类名（在该方向下找到或建立） |
| `party` | LedgerParty |  |  |
| `party.name` | string | 是 |  |
| `party.ref` | string |  |  |
| `party.kind` | string |  | customer、supplier 或 both（默认：收入记 customer，支出记 supplier） |
| `memo` | string |  |  |
| `bill` | LedgerBill |  | 要一张账单（收入是应收，支出是应付），而不是一笔流水 |
| `bill.title` | string |  |  |
| `bill.dueOn` | string |  | YYYY-MM-DD；省略为没有到期日 |
| `reverses` | string |  | 更早的同来源同引用的记账的 kind，本笔是它的冲销。金额须相同，方向相反 |
| `settles` | string |  | 开出账单的更早记账的 kind，本笔是对这张账单的付款，至多付到账单还欠的数目 |
| `createdBy` | string |  | 记录的所有人（用于数据范围）；省略为调用者 |
| `ownerUnit` | string |  | 记录的归属部门；省略为调用者的部门 |

**响应**

| 状态 | 说明 |
|---|---|
| 200 | 已进入发件箱。`key` 是发件箱键 |
| 401 | 见 [Unauthorized](#resp-unauthorized) |
| 403 | 见 [Forbidden](#resp-forbidden) |
| 413 | `payload_too_large`：正文太大。见 [PayloadTooLarge](#resp-payloadtoolarge) |
| 422 | 字段或参数不被接受（正文读不出来、有未知字段、缺必填、取值越界）。见 [Unprocessable](#resp-unprocessable) |
| 429 | 见 [RateLimited](#resp-ratelimited) |
| 500 | 见 [Internal](#resp-internal) |
| 501 | `capability_off`：这个部署（或这个宿主）不提供这项能力，正文的 `capability` 是它的范围。见 [CapabilityOff](#resp-capabilityoff) |
| 503 | `unavailable`：暂时不可用（这个部署不提供的能力是 501 `capability_off`）。见 [Unavailable](#resp-unavailable) |

**200 响应正文**（`application/json`）

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `key` | string | 是 | 发件箱键 |

## audit

向企业的审计链写入本应用的操作

### `POST /audit`

向企业的审计链写入本应用的一条操作

需要范围 `app.audit:write`。English: appends one entry to the tenant's audit chain as the caller.

`action` 必须以 `{本应用 id}.` 开头，否则 403 `forbidden`。成功回答 204，没有正文。

- 认证：`bearer`
- 范围：`app.audit:write`
- operationId：`appendAudit`

**参数**

| 名称 | 位置 | 类型 | 必填 | 说明 |
|---|---|---|---|---|
| `Idempotency-Key` | header | string |  | 每个 POST 都收。SDK 未传入时会生成 uuid v4。24 小时内同键同正文重放第一次的回答<br>最短 1，最长 255 |

**请求正文（必填）**（`application/json`）

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `action` | string | 是 | 必须以 {本应用 id}. 开头 |
| `resource` | string | 是 |  |
| `diff` | any |  | 任意 JSON，记入审计链 |

**响应**

| 状态 | 说明 |
|---|---|
| 204 | 已写入。没有正文 |
| 401 | 见 [Unauthorized](#resp-unauthorized) |
| 403 | 见 [Forbidden](#resp-forbidden) |
| 413 | `payload_too_large`：正文太大。见 [PayloadTooLarge](#resp-payloadtoolarge) |
| 422 | 字段或参数不被接受（正文读不出来、有未知字段、缺必填、取值越界）。见 [Unprocessable](#resp-unprocessable) |
| 429 | 见 [RateLimited](#resp-ratelimited) |
| 500 | 见 [Internal](#resp-internal) |
| 501 | `capability_off`：这个部署（或这个宿主）不提供这项能力，正文的 `capability` 是它的范围。见 [CapabilityOff](#resp-capabilityoff) |
| 503 | `unavailable`：暂时不可用（这个部署不提供的能力是 501 `capability_off`）。见 [Unavailable](#resp-unavailable) |

## installations

安装了本应用的企业（只收应用自己的令牌）

### `GET /installations`

安装了本应用且已开启的企业

需要范围 `app.installations:read`。只收应用自己的令牌；页面会话是 403 `forbidden`。测试凭证的令牌列出装了本应用的测试企业（不论属于哪个开发者组织）；正式凭证的令牌只列真实组织。English: the tenants that have the app installed and switched on (the app's own calls only); a test credential's token lists the test enterprises that have the app installed, of any developer organisation, a live one real organisations only.

- 认证：`bearer`
- 范围：`app.installations:read`
- operationId：`listInstallations`

**响应**

| 状态 | 说明 |
|---|---|
| 200 | 企业 id |
| 401 | 见 [Unauthorized](#resp-unauthorized) |
| 403 | 见 [Forbidden](#resp-forbidden) |
| 429 | 见 [RateLimited](#resp-ratelimited) |
| 500 | 见 [Internal](#resp-internal) |
| 501 | `capability_off`：这个部署（或这个宿主）不提供这项能力，正文的 `capability` 是它的范围。见 [CapabilityOff](#resp-capabilityoff) |
| 503 | `unavailable`：暂时不可用（这个部署不提供的能力是 501 `capability_off`）。见 [Unavailable](#resp-unavailable) |

**200 响应正文**（`application/json`）

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `tenants` | string[] | 是 | 安装了本应用且已开启的企业 id |

## services

调用其他应用声明的服务（只收应用自己的令牌）

### `POST /services/{owner}/{name}/calls`

调用另一个应用声明的服务（排队，至少投递一次）

需要范围 `app.services:call`。只收应用自己的令牌；页面会话是 403 `forbidden`。English: starts one run of a service another app declares (the `call` trigger); the platform queues it and delivers it at least once.

`owner` 是声明服务的应用，`name` 是服务名。正文 `input` 是交给服务的 JSON，`idempotencyKey` 去重：同一个键就是同一次运行。第一次返回 202 且 `created` 为 true；这个键以前用过则 200，返回当时那次运行。

- 认证：`bearer`
- 范围：`app.services:call`
- operationId：`callService`

**参数**

| 名称 | 位置 | 类型 | 必填 | 说明 |
|---|---|---|---|---|
| `owner` | path | string | 是 | 声明服务的应用 id |
| `name` | path | string | 是 | 服务名 |
| `Idempotency-Key` | header | string |  | 每个 POST 都收。SDK 未传入时会生成 uuid v4。24 小时内同键同正文重放第一次的回答<br>最短 1，最长 255 |

**请求正文（必填）**（`application/json`）

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `input` | any |  | 交给服务的 JSON |
| `idempotencyKey` | string |  | 去重：同一个键就是同一次运行 |

**响应**

| 状态 | 说明 |
|---|---|
| 200 | 这个 idempotencyKey 以前用过：这是当时那次运行 |
| 202 | 新建了一次运行 |
| 401 | 见 [Unauthorized](#resp-unauthorized) |
| 403 | 见 [Forbidden](#resp-forbidden) |
| 404 | `not_found`：对应的记录不存在（客户中心没有这个客户，访客、服务或所指的东西不存在）。见 [NotFound](#resp-notfound) |
| 413 | `payload_too_large`：正文太大。见 [PayloadTooLarge](#resp-payloadtoolarge) |
| 422 | 字段或参数不被接受（正文读不出来、有未知字段、缺必填、取值越界）。见 [Unprocessable](#resp-unprocessable) |
| 429 | 见 [RateLimited](#resp-ratelimited) |
| 500 | 见 [Internal](#resp-internal) |
| 501 | `capability_off`：这个部署（或这个宿主）不提供这项能力，正文的 `capability` 是它的范围。见 [CapabilityOff](#resp-capabilityoff) |
| 503 | `unavailable`：暂时不可用（这个部署不提供的能力是 501 `capability_off`）。见 [Unavailable](#resp-unavailable) |

**200 响应正文**（`application/json`）

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `id` | string | 是 | 运行的 id |
| `status` | string | 是 | 运行的状态 |
| `created` | boolean | 是 | 这次调用新建了它 |

**202 响应正文**（`application/json`）

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `id` | string | 是 | 运行的 id |
| `status` | string | 是 | 运行的状态 |
| `created` | boolean | 是 | 这次调用新建了它 |

## events

发出本应用在清单里声明的开放事件

### `POST /events/emit`

发出本应用声明的一个开放事件

需要范围 `app.events:emit`。English: writes one open event of the app into the platform's outbox and starts its delivery.

`type` 必须是本应用在清单 `events.emits` 里声明的事件类型，并以 `{本应用 id}.` 开头，否则 403 `forbidden`。`key` 必填（最多 200 字符）：同一个键就是同一个事件。`customer` 是事件涉及的客户，`data` 是事件内容。返回 202。

**应用之间的事件**（opv2 J2 ②）：同一个事务里，事件也写进平台的发件箱，送给本租户里装了并开着、在清单 `services[]` 里声明了触发器 `event:{type}` 的应用（见 webhook `event:{app}.{what}`），至少一次，幂等键是 `appevt:{本应用 id}:{key}`（超过 128 字符时用摘要）。`type` 同时是平台登记的开放事件、且有 `customer` 时，才另外送给集成的 webhook。`data` 最多 16 KiB（JSON），超过返回 422；事件只用 id 指代人和记录，不放手机号、姓名。清单里声明了的类型只由声明它的应用发布：同名的审计动作不再作为事件送给服务。

- 认证：`bearer`
- 范围：`app.events:emit`
- operationId：`emitEvent`

**参数**

| 名称 | 位置 | 类型 | 必填 | 说明 |
|---|---|---|---|---|
| `Idempotency-Key` | header | string |  | 每个 POST 都收。SDK 未传入时会生成 uuid v4。24 小时内同键同正文重放第一次的回答<br>最短 1，最长 255 |

**请求正文（必填）**（`application/json`）

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `type` | string | 是 | 本应用在清单 events.emits 里声明的事件类型，以 {本应用 id}. 开头 |
| `key` | string | 是 | 必填，最多 200 字符。同一个键就是同一个事件 |
| `customer` | string |  | 事件涉及的客户 id |
| `data` | object |  | 事件内容，最多 16 KiB（JSON）；只用 id 指代人和记录 |

**响应**

| 状态 | 说明 |
|---|---|
| 202 | 已进入发件箱。`written` 为 false 表示没有写入新事件 |
| 401 | 见 [Unauthorized](#resp-unauthorized) |
| 403 | 见 [Forbidden](#resp-forbidden) |
| 413 | `payload_too_large`：正文太大。见 [PayloadTooLarge](#resp-payloadtoolarge) |
| 422 | 字段或参数不被接受（正文读不出来、有未知字段、缺必填、取值越界）。见 [Unprocessable](#resp-unprocessable) |
| 429 | 见 [RateLimited](#resp-ratelimited) |
| 500 | 见 [Internal](#resp-internal) |
| 501 | `capability_off`：这个部署（或这个宿主）不提供这项能力，正文的 `capability` 是它的范围。见 [CapabilityOff](#resp-capabilityoff) |
| 503 | `unavailable`：暂时不可用（这个部署不提供的能力是 501 `capability_off`）。见 [Unavailable](#resp-unavailable) |

**202 响应正文**（`application/json`）

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `key` | string | 是 | 发件箱键 |
| `written` | boolean | 是 | false 表示没有写入新事件 |

## links

其他应用的记录链接：搜索与解析

### `GET /links`

按当前人员的权限搜索另一个应用的记录链接

需要范围 `app.links:read`。English: searches the records of another app as the caller sees them.

`app` 是要搜的应用；省略时搜这个组织已安装并开启的全部应用（每个应用最多 `limit` 条，`installed` 为 true）。`q` 是文字查询（省略为最近的）。`limit` 默认 20，1 到 50，超出是 422。指定的应用在这个组织没有安装或没有开启时，`installed` 为 false 且 `items` 是 `[]`，选择器可以据此区分“没装”和“没找到”。English: `app` omitted searches every installed app.

- 认证：`bearer`
- 范围：`app.links:read`
- operationId：`searchLinks`

**参数**

| 名称 | 位置 | 类型 | 必填 | 说明 |
|---|---|---|---|---|
| `app` | query | string |  | 要搜索其记录的应用 id；省略为全部已安装的应用 |
| `q` | query | string |  | 文字查询，省略为最近的 |
| `limit` | query | integer |  | 条数，默认 20，最大 50<br>默认 `20`<br>最小 1，最大 50 |

**响应**

| 状态 | 说明 |
|---|---|
| 200 | 链接 |
| 401 | 见 [Unauthorized](#resp-unauthorized) |
| 403 | 见 [Forbidden](#resp-forbidden) |
| 422 | 字段或参数不被接受（正文读不出来、有未知字段、缺必填、取值越界）。见 [Unprocessable](#resp-unprocessable) |
| 429 | 见 [RateLimited](#resp-ratelimited) |
| 500 | 见 [Internal](#resp-internal) |
| 501 | `capability_off`：这个部署（或这个宿主）不提供这项能力，正文的 `capability` 是它的范围。见 [CapabilityOff](#resp-capabilityoff) |
| 503 | `unavailable`：暂时不可用（这个部署不提供的能力是 501 `capability_off`）。见 [Unavailable](#resp-unavailable) |

**200 响应正文**（`application/json`）

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `items` | Link[] | 是 |  |
| `items[].ref` | string | 是 | k3://应用/类型/id |
| `items[].title` | string | 是 |  |
| `items[].kind` | string | 是 | 类型的中文名，例如 会员、审批单、文件 |
| `items[].app` | string | 是 | 记录所属的应用 id |
| `items[].route` | string |  | 在所属应用里打开它的路由 |
| `items[].excerpt` | string |  | 文档正文开头约 200 字的纯文本（不含标题）。只有记录是文档的应用才给 |
| `items[].doc` | boolean |  | 记录是文档（页面、Word、Excel、PowerPoint 文件） |
| `items[].icon` | string |  | 人给记录选的图标（emoji、px:&lt;id>、med:&lt;id>） |
| `installed` | boolean | 是 | 该应用在这个组织是否已安装并开启 |

### `GET /links/resolve`

解析一个 k3:// 引用：它指向什么，当前人员能不能看到

需要范围 `app.links:read`。English: what a reference points at and whether the caller may see it.

`ref` 必填，必须是 `k3://` 引用，否则 422。不存在或看不到时 `found` 为 false，没有 `link`。

- 认证：`bearer`
- 范围：`app.links:read`
- operationId：`resolveLink`

**参数**

| 名称 | 位置 | 类型 | 必填 | 说明 |
|---|---|---|---|---|
| `ref` | query | string | 是 | k3://应用/类型/id |

**响应**

| 状态 | 说明 |
|---|---|
| 200 | 解析结果 |
| 401 | 见 [Unauthorized](#resp-unauthorized) |
| 403 | 见 [Forbidden](#resp-forbidden) |
| 422 | 字段或参数不被接受（正文读不出来、有未知字段、缺必填、取值越界）。见 [Unprocessable](#resp-unprocessable) |
| 429 | 见 [RateLimited](#resp-ratelimited) |
| 500 | 见 [Internal](#resp-internal) |
| 501 | `capability_off`：这个部署（或这个宿主）不提供这项能力，正文的 `capability` 是它的范围。见 [CapabilityOff](#resp-capabilityoff) |
| 503 | `unavailable`：暂时不可用（这个部署不提供的能力是 501 `capability_off`）。见 [Unavailable](#resp-unavailable) |

**200 响应正文**（`application/json`）

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `found` | boolean | 是 |  |
| `link` | Link |  | 另一个应用的一条记录 |
| `link.ref` | string | 是 | k3://应用/类型/id |
| `link.title` | string | 是 |  |
| `link.kind` | string | 是 | 类型的中文名，例如 会员、审批单、文件 |
| `link.app` | string | 是 | 记录所属的应用 id |
| `link.route` | string |  | 在所属应用里打开它的路由 |
| `link.excerpt` | string |  | 文档正文开头约 200 字的纯文本（不含标题）。只有记录是文档的应用才给 |
| `link.doc` | boolean |  | 记录是文档（页面、Word、Excel、PowerPoint 文件） |
| `link.icon` | string |  | 人给记录选的图标（emoji、px:&lt;id>、med:&lt;id>） |

### `GET /backlinks`

列出提到本应用某条记录的其他应用的记录

需要范围 `app.links:read`。English: the records of other apps that mention one of the calling app's own records, as the page session's person sees them.

`ref` 必填，必须是调用应用自己的记录（`k3://<应用>/…`）：不是 `k3://` 引用是 422，是别的应用的记录是 403（应用只能问谁提到了自己的记录）。各应用的提供者按当前人员的权限过滤，所以要用页面会话（有人）调用；应用自己的调用没有人，`items` 是 `[]`。调用应用自己的记录不在其中（应用自己列）。`limit` 默认 50，1 到 100，超出是 422。English: items exclude the calling app's own records; the app's own token (no person) gets `[]`.

- 认证：`bearer`
- 范围：`app.links:read`
- operationId：`listBacklinks`

**参数**

| 名称 | 位置 | 类型 | 必填 | 说明 |
|---|---|---|---|---|
| `ref` | query | string | 是 | 调用应用自己的记录，k3://应用/类型/id |
| `limit` | query | integer |  | 条数，默认 50，最大 100<br>默认 `50`<br>最小 1，最大 100 |

**响应**

| 状态 | 说明 |
|---|---|
| 200 | 提到它的记录 |
| 401 | 见 [Unauthorized](#resp-unauthorized) |
| 403 | 见 [Forbidden](#resp-forbidden) |
| 422 | 字段或参数不被接受（正文读不出来、有未知字段、缺必填、取值越界）。见 [Unprocessable](#resp-unprocessable) |
| 429 | 见 [RateLimited](#resp-ratelimited) |
| 500 | 见 [Internal](#resp-internal) |
| 501 | `capability_off`：这个部署（或这个宿主）不提供这项能力，正文的 `capability` 是它的范围。见 [CapabilityOff](#resp-capabilityoff) |
| 503 | `unavailable`：暂时不可用（这个部署不提供的能力是 501 `capability_off`）。见 [Unavailable](#resp-unavailable) |

**200 响应正文**（`application/json`）

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `items` | Backlink[] | 是 |  |
| `items[].ref` | string | 是 | 提到它的记录，k3://应用/类型/id |
| `items[].app` | string | 是 | 记录所属的应用 id |
| `items[].kind` | string | 是 | 类型的中文名，例如 文档、审批单 |
| `items[].title` | string | 是 |  |
| `items[].path` | string |  | 在所属应用里打开它的路径 |
| `items[].context` | string |  | 提到处前后的一句话 |
| `items[].updatedAt` | string(date-time) | 是 |  |

## entitlements

权益服务：模板、发放、预留与核销、退款、转让，以及其他应用声明的 granter

### `GET /entitlements/templates`

列出权益模板

需要范围 `app.entitlements:read`。English: the tenant's entitlement templates, optionally of one app or only the active ones.

`ownerApp` 筛归属的应用；`activeOnly` 为 true 时只留启用的。没有时 `data` 是 `[]`。

- 认证：`bearer`
- 范围：`app.entitlements:read`
- operationId：`listEntitlementTemplates`

**参数**

| 名称 | 位置 | 类型 | 必填 | 说明 |
|---|---|---|---|---|
| `ownerApp` | query | string |  | 只要这个应用的模板 |
| `activeOnly` | query | boolean |  | 只要启用的 |

**响应**

| 状态 | 说明 |
|---|---|
| 200 | 模板 |
| 401 | 见 [Unauthorized](#resp-unauthorized) |
| 403 | 见 [Forbidden](#resp-forbidden) |
| 422 | 字段或参数不被接受（正文读不出来、有未知字段、缺必填、取值越界）。见 [Unprocessable](#resp-unprocessable) |
| 429 | 见 [RateLimited](#resp-ratelimited) |
| 500 | 见 [Internal](#resp-internal) |
| 501 | `capability_off`：这个部署（或这个宿主）不提供这项能力，正文的 `capability` 是它的范围。见 [CapabilityOff](#resp-capabilityoff) |
| 503 | `unavailable`：暂时不可用（这个部署不提供的能力是 501 `capability_off`）。见 [Unavailable](#resp-unavailable) |

**200 响应正文**（`application/json`）

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `data` | EntitlementTemplate[] | 是 |  |
| `data[].id` | string | 是 | 模板 id |
| `data[].ownerApp` | string | 是 | 归属的应用 |
| `data[].kind` | string | 是 | 种类 |
| `data[].name` | string | 是 | 名称 |
| `data[].params` | EntitlementParams | 是 | 种类相关的参数（字段名是 snake_case）；哪个种类读哪些字段由服务在保存模板时检查 |
| `data[].params.total_times` | integer |  | 次数（count、count_period） |
| `data[].params.valid_days` | integer |  | 有效天数（period、count_period 必填） |
| `data[].params.valid_from` | string |  | "grant"（默认）或 "first_use" |
| `data[].params.daily_cap` | integer |  | 每日上限（期卡） |
| `data[].params.weekly_cap` | integer |  | 每周上限（期卡） |
| `data[].params.starts_on` | string |  | 固定有效期开始，YYYY-MM-DD（券；覆盖 valid_days） |
| `data[].params.ends_on` | string |  | 固定有效期结束，含当天 |
| `data[].params.form` | string |  | 券的形式：cash、percent、service、code |
| `data[].params.amount_off_cents` | integer |  | 满减金额（cash） |
| `data[].params.percent` | integer |  | 折扣：减多少个百分点，1 到 99（percent） |
| `data[].params.threshold_cents` | integer |  | 订单要达到的金额（cash、percent） |
| `data[].params.max_off_cents` | integer |  | 最多减多少，0 为不封顶（percent） |
| `data[].params.restore_on_refund` | boolean |  | 订单退款时把券还回去（由调用方读到后作废核销单） |
| `data[].params.description` | string |  | 奖品说明 |
| `data[].params.fulfilment` | string |  | 奖品如何交付（对平台不透明） |
| `data[].scope` | string[] | 是 | 范围标签 |
| `data[].transferable` | boolean | 是 | 可转让 |
| `data[].freezable` | boolean | 是 | 可冻结 |
| `data[].maxFreezeDays` | integer | 是 | 最多冻结天数 |
| `data[].active` | boolean | 是 | 启用 |
| `data[].createdAt` | string(date-time) | 是 | 建立时间 |
| `data[].updatedAt` | string(date-time) | 是 | 修改时间 |

### `POST /entitlements/templates`

建立一个权益模板（归属永远是调用的应用）

需要范围 `app.entitlements:write`。English: creates a template owned by the calling app.

`ownerApp` 可省略；写了别的应用是 403 `forbidden`。种类（`kind`）建立后不能改。带请求头 `K3-Dry-Run: 1` 只预演，什么也不写。

- 认证：`bearer`
- 范围：`app.entitlements:write`
- operationId：`createEntitlementTemplate`

**参数**

| 名称 | 位置 | 类型 | 必填 | 说明 |
|---|---|---|---|---|
| `K3-Dry-Run` | header | string |  | 传 1 时只预演：回答和真做一样，什么也不留下<br>可选值：`1` |
| `Idempotency-Key` | header | string |  | 每个 POST 都收。SDK 未传入时会生成 uuid v4。24 小时内同键同正文重放第一次的回答<br>最短 1，最长 255 |

**请求正文（必填）**（`application/json`）

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `ownerApp` | string |  | 可省略；写了就必须是调用的应用 |
| `kind` | string | 是 | 种类：count、period、count_period、voucher、prize。建立后不能改 |
| `name` | string | 是 | 名称 |
| `params` | EntitlementParams |  | 种类相关的参数（字段名是 snake_case）；哪个种类读哪些字段由服务在保存模板时检查 |
| `params.total_times` | integer |  | 次数（count、count_period） |
| `params.valid_days` | integer |  | 有效天数（period、count_period 必填） |
| `params.valid_from` | string |  | "grant"（默认）或 "first_use" |
| `params.daily_cap` | integer |  | 每日上限（期卡） |
| `params.weekly_cap` | integer |  | 每周上限（期卡） |
| `params.starts_on` | string |  | 固定有效期开始，YYYY-MM-DD（券；覆盖 valid_days） |
| `params.ends_on` | string |  | 固定有效期结束，含当天 |
| `params.form` | string |  | 券的形式：cash、percent、service、code |
| `params.amount_off_cents` | integer |  | 满减金额（cash） |
| `params.percent` | integer |  | 折扣：减多少个百分点，1 到 99（percent） |
| `params.threshold_cents` | integer |  | 订单要达到的金额（cash、percent） |
| `params.max_off_cents` | integer |  | 最多减多少，0 为不封顶（percent） |
| `params.restore_on_refund` | boolean |  | 订单退款时把券还回去（由调用方读到后作废核销单） |
| `params.description` | string |  | 奖品说明 |
| `params.fulfilment` | string |  | 奖品如何交付（对平台不透明） |
| `scope` | string[] |  | 范围标签（不透明） |
| `transferable` | boolean |  | 可转让 |
| `freezable` | boolean |  | 可冻结 |
| `maxFreezeDays` | integer |  | 最多冻结天数 |
| `active` | boolean |  | 省略：建立时为启用，修改时不变 |

**响应**

| 状态 | 说明 |
|---|---|
| 201 | 已建立 |
| 401 | 见 [Unauthorized](#resp-unauthorized) |
| 403 | 见 [Forbidden](#resp-forbidden) |
| 413 | `payload_too_large`：正文太大。见 [PayloadTooLarge](#resp-payloadtoolarge) |
| 422 | 服务拒绝，或字段不被接受。见 [Refused](#resp-refused) |
| 429 | 见 [RateLimited](#resp-ratelimited) |
| 500 | 见 [Internal](#resp-internal) |
| 501 | `capability_off`：这个部署（或这个宿主）不提供这项能力，正文的 `capability` 是它的范围。见 [CapabilityOff](#resp-capabilityoff) |
| 503 | `unavailable`：暂时不可用（这个部署不提供的能力是 501 `capability_off`）。见 [Unavailable](#resp-unavailable) |

**201 响应正文**（`application/json`）

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `id` | string | 是 | 模板 id |
| `ownerApp` | string | 是 | 归属的应用 |
| `kind` | string | 是 | 种类 |
| `name` | string | 是 | 名称 |
| `params` | EntitlementParams | 是 | 种类相关的参数（字段名是 snake_case）；哪个种类读哪些字段由服务在保存模板时检查 |
| `params.total_times` | integer |  | 次数（count、count_period） |
| `params.valid_days` | integer |  | 有效天数（period、count_period 必填） |
| `params.valid_from` | string |  | "grant"（默认）或 "first_use" |
| `params.daily_cap` | integer |  | 每日上限（期卡） |
| `params.weekly_cap` | integer |  | 每周上限（期卡） |
| `params.starts_on` | string |  | 固定有效期开始，YYYY-MM-DD（券；覆盖 valid_days） |
| `params.ends_on` | string |  | 固定有效期结束，含当天 |
| `params.form` | string |  | 券的形式：cash、percent、service、code |
| `params.amount_off_cents` | integer |  | 满减金额（cash） |
| `params.percent` | integer |  | 折扣：减多少个百分点，1 到 99（percent） |
| `params.threshold_cents` | integer |  | 订单要达到的金额（cash、percent） |
| `params.max_off_cents` | integer |  | 最多减多少，0 为不封顶（percent） |
| `params.restore_on_refund` | boolean |  | 订单退款时把券还回去（由调用方读到后作废核销单） |
| `params.description` | string |  | 奖品说明 |
| `params.fulfilment` | string |  | 奖品如何交付（对平台不透明） |
| `scope` | string[] | 是 | 范围标签 |
| `transferable` | boolean | 是 | 可转让 |
| `freezable` | boolean | 是 | 可冻结 |
| `maxFreezeDays` | integer | 是 | 最多冻结天数 |
| `active` | boolean | 是 | 启用 |
| `createdAt` | string(date-time) | 是 | 建立时间 |
| `updatedAt` | string(date-time) | 是 | 修改时间 |

### `GET /entitlements/templates/{id}`

读一个权益模板

需要范围 `app.entitlements:read`。English: one template of the tenant.

- 认证：`bearer`
- 范围：`app.entitlements:read`
- operationId：`getEntitlementTemplate`

**参数**

| 名称 | 位置 | 类型 | 必填 | 说明 |
|---|---|---|---|---|
| `id` | path | string | 是 | id |

**响应**

| 状态 | 说明 |
|---|---|
| 200 | 模板 |
| 401 | 见 [Unauthorized](#resp-unauthorized) |
| 403 | 见 [Forbidden](#resp-forbidden) |
| 404 | `not_found`：对应的记录不存在（客户中心没有这个客户，访客、服务或所指的东西不存在）。见 [NotFound](#resp-notfound) |
| 422 | 服务拒绝，或字段不被接受。见 [Refused](#resp-refused) |
| 429 | 见 [RateLimited](#resp-ratelimited) |
| 500 | 见 [Internal](#resp-internal) |
| 501 | `capability_off`：这个部署（或这个宿主）不提供这项能力，正文的 `capability` 是它的范围。见 [CapabilityOff](#resp-capabilityoff) |
| 503 | `unavailable`：暂时不可用（这个部署不提供的能力是 501 `capability_off`）。见 [Unavailable](#resp-unavailable) |

**200 响应正文**（`application/json`）

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `id` | string | 是 | 模板 id |
| `ownerApp` | string | 是 | 归属的应用 |
| `kind` | string | 是 | 种类 |
| `name` | string | 是 | 名称 |
| `params` | EntitlementParams | 是 | 种类相关的参数（字段名是 snake_case）；哪个种类读哪些字段由服务在保存模板时检查 |
| `params.total_times` | integer |  | 次数（count、count_period） |
| `params.valid_days` | integer |  | 有效天数（period、count_period 必填） |
| `params.valid_from` | string |  | "grant"（默认）或 "first_use" |
| `params.daily_cap` | integer |  | 每日上限（期卡） |
| `params.weekly_cap` | integer |  | 每周上限（期卡） |
| `params.starts_on` | string |  | 固定有效期开始，YYYY-MM-DD（券；覆盖 valid_days） |
| `params.ends_on` | string |  | 固定有效期结束，含当天 |
| `params.form` | string |  | 券的形式：cash、percent、service、code |
| `params.amount_off_cents` | integer |  | 满减金额（cash） |
| `params.percent` | integer |  | 折扣：减多少个百分点，1 到 99（percent） |
| `params.threshold_cents` | integer |  | 订单要达到的金额（cash、percent） |
| `params.max_off_cents` | integer |  | 最多减多少，0 为不封顶（percent） |
| `params.restore_on_refund` | boolean |  | 订单退款时把券还回去（由调用方读到后作废核销单） |
| `params.description` | string |  | 奖品说明 |
| `params.fulfilment` | string |  | 奖品如何交付（对平台不透明） |
| `scope` | string[] | 是 | 范围标签 |
| `transferable` | boolean | 是 | 可转让 |
| `freezable` | boolean | 是 | 可冻结 |
| `maxFreezeDays` | integer | 是 | 最多冻结天数 |
| `active` | boolean | 是 | 启用 |
| `createdAt` | string(date-time) | 是 | 建立时间 |
| `updatedAt` | string(date-time) | 是 | 修改时间 |

### `PUT /entitlements/templates/{id}`

修改本应用的权益模板

需要范围 `app.entitlements:write`。English: changes a template of the calling app.

已发出的权益不受影响（权益是发放时的副本）。别的应用的模板是 403 `forbidden`。种类建立后不能改。带 `K3-Dry-Run: 1` 只预演。

- 认证：`bearer`
- 范围：`app.entitlements:write`
- operationId：`updateEntitlementTemplate`

**参数**

| 名称 | 位置 | 类型 | 必填 | 说明 |
|---|---|---|---|---|
| `id` | path | string | 是 | id |
| `K3-Dry-Run` | header | string |  | 传 1 时只预演：回答和真做一样，什么也不留下<br>可选值：`1` |

**请求正文（必填）**（`application/json`）

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `ownerApp` | string |  | 可省略；写了就必须是调用的应用 |
| `kind` | string | 是 | 种类：count、period、count_period、voucher、prize。建立后不能改 |
| `name` | string | 是 | 名称 |
| `params` | EntitlementParams |  | 种类相关的参数（字段名是 snake_case）；哪个种类读哪些字段由服务在保存模板时检查 |
| `params.total_times` | integer |  | 次数（count、count_period） |
| `params.valid_days` | integer |  | 有效天数（period、count_period 必填） |
| `params.valid_from` | string |  | "grant"（默认）或 "first_use" |
| `params.daily_cap` | integer |  | 每日上限（期卡） |
| `params.weekly_cap` | integer |  | 每周上限（期卡） |
| `params.starts_on` | string |  | 固定有效期开始，YYYY-MM-DD（券；覆盖 valid_days） |
| `params.ends_on` | string |  | 固定有效期结束，含当天 |
| `params.form` | string |  | 券的形式：cash、percent、service、code |
| `params.amount_off_cents` | integer |  | 满减金额（cash） |
| `params.percent` | integer |  | 折扣：减多少个百分点，1 到 99（percent） |
| `params.threshold_cents` | integer |  | 订单要达到的金额（cash、percent） |
| `params.max_off_cents` | integer |  | 最多减多少，0 为不封顶（percent） |
| `params.restore_on_refund` | boolean |  | 订单退款时把券还回去（由调用方读到后作废核销单） |
| `params.description` | string |  | 奖品说明 |
| `params.fulfilment` | string |  | 奖品如何交付（对平台不透明） |
| `scope` | string[] |  | 范围标签（不透明） |
| `transferable` | boolean |  | 可转让 |
| `freezable` | boolean |  | 可冻结 |
| `maxFreezeDays` | integer |  | 最多冻结天数 |
| `active` | boolean |  | 省略：建立时为启用，修改时不变 |

**响应**

| 状态 | 说明 |
|---|---|
| 200 | 修改后的模板 |
| 401 | 见 [Unauthorized](#resp-unauthorized) |
| 403 | 见 [Forbidden](#resp-forbidden) |
| 404 | `not_found`：对应的记录不存在（客户中心没有这个客户，访客、服务或所指的东西不存在）。见 [NotFound](#resp-notfound) |
| 413 | `payload_too_large`：正文太大。见 [PayloadTooLarge](#resp-payloadtoolarge) |
| 422 | 服务拒绝，或字段不被接受。见 [Refused](#resp-refused) |
| 429 | 见 [RateLimited](#resp-ratelimited) |
| 500 | 见 [Internal](#resp-internal) |
| 501 | `capability_off`：这个部署（或这个宿主）不提供这项能力，正文的 `capability` 是它的范围。见 [CapabilityOff](#resp-capabilityoff) |
| 503 | `unavailable`：暂时不可用（这个部署不提供的能力是 501 `capability_off`）。见 [Unavailable](#resp-unavailable) |

**200 响应正文**（`application/json`）

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `id` | string | 是 | 模板 id |
| `ownerApp` | string | 是 | 归属的应用 |
| `kind` | string | 是 | 种类 |
| `name` | string | 是 | 名称 |
| `params` | EntitlementParams | 是 | 种类相关的参数（字段名是 snake_case）；哪个种类读哪些字段由服务在保存模板时检查 |
| `params.total_times` | integer |  | 次数（count、count_period） |
| `params.valid_days` | integer |  | 有效天数（period、count_period 必填） |
| `params.valid_from` | string |  | "grant"（默认）或 "first_use" |
| `params.daily_cap` | integer |  | 每日上限（期卡） |
| `params.weekly_cap` | integer |  | 每周上限（期卡） |
| `params.starts_on` | string |  | 固定有效期开始，YYYY-MM-DD（券；覆盖 valid_days） |
| `params.ends_on` | string |  | 固定有效期结束，含当天 |
| `params.form` | string |  | 券的形式：cash、percent、service、code |
| `params.amount_off_cents` | integer |  | 满减金额（cash） |
| `params.percent` | integer |  | 折扣：减多少个百分点，1 到 99（percent） |
| `params.threshold_cents` | integer |  | 订单要达到的金额（cash、percent） |
| `params.max_off_cents` | integer |  | 最多减多少，0 为不封顶（percent） |
| `params.restore_on_refund` | boolean |  | 订单退款时把券还回去（由调用方读到后作废核销单） |
| `params.description` | string |  | 奖品说明 |
| `params.fulfilment` | string |  | 奖品如何交付（对平台不透明） |
| `scope` | string[] | 是 | 范围标签 |
| `transferable` | boolean | 是 | 可转让 |
| `freezable` | boolean | 是 | 可冻结 |
| `maxFreezeDays` | integer | 是 | 最多冻结天数 |
| `active` | boolean | 是 | 启用 |
| `createdAt` | string(date-time) | 是 | 建立时间 |
| `updatedAt` | string(date-time) | 是 | 修改时间 |

### `GET /entitlements/grants`

列出一位客户的权益（新的在前）

需要范围 `app.entitlements:read`。English: a holder's grants, newest first.

holder 必填。status 可选：active、not_started、frozen、expired、used_up、used、refunded、voided、transferred（按今天计算）。正文是 {data: [...]}，不分页。

- 认证：`bearer`
- 范围：`app.entitlements:read`
- operationId：`listEntitlementGrants`

**参数**

| 名称 | 位置 | 类型 | 必填 | 说明 |
|---|---|---|---|---|
| `holder` | query | string | 是 | 客户 id（cu_…） |
| `status` | query | string |  | 只要这个状态的权益 |

**响应**

| 状态 | 说明 |
|---|---|
| 200 | 权益 |
| 401 | 见 [Unauthorized](#resp-unauthorized) |
| 403 | 见 [Forbidden](#resp-forbidden) |
| 422 | 服务拒绝，或字段不被接受。见 [Refused](#resp-refused) |
| 429 | 见 [RateLimited](#resp-ratelimited) |
| 500 | 见 [Internal](#resp-internal) |
| 501 | `capability_off`：这个部署（或这个宿主）不提供这项能力，正文的 `capability` 是它的范围。见 [CapabilityOff](#resp-capabilityoff) |
| 503 | `unavailable`：暂时不可用（这个部署不提供的能力是 501 `capability_off`）。见 [Unavailable](#resp-unavailable) |

**200 响应正文**（`application/json`）

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `data` | EntitlementGrant[] | 是 |  |
| `data[].id` | string | 是 | 权益 id |
| `data[].template` | string | 是 | 来源模板 id |
| `data[].ownerApp` | string | 是 | 模板归属的应用 |
| `data[].kind` | string | 是 | 种类 |
| `data[].name` | string | 是 | 名称 |
| `data[].params` | EntitlementParams | 是 | 种类相关的参数（字段名是 snake_case）；哪个种类读哪些字段由服务在保存模板时检查 |
| `data[].params.total_times` | integer |  | 次数（count、count_period） |
| `data[].params.valid_days` | integer |  | 有效天数（period、count_period 必填） |
| `data[].params.valid_from` | string |  | "grant"（默认）或 "first_use" |
| `data[].params.daily_cap` | integer |  | 每日上限（期卡） |
| `data[].params.weekly_cap` | integer |  | 每周上限（期卡） |
| `data[].params.starts_on` | string |  | 固定有效期开始，YYYY-MM-DD（券；覆盖 valid_days） |
| `data[].params.ends_on` | string |  | 固定有效期结束，含当天 |
| `data[].params.form` | string |  | 券的形式：cash、percent、service、code |
| `data[].params.amount_off_cents` | integer |  | 满减金额（cash） |
| `data[].params.percent` | integer |  | 折扣：减多少个百分点，1 到 99（percent） |
| `data[].params.threshold_cents` | integer |  | 订单要达到的金额（cash、percent） |
| `data[].params.max_off_cents` | integer |  | 最多减多少，0 为不封顶（percent） |
| `data[].params.restore_on_refund` | boolean |  | 订单退款时把券还回去（由调用方读到后作废核销单） |
| `data[].params.description` | string |  | 奖品说明 |
| `data[].params.fulfilment` | string |  | 奖品如何交付（对平台不透明） |
| `data[].scope` | string[] | 是 | 范围标签 |
| `data[].transferable` | boolean | 是 | 可转让 |
| `data[].freezable` | boolean | 是 | 可冻结 |
| `data[].maxFreezeDays` | integer | 是 | 最多冻结天数 |
| `data[].holder` | string | 是 | 持有的客户 id |
| `data[].total` | integer | 是 | 总次数 |
| `data[].remaining` | integer | 是 | 剩余次数 |
| `data[].startsOn` | string | 是 | 开始日期 |
| `data[].endsOn` | string | 是 | 结束日期 |
| `data[].state` | string | 是 | 对它做过什么 |
| `data[].status` | string | 是 | 对人说的状态：active、not_started、frozen、expired、used_up、used、refunded、voided、transferred（按今天算） |
| `data[].frozenFrom` | string |  | 冻结开始 |
| `data[].frozenUntil` | string |  | 冻结到 |
| `data[].frozenDaysUsed` | integer | 是 | 已冻结天数 |
| `data[].source` | string | 是 | 来源 |
| `data[].paidCents` | integer | 是 | 付款 |
| `data[].refundedCents` | integer | 是 | 已退 |
| `data[].origin` | string |  | k3:// 引用 |
| `data[].attribution` | object |  | 归因 |
| `data[].code` | string |  | 券码 |
| `data[].transferredFrom` | string |  | 转让自哪一项 |
| `data[].held` | EntitlementHold |  | 一个未支付订单对权益的占用 |
| `data[].held.id` | string | 是 | 占用 id |
| `data[].held.grant` | string | 是 | 权益 id |
| `data[].held.ref` | string | 是 | 占用它的订单 |
| `data[].held.expiresAt` | string(date-time) | 是 | 占用到何时 |
| `data[].createdBy` | string | 是 | 发放者 |
| `data[].createdAt` | string(date-time) | 是 | 发放时间 |
| `data[].ledger` | EntitlementLedgerEntry[] |  | 只在读单项权益时有 |
| `data[].ledger[].id` | integer | 是 | 流水 id |
| `data[].ledger[].kind` | string | 是 | 变动的种类 |
| `data[].ledger[].delta` | integer | 是 | 次数的增减 |
| `data[].ledger[].balance` | integer | 是 | 变动后的余额 |
| `data[].ledger[].actorName` | string | 是 | 谁做的 |
| `data[].ledger[].reason` | string | 是 | 原因 |
| `data[].ledger[].ref` | string |  | 关联的订单或预约 |
| `data[].ledger[].redemptionId` | string |  | 关联的核销单 |
| `data[].ledger[].at` | string(date-time) | 是 | 时间 |

### `POST /entitlements/grants`

按本应用的模板向客户发放权益（同一个 key 只发一次）

需要范围 `app.entitlements:write`。English: issues `count` grants of one of the app's own templates to a customer, once per key.

`key` 在正文里；正文没写时用请求头 `Idempotency-Key`。同键重来返回 200 且 `replay` 为 true，不再发放；新发放返回 201。模板不是本应用的是 403 `forbidden`。`count` 为 1 到 1000，默认 1；`code` 只用于 `count` 为 1 的券或奖品。

- 认证：`bearer`
- 范围：`app.entitlements:write`
- operationId：`grantEntitlement`

**参数**

| 名称 | 位置 | 类型 | 必填 | 说明 |
|---|---|---|---|---|
| `K3-Dry-Run` | header | string |  | 传 1 时只预演：回答和真做一样，什么也不留下<br>可选值：`1` |
| `Idempotency-Key` | header | string |  | 每个 POST 都收。SDK 未传入时会生成 uuid v4。24 小时内同键同正文重放第一次的回答<br>最短 1，最长 255 |

**请求正文（必填）**（`application/json`）

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `key` | string |  | 幂等键（“来源应用:来源引用:种类”）；省略时用请求头 Idempotency-Key |
| `template` | string | 是 | 模板 id（必须是本应用的） |
| `holder` | string | 是 | 持有的客户 id（cu_…） |
| `source` | string |  | 来源：purchase、gift、claim、draw、import、transfer。默认 purchase（付款为 0 且没写时为 gift） |
| `origin` | string |  | k3:// 引用：谁发的、为什么 |
| `paidCents` | integer |  | 持有人为每一项付的金额 |
| `attribution` | object |  | 来源归因 |
| `count` | integer |  | 发几项，1 到 1000，默认 1<br>最小 1，最大 1000 |
| `code` | string |  | 给这一项用的券码，而不是生成的；只在 count 为 1 的券或奖品，租户内唯一 |

**响应**

| 状态 | 说明 |
|---|---|
| 200 | 同一个 key 已发放过：这是当时的权益（replay 为 true） |
| 201 | 已发放 |
| 401 | 见 [Unauthorized](#resp-unauthorized) |
| 403 | 见 [Forbidden](#resp-forbidden) |
| 404 | `not_found`：对应的记录不存在（客户中心没有这个客户，访客、服务或所指的东西不存在）。见 [NotFound](#resp-notfound) |
| 413 | `payload_too_large`：正文太大。见 [PayloadTooLarge](#resp-payloadtoolarge) |
| 422 | 服务拒绝，或字段不被接受。见 [Refused](#resp-refused) |
| 429 | 见 [RateLimited](#resp-ratelimited) |
| 500 | 见 [Internal](#resp-internal) |
| 501 | `capability_off`：这个部署（或这个宿主）不提供这项能力，正文的 `capability` 是它的范围。见 [CapabilityOff](#resp-capabilityoff) |
| 503 | `unavailable`：暂时不可用（这个部署不提供的能力是 501 `capability_off`）。见 [Unavailable](#resp-unavailable) |

**200 响应正文**（`application/json`）

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `grants` | EntitlementGrant[] | 是 |  |
| `grants[].id` | string | 是 | 权益 id |
| `grants[].template` | string | 是 | 来源模板 id |
| `grants[].ownerApp` | string | 是 | 模板归属的应用 |
| `grants[].kind` | string | 是 | 种类 |
| `grants[].name` | string | 是 | 名称 |
| `grants[].params` | EntitlementParams | 是 | 种类相关的参数（字段名是 snake_case）；哪个种类读哪些字段由服务在保存模板时检查 |
| `grants[].params.total_times` | integer |  | 次数（count、count_period） |
| `grants[].params.valid_days` | integer |  | 有效天数（period、count_period 必填） |
| `grants[].params.valid_from` | string |  | "grant"（默认）或 "first_use" |
| `grants[].params.daily_cap` | integer |  | 每日上限（期卡） |
| `grants[].params.weekly_cap` | integer |  | 每周上限（期卡） |
| `grants[].params.starts_on` | string |  | 固定有效期开始，YYYY-MM-DD（券；覆盖 valid_days） |
| `grants[].params.ends_on` | string |  | 固定有效期结束，含当天 |
| `grants[].params.form` | string |  | 券的形式：cash、percent、service、code |
| `grants[].params.amount_off_cents` | integer |  | 满减金额（cash） |
| `grants[].params.percent` | integer |  | 折扣：减多少个百分点，1 到 99（percent） |
| `grants[].params.threshold_cents` | integer |  | 订单要达到的金额（cash、percent） |
| `grants[].params.max_off_cents` | integer |  | 最多减多少，0 为不封顶（percent） |
| `grants[].params.restore_on_refund` | boolean |  | 订单退款时把券还回去（由调用方读到后作废核销单） |
| `grants[].params.description` | string |  | 奖品说明 |
| `grants[].params.fulfilment` | string |  | 奖品如何交付（对平台不透明） |
| `grants[].scope` | string[] | 是 | 范围标签 |
| `grants[].transferable` | boolean | 是 | 可转让 |
| `grants[].freezable` | boolean | 是 | 可冻结 |
| `grants[].maxFreezeDays` | integer | 是 | 最多冻结天数 |
| `grants[].holder` | string | 是 | 持有的客户 id |
| `grants[].total` | integer | 是 | 总次数 |
| `grants[].remaining` | integer | 是 | 剩余次数 |
| `grants[].startsOn` | string | 是 | 开始日期 |
| `grants[].endsOn` | string | 是 | 结束日期 |
| `grants[].state` | string | 是 | 对它做过什么 |
| `grants[].status` | string | 是 | 对人说的状态：active、not_started、frozen、expired、used_up、used、refunded、voided、transferred（按今天算） |
| `grants[].frozenFrom` | string |  | 冻结开始 |
| `grants[].frozenUntil` | string |  | 冻结到 |
| `grants[].frozenDaysUsed` | integer | 是 | 已冻结天数 |
| `grants[].source` | string | 是 | 来源 |
| `grants[].paidCents` | integer | 是 | 付款 |
| `grants[].refundedCents` | integer | 是 | 已退 |
| `grants[].origin` | string |  | k3:// 引用 |
| `grants[].attribution` | object |  | 归因 |
| `grants[].code` | string |  | 券码 |
| `grants[].transferredFrom` | string |  | 转让自哪一项 |
| `grants[].held` | EntitlementHold |  | 一个未支付订单对权益的占用 |
| `grants[].held.id` | string | 是 | 占用 id |
| `grants[].held.grant` | string | 是 | 权益 id |
| `grants[].held.ref` | string | 是 | 占用它的订单 |
| `grants[].held.expiresAt` | string(date-time) | 是 | 占用到何时 |
| `grants[].createdBy` | string | 是 | 发放者 |
| `grants[].createdAt` | string(date-time) | 是 | 发放时间 |
| `grants[].ledger` | EntitlementLedgerEntry[] |  | 只在读单项权益时有 |
| `grants[].ledger[].id` | integer | 是 | 流水 id |
| `grants[].ledger[].kind` | string | 是 | 变动的种类 |
| `grants[].ledger[].delta` | integer | 是 | 次数的增减 |
| `grants[].ledger[].balance` | integer | 是 | 变动后的余额 |
| `grants[].ledger[].actorName` | string | 是 | 谁做的 |
| `grants[].ledger[].reason` | string | 是 | 原因 |
| `grants[].ledger[].ref` | string |  | 关联的订单或预约 |
| `grants[].ledger[].redemptionId` | string |  | 关联的核销单 |
| `grants[].ledger[].at` | string(date-time) | 是 | 时间 |
| `replay` | boolean | 是 | 同一个 key 已发放过，这是当时的权益 |

**201 响应正文**（`application/json`）

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `grants` | EntitlementGrant[] | 是 |  |
| `grants[].id` | string | 是 | 权益 id |
| `grants[].template` | string | 是 | 来源模板 id |
| `grants[].ownerApp` | string | 是 | 模板归属的应用 |
| `grants[].kind` | string | 是 | 种类 |
| `grants[].name` | string | 是 | 名称 |
| `grants[].params` | EntitlementParams | 是 | 种类相关的参数（字段名是 snake_case）；哪个种类读哪些字段由服务在保存模板时检查 |
| `grants[].params.total_times` | integer |  | 次数（count、count_period） |
| `grants[].params.valid_days` | integer |  | 有效天数（period、count_period 必填） |
| `grants[].params.valid_from` | string |  | "grant"（默认）或 "first_use" |
| `grants[].params.daily_cap` | integer |  | 每日上限（期卡） |
| `grants[].params.weekly_cap` | integer |  | 每周上限（期卡） |
| `grants[].params.starts_on` | string |  | 固定有效期开始，YYYY-MM-DD（券；覆盖 valid_days） |
| `grants[].params.ends_on` | string |  | 固定有效期结束，含当天 |
| `grants[].params.form` | string |  | 券的形式：cash、percent、service、code |
| `grants[].params.amount_off_cents` | integer |  | 满减金额（cash） |
| `grants[].params.percent` | integer |  | 折扣：减多少个百分点，1 到 99（percent） |
| `grants[].params.threshold_cents` | integer |  | 订单要达到的金额（cash、percent） |
| `grants[].params.max_off_cents` | integer |  | 最多减多少，0 为不封顶（percent） |
| `grants[].params.restore_on_refund` | boolean |  | 订单退款时把券还回去（由调用方读到后作废核销单） |
| `grants[].params.description` | string |  | 奖品说明 |
| `grants[].params.fulfilment` | string |  | 奖品如何交付（对平台不透明） |
| `grants[].scope` | string[] | 是 | 范围标签 |
| `grants[].transferable` | boolean | 是 | 可转让 |
| `grants[].freezable` | boolean | 是 | 可冻结 |
| `grants[].maxFreezeDays` | integer | 是 | 最多冻结天数 |
| `grants[].holder` | string | 是 | 持有的客户 id |
| `grants[].total` | integer | 是 | 总次数 |
| `grants[].remaining` | integer | 是 | 剩余次数 |
| `grants[].startsOn` | string | 是 | 开始日期 |
| `grants[].endsOn` | string | 是 | 结束日期 |
| `grants[].state` | string | 是 | 对它做过什么 |
| `grants[].status` | string | 是 | 对人说的状态：active、not_started、frozen、expired、used_up、used、refunded、voided、transferred（按今天算） |
| `grants[].frozenFrom` | string |  | 冻结开始 |
| `grants[].frozenUntil` | string |  | 冻结到 |
| `grants[].frozenDaysUsed` | integer | 是 | 已冻结天数 |
| `grants[].source` | string | 是 | 来源 |
| `grants[].paidCents` | integer | 是 | 付款 |
| `grants[].refundedCents` | integer | 是 | 已退 |
| `grants[].origin` | string |  | k3:// 引用 |
| `grants[].attribution` | object |  | 归因 |
| `grants[].code` | string |  | 券码 |
| `grants[].transferredFrom` | string |  | 转让自哪一项 |
| `grants[].held` | EntitlementHold |  | 一个未支付订单对权益的占用 |
| `grants[].held.id` | string | 是 | 占用 id |
| `grants[].held.grant` | string | 是 | 权益 id |
| `grants[].held.ref` | string | 是 | 占用它的订单 |
| `grants[].held.expiresAt` | string(date-time) | 是 | 占用到何时 |
| `grants[].createdBy` | string | 是 | 发放者 |
| `grants[].createdAt` | string(date-time) | 是 | 发放时间 |
| `grants[].ledger` | EntitlementLedgerEntry[] |  | 只在读单项权益时有 |
| `grants[].ledger[].id` | integer | 是 | 流水 id |
| `grants[].ledger[].kind` | string | 是 | 变动的种类 |
| `grants[].ledger[].delta` | integer | 是 | 次数的增减 |
| `grants[].ledger[].balance` | integer | 是 | 变动后的余额 |
| `grants[].ledger[].actorName` | string | 是 | 谁做的 |
| `grants[].ledger[].reason` | string | 是 | 原因 |
| `grants[].ledger[].ref` | string |  | 关联的订单或预约 |
| `grants[].ledger[].redemptionId` | string |  | 关联的核销单 |
| `grants[].ledger[].at` | string(date-time) | 是 | 时间 |
| `replay` | boolean | 是 | 同一个 key 已发放过，这是当时的权益 |

### `GET /entitlements/grants/{id}`

读一项权益和它的流水

需要范围 `app.entitlements:read`。English: one grant with its ledger.

- 认证：`bearer`
- 范围：`app.entitlements:read`
- operationId：`getEntitlementGrant`

**参数**

| 名称 | 位置 | 类型 | 必填 | 说明 |
|---|---|---|---|---|
| `id` | path | string | 是 | id |

**响应**

| 状态 | 说明 |
|---|---|
| 200 | 权益，带 ledger |
| 401 | 见 [Unauthorized](#resp-unauthorized) |
| 403 | 见 [Forbidden](#resp-forbidden) |
| 404 | `not_found`：对应的记录不存在（客户中心没有这个客户，访客、服务或所指的东西不存在）。见 [NotFound](#resp-notfound) |
| 422 | 服务拒绝，或字段不被接受。见 [Refused](#resp-refused) |
| 429 | 见 [RateLimited](#resp-ratelimited) |
| 500 | 见 [Internal](#resp-internal) |
| 501 | `capability_off`：这个部署（或这个宿主）不提供这项能力，正文的 `capability` 是它的范围。见 [CapabilityOff](#resp-capabilityoff) |
| 503 | `unavailable`：暂时不可用（这个部署不提供的能力是 501 `capability_off`）。见 [Unavailable](#resp-unavailable) |

**200 响应正文**（`application/json`）

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `id` | string | 是 | 权益 id |
| `template` | string | 是 | 来源模板 id |
| `ownerApp` | string | 是 | 模板归属的应用 |
| `kind` | string | 是 | 种类 |
| `name` | string | 是 | 名称 |
| `params` | EntitlementParams | 是 | 种类相关的参数（字段名是 snake_case）；哪个种类读哪些字段由服务在保存模板时检查 |
| `params.total_times` | integer |  | 次数（count、count_period） |
| `params.valid_days` | integer |  | 有效天数（period、count_period 必填） |
| `params.valid_from` | string |  | "grant"（默认）或 "first_use" |
| `params.daily_cap` | integer |  | 每日上限（期卡） |
| `params.weekly_cap` | integer |  | 每周上限（期卡） |
| `params.starts_on` | string |  | 固定有效期开始，YYYY-MM-DD（券；覆盖 valid_days） |
| `params.ends_on` | string |  | 固定有效期结束，含当天 |
| `params.form` | string |  | 券的形式：cash、percent、service、code |
| `params.amount_off_cents` | integer |  | 满减金额（cash） |
| `params.percent` | integer |  | 折扣：减多少个百分点，1 到 99（percent） |
| `params.threshold_cents` | integer |  | 订单要达到的金额（cash、percent） |
| `params.max_off_cents` | integer |  | 最多减多少，0 为不封顶（percent） |
| `params.restore_on_refund` | boolean |  | 订单退款时把券还回去（由调用方读到后作废核销单） |
| `params.description` | string |  | 奖品说明 |
| `params.fulfilment` | string |  | 奖品如何交付（对平台不透明） |
| `scope` | string[] | 是 | 范围标签 |
| `transferable` | boolean | 是 | 可转让 |
| `freezable` | boolean | 是 | 可冻结 |
| `maxFreezeDays` | integer | 是 | 最多冻结天数 |
| `holder` | string | 是 | 持有的客户 id |
| `total` | integer | 是 | 总次数 |
| `remaining` | integer | 是 | 剩余次数 |
| `startsOn` | string | 是 | 开始日期 |
| `endsOn` | string | 是 | 结束日期 |
| `state` | string | 是 | 对它做过什么 |
| `status` | string | 是 | 对人说的状态：active、not_started、frozen、expired、used_up、used、refunded、voided、transferred（按今天算） |
| `frozenFrom` | string |  | 冻结开始 |
| `frozenUntil` | string |  | 冻结到 |
| `frozenDaysUsed` | integer | 是 | 已冻结天数 |
| `source` | string | 是 | 来源 |
| `paidCents` | integer | 是 | 付款 |
| `refundedCents` | integer | 是 | 已退 |
| `origin` | string |  | k3:// 引用 |
| `attribution` | object |  | 归因 |
| `code` | string |  | 券码 |
| `transferredFrom` | string |  | 转让自哪一项 |
| `held` | EntitlementHold |  | 一个未支付订单对权益的占用 |
| `held.id` | string | 是 | 占用 id |
| `held.grant` | string | 是 | 权益 id |
| `held.ref` | string | 是 | 占用它的订单 |
| `held.expiresAt` | string(date-time) | 是 | 占用到何时 |
| `createdBy` | string | 是 | 发放者 |
| `createdAt` | string(date-time) | 是 | 发放时间 |
| `ledger` | EntitlementLedgerEntry[] |  | 只在读单项权益时有 |
| `ledger[].id` | integer | 是 | 流水 id |
| `ledger[].kind` | string | 是 | 变动的种类 |
| `ledger[].delta` | integer | 是 | 次数的增减 |
| `ledger[].balance` | integer | 是 | 变动后的余额 |
| `ledger[].actorName` | string | 是 | 谁做的 |
| `ledger[].reason` | string | 是 | 原因 |
| `ledger[].ref` | string |  | 关联的订单或预约 |
| `ledger[].redemptionId` | string |  | 关联的核销单 |
| `ledger[].at` | string(date-time) | 是 | 时间 |

### `POST /entitlements/grants/{id}/refund`

退本应用发出的一项权益

需要范围 `app.entitlements:write`。English: ends a grant of the app's own template and answers the amount to give back.

`mode` 省略或 `times`：按剩余次数的份额；`amount`：按 `amountCents`，不超过还没退的付款。`reason` 必填。路径里的 id 优先于正文里的 `grant`。别的应用的权益是 403 `forbidden`。服务不动钱，钱由调用方退。

- 认证：`bearer`
- 范围：`app.entitlements:write`
- operationId：`refundEntitlement`

**参数**

| 名称 | 位置 | 类型 | 必填 | 说明 |
|---|---|---|---|---|
| `id` | path | string | 是 | id |
| `K3-Dry-Run` | header | string |  | 传 1 时只预演：回答和真做一样，什么也不留下<br>可选值：`1` |
| `Idempotency-Key` | header | string |  | 每个 POST 都收。SDK 未传入时会生成 uuid v4。24 小时内同键同正文重放第一次的回答<br>最短 1，最长 255 |

**请求正文（必填）**（`application/json`）

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `mode` | string |  | 省略或 times：按剩余次数的份额；amount：按 amountCents<br>可选值：`times`、`amount` |
| `amountCents` | integer |  | mode 为 amount 时的退款金额 |
| `reason` | string | 是 | 原因（必填） |
| `grant` | string |  | 被路径里的 id 取代，可省略 |

**响应**

| 状态 | 说明 |
|---|---|
| 200 | 退款结果 |
| 401 | 见 [Unauthorized](#resp-unauthorized) |
| 403 | 见 [Forbidden](#resp-forbidden) |
| 404 | `not_found`：对应的记录不存在（客户中心没有这个客户，访客、服务或所指的东西不存在）。见 [NotFound](#resp-notfound) |
| 409 | 见 [Conflict](#resp-conflict) |
| 413 | `payload_too_large`：正文太大。见 [PayloadTooLarge](#resp-payloadtoolarge) |
| 422 | 服务拒绝，或字段不被接受。见 [Refused](#resp-refused) |
| 429 | 见 [RateLimited](#resp-ratelimited) |
| 500 | 见 [Internal](#resp-internal) |
| 501 | `capability_off`：这个部署（或这个宿主）不提供这项能力，正文的 `capability` 是它的范围。见 [CapabilityOff](#resp-capabilityoff) |
| 503 | `unavailable`：暂时不可用（这个部署不提供的能力是 501 `capability_off`）。见 [Unavailable](#resp-unavailable) |

**200 响应正文**（`application/json`）

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `amountCents` | integer | 是 | 退回的金额（服务不动钱，由调用方退） |
| `grant` | EntitlementGrant | 是 | 一项权益 |
| `grant.id` | string | 是 | 权益 id |
| `grant.template` | string | 是 | 来源模板 id |
| `grant.ownerApp` | string | 是 | 模板归属的应用 |
| `grant.kind` | string | 是 | 种类 |
| `grant.name` | string | 是 | 名称 |
| `grant.params` | EntitlementParams | 是 | 种类相关的参数（字段名是 snake_case）；哪个种类读哪些字段由服务在保存模板时检查 |
| `grant.params.total_times` | integer |  | 次数（count、count_period） |
| `grant.params.valid_days` | integer |  | 有效天数（period、count_period 必填） |
| `grant.params.valid_from` | string |  | "grant"（默认）或 "first_use" |
| `grant.params.daily_cap` | integer |  | 每日上限（期卡） |
| `grant.params.weekly_cap` | integer |  | 每周上限（期卡） |
| `grant.params.starts_on` | string |  | 固定有效期开始，YYYY-MM-DD（券；覆盖 valid_days） |
| `grant.params.ends_on` | string |  | 固定有效期结束，含当天 |
| `grant.params.form` | string |  | 券的形式：cash、percent、service、code |
| `grant.params.amount_off_cents` | integer |  | 满减金额（cash） |
| `grant.params.percent` | integer |  | 折扣：减多少个百分点，1 到 99（percent） |
| `grant.params.threshold_cents` | integer |  | 订单要达到的金额（cash、percent） |
| `grant.params.max_off_cents` | integer |  | 最多减多少，0 为不封顶（percent） |
| `grant.params.restore_on_refund` | boolean |  | 订单退款时把券还回去（由调用方读到后作废核销单） |
| `grant.params.description` | string |  | 奖品说明 |
| `grant.params.fulfilment` | string |  | 奖品如何交付（对平台不透明） |
| `grant.scope` | string[] | 是 | 范围标签 |
| `grant.transferable` | boolean | 是 | 可转让 |
| `grant.freezable` | boolean | 是 | 可冻结 |
| `grant.maxFreezeDays` | integer | 是 | 最多冻结天数 |
| `grant.holder` | string | 是 | 持有的客户 id |
| `grant.total` | integer | 是 | 总次数 |
| `grant.remaining` | integer | 是 | 剩余次数 |
| `grant.startsOn` | string | 是 | 开始日期 |
| `grant.endsOn` | string | 是 | 结束日期 |
| `grant.state` | string | 是 | 对它做过什么 |
| `grant.status` | string | 是 | 对人说的状态：active、not_started、frozen、expired、used_up、used、refunded、voided、transferred（按今天算） |
| `grant.frozenFrom` | string |  | 冻结开始 |
| `grant.frozenUntil` | string |  | 冻结到 |
| `grant.frozenDaysUsed` | integer | 是 | 已冻结天数 |
| `grant.source` | string | 是 | 来源 |
| `grant.paidCents` | integer | 是 | 付款 |
| `grant.refundedCents` | integer | 是 | 已退 |
| `grant.origin` | string |  | k3:// 引用 |
| `grant.attribution` | object |  | 归因 |
| `grant.code` | string |  | 券码 |
| `grant.transferredFrom` | string |  | 转让自哪一项 |
| `grant.held` | EntitlementHold |  | 一个未支付订单对权益的占用 |
| `grant.held.id` | string | 是 | 占用 id |
| `grant.held.grant` | string | 是 | 权益 id |
| `grant.held.ref` | string | 是 | 占用它的订单 |
| `grant.held.expiresAt` | string(date-time) | 是 | 占用到何时 |
| `grant.createdBy` | string | 是 | 发放者 |
| `grant.createdAt` | string(date-time) | 是 | 发放时间 |
| `grant.ledger` | EntitlementLedgerEntry[] |  | 只在读单项权益时有 |
| `grant.ledger[].id` | integer | 是 | 流水 id |
| `grant.ledger[].kind` | string | 是 | 变动的种类 |
| `grant.ledger[].delta` | integer | 是 | 次数的增减 |
| `grant.ledger[].balance` | integer | 是 | 变动后的余额 |
| `grant.ledger[].actorName` | string | 是 | 谁做的 |
| `grant.ledger[].reason` | string | 是 | 原因 |
| `grant.ledger[].ref` | string |  | 关联的订单或预约 |
| `grant.ledger[].redemptionId` | string |  | 关联的核销单 |
| `grant.ledger[].at` | string(date-time) | 是 | 时间 |

### `POST /entitlements/stores/relink`

把本应用权益里的旧门店 id 换成组织里的门店

需要范围 `app.entitlements:write`。English: rewrites a legacy store id to the organisation's store in the templates and grants the calling app owns, and in the redemptions of those grants.

`scope` 里的 `store:<from>` 换成 `store:<to>`，这些权益的核销记录的地点 `from` 换成 `to`。只动调用应用自己发出的模板与权益（所有者由平台按调用方定，正文不能指定）。重复调用不再改变任何东西。活动关联旧门店记录时用它（ADR-0057）。

- 认证：`bearer`
- 范围：`app.entitlements:write`
- operationId：`relinkEntitlementStore`

**参数**

| 名称 | 位置 | 类型 | 必填 | 说明 |
|---|---|---|---|---|
| `K3-Dry-Run` | header | string |  | 传 1 时只预演：回答和真做一样，什么也不留下<br>可选值：`1` |
| `Idempotency-Key` | header | string |  | 每个 POST 都收。SDK 未传入时会生成 uuid v4。24 小时内同键同正文重放第一次的回答<br>最短 1，最长 255 |

**请求正文（必填）**（`application/json`）

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `from` | string | 是 | 旧门店 id（应用自己的旧记录）<br>最长 100 |
| `to` | string | 是 | 组织里的门店（部门 id）<br>最长 100 |

**响应**

| 状态 | 说明 |
|---|---|
| 204 | 已改写。没有正文 |
| 401 | 见 [Unauthorized](#resp-unauthorized) |
| 403 | 见 [Forbidden](#resp-forbidden) |
| 413 | `payload_too_large`：正文太大。见 [PayloadTooLarge](#resp-payloadtoolarge) |
| 422 | 服务拒绝，或字段不被接受。见 [Refused](#resp-refused) |
| 429 | 见 [RateLimited](#resp-ratelimited) |
| 500 | 见 [Internal](#resp-internal) |

### `POST /entitlements/grants/{id}/transfer`

把本应用发出的一项权益转给另一位客户

需要范围 `app.entitlements:write`。English: gives what is left of a grant to another holder.

原权益结束，生成一项条款相同的新权益并返回。不能转让时是 422，说明原因。别的应用的权益是 403 `forbidden`。

- 认证：`bearer`
- 范围：`app.entitlements:write`
- operationId：`transferEntitlement`

**参数**

| 名称 | 位置 | 类型 | 必填 | 说明 |
|---|---|---|---|---|
| `id` | path | string | 是 | id |
| `K3-Dry-Run` | header | string |  | 传 1 时只预演：回答和真做一样，什么也不留下<br>可选值：`1` |
| `Idempotency-Key` | header | string |  | 每个 POST 都收。SDK 未传入时会生成 uuid v4。24 小时内同键同正文重放第一次的回答<br>最短 1，最长 255 |

**请求正文（必填）**（`application/json`）

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `to` | string | 是 | 接收的客户 id |
| `reason` | string |  | 原因 |
| `grant` | string |  | 被路径里的 id 取代，可省略 |

**响应**

| 状态 | 说明 |
|---|---|
| 200 | 新的权益 |
| 401 | 见 [Unauthorized](#resp-unauthorized) |
| 403 | 见 [Forbidden](#resp-forbidden) |
| 404 | `not_found`：对应的记录不存在（客户中心没有这个客户，访客、服务或所指的东西不存在）。见 [NotFound](#resp-notfound) |
| 409 | 见 [Conflict](#resp-conflict) |
| 413 | `payload_too_large`：正文太大。见 [PayloadTooLarge](#resp-payloadtoolarge) |
| 422 | 服务拒绝，或字段不被接受。见 [Refused](#resp-refused) |
| 429 | 见 [RateLimited](#resp-ratelimited) |
| 500 | 见 [Internal](#resp-internal) |
| 501 | `capability_off`：这个部署（或这个宿主）不提供这项能力，正文的 `capability` 是它的范围。见 [CapabilityOff](#resp-capabilityoff) |
| 503 | `unavailable`：暂时不可用（这个部署不提供的能力是 501 `capability_off`）。见 [Unavailable](#resp-unavailable) |

**200 响应正文**（`application/json`）

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `id` | string | 是 | 权益 id |
| `template` | string | 是 | 来源模板 id |
| `ownerApp` | string | 是 | 模板归属的应用 |
| `kind` | string | 是 | 种类 |
| `name` | string | 是 | 名称 |
| `params` | EntitlementParams | 是 | 种类相关的参数（字段名是 snake_case）；哪个种类读哪些字段由服务在保存模板时检查 |
| `params.total_times` | integer |  | 次数（count、count_period） |
| `params.valid_days` | integer |  | 有效天数（period、count_period 必填） |
| `params.valid_from` | string |  | "grant"（默认）或 "first_use" |
| `params.daily_cap` | integer |  | 每日上限（期卡） |
| `params.weekly_cap` | integer |  | 每周上限（期卡） |
| `params.starts_on` | string |  | 固定有效期开始，YYYY-MM-DD（券；覆盖 valid_days） |
| `params.ends_on` | string |  | 固定有效期结束，含当天 |
| `params.form` | string |  | 券的形式：cash、percent、service、code |
| `params.amount_off_cents` | integer |  | 满减金额（cash） |
| `params.percent` | integer |  | 折扣：减多少个百分点，1 到 99（percent） |
| `params.threshold_cents` | integer |  | 订单要达到的金额（cash、percent） |
| `params.max_off_cents` | integer |  | 最多减多少，0 为不封顶（percent） |
| `params.restore_on_refund` | boolean |  | 订单退款时把券还回去（由调用方读到后作废核销单） |
| `params.description` | string |  | 奖品说明 |
| `params.fulfilment` | string |  | 奖品如何交付（对平台不透明） |
| `scope` | string[] | 是 | 范围标签 |
| `transferable` | boolean | 是 | 可转让 |
| `freezable` | boolean | 是 | 可冻结 |
| `maxFreezeDays` | integer | 是 | 最多冻结天数 |
| `holder` | string | 是 | 持有的客户 id |
| `total` | integer | 是 | 总次数 |
| `remaining` | integer | 是 | 剩余次数 |
| `startsOn` | string | 是 | 开始日期 |
| `endsOn` | string | 是 | 结束日期 |
| `state` | string | 是 | 对它做过什么 |
| `status` | string | 是 | 对人说的状态：active、not_started、frozen、expired、used_up、used、refunded、voided、transferred（按今天算） |
| `frozenFrom` | string |  | 冻结开始 |
| `frozenUntil` | string |  | 冻结到 |
| `frozenDaysUsed` | integer | 是 | 已冻结天数 |
| `source` | string | 是 | 来源 |
| `paidCents` | integer | 是 | 付款 |
| `refundedCents` | integer | 是 | 已退 |
| `origin` | string |  | k3:// 引用 |
| `attribution` | object |  | 归因 |
| `code` | string |  | 券码 |
| `transferredFrom` | string |  | 转让自哪一项 |
| `held` | EntitlementHold |  | 一个未支付订单对权益的占用 |
| `held.id` | string | 是 | 占用 id |
| `held.grant` | string | 是 | 权益 id |
| `held.ref` | string | 是 | 占用它的订单 |
| `held.expiresAt` | string(date-time) | 是 | 占用到何时 |
| `createdBy` | string | 是 | 发放者 |
| `createdAt` | string(date-time) | 是 | 发放时间 |
| `ledger` | EntitlementLedgerEntry[] |  | 只在读单项权益时有 |
| `ledger[].id` | integer | 是 | 流水 id |
| `ledger[].kind` | string | 是 | 变动的种类 |
| `ledger[].delta` | integer | 是 | 次数的增减 |
| `ledger[].balance` | integer | 是 | 变动后的余额 |
| `ledger[].actorName` | string | 是 | 谁做的 |
| `ledger[].reason` | string | 是 | 原因 |
| `ledger[].ref` | string |  | 关联的订单或预约 |
| `ledger[].redemptionId` | string |  | 关联的核销单 |
| `ledger[].at` | string(date-time) | 是 | 时间 |

### `POST /entitlements/redemptions`

核销一次权益（原子，按 key 幂等）

需要范围 `app.entitlements:write`。English: uses a grant once or more, atomically.

`pick` 指定权益；不指定时服务在 `holder` 的权益里按“先到期的、无期限的最后、剩得少的、先发的”选。`key` 在正文里，没写时用请求头 `Idempotency-Key`；同键重来 `replay` 为 true，不再扣。任何权益都可以核销，不限本应用的模板。不能核销时是 422，`error.message` 是给人看的原因，`error.details.items` 逐项说明其他权益为什么不行。

- 认证：`bearer`
- 范围：`app.entitlements:write`
- operationId：`redeemEntitlement`

**参数**

| 名称 | 位置 | 类型 | 必填 | 说明 |
|---|---|---|---|---|
| `K3-Dry-Run` | header | string |  | 传 1 时只预演：回答和真做一样，什么也不留下<br>可选值：`1` |
| `Idempotency-Key` | header | string |  | 每个 POST 都收。SDK 未传入时会生成 uuid v4。24 小时内同键同正文重放第一次的回答<br>最短 1，最长 255 |

**请求正文（必填）**（`application/json`）

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `key` | string |  | 幂等键；省略时用请求头 Idempotency-Key |
| `holder` | string |  | 从谁的权益里选；pick 指定了权益时可省略 |
| `pick` | string |  | 指定权益 id；省略则由服务选 |
| `scope` | string[] |  | 这次使用的范围标签：权益的范围为空、这里为空或这里每个标签都在权益的范围里，权益才适用 |
| `times` | integer |  | 次数，默认 1（券和奖品总是 1 次） |
| `by` | string |  | 核销的人或系统，默认调用者 |
| `where` | string |  | 线下核销的门店 |
| `ref` | string |  | 它所付的订单或预约 |
| `orderCents` | integer |  | 满减券和折扣券所用于的订单金额（这两种必填） |

**响应**

| 状态 | 说明 |
|---|---|
| 200 | 核销单和核销后的权益 |
| 401 | 见 [Unauthorized](#resp-unauthorized) |
| 403 | 见 [Forbidden](#resp-forbidden) |
| 404 | `not_found`：对应的记录不存在（客户中心没有这个客户，访客、服务或所指的东西不存在）。见 [NotFound](#resp-notfound) |
| 409 | 见 [Conflict](#resp-conflict) |
| 413 | `payload_too_large`：正文太大。见 [PayloadTooLarge](#resp-payloadtoolarge) |
| 422 | 服务拒绝，或字段不被接受。见 [Refused](#resp-refused) |
| 429 | 见 [RateLimited](#resp-ratelimited) |
| 500 | 见 [Internal](#resp-internal) |
| 501 | `capability_off`：这个部署（或这个宿主）不提供这项能力，正文的 `capability` 是它的范围。见 [CapabilityOff](#resp-capabilityoff) |
| 503 | `unavailable`：暂时不可用（这个部署不提供的能力是 501 `capability_off`）。见 [Unavailable](#resp-unavailable) |

**200 响应正文**（`application/json`）

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `redemption` | EntitlementRedemption | 是 | 一次使用（核销单） |
| `redemption.id` | string | 是 | 核销单 id |
| `redemption.no` | string | 是 | 核销单号 |
| `redemption.grant` | string | 是 | 权益 id |
| `redemption.holder` | string | 是 | 客户 id |
| `redemption.times` | integer | 是 | 次数 |
| `redemption.discountCents` | integer | 是 | 券减掉的金额 |
| `redemption.state` | string | 是 | ok 或 void |
| `redemption.voidReason` | string |  | 作废原因 |
| `redemption.voidedAt` | string(date-time) |  | 作废时间 |
| `redemption.by` | string | 是 | 核销者 |
| `redemption.byName` | string | 是 | 核销者姓名 |
| `redemption.where` | string |  | 门店 |
| `redemption.ref` | string |  | 订单或预约 |
| `redemption.createdAt` | string(date-time) | 是 | 核销时间 |
| `grant` | EntitlementGrant | 是 | 一项权益 |
| `grant.id` | string | 是 | 权益 id |
| `grant.template` | string | 是 | 来源模板 id |
| `grant.ownerApp` | string | 是 | 模板归属的应用 |
| `grant.kind` | string | 是 | 种类 |
| `grant.name` | string | 是 | 名称 |
| `grant.params` | EntitlementParams | 是 | 种类相关的参数（字段名是 snake_case）；哪个种类读哪些字段由服务在保存模板时检查 |
| `grant.params.total_times` | integer |  | 次数（count、count_period） |
| `grant.params.valid_days` | integer |  | 有效天数（period、count_period 必填） |
| `grant.params.valid_from` | string |  | "grant"（默认）或 "first_use" |
| `grant.params.daily_cap` | integer |  | 每日上限（期卡） |
| `grant.params.weekly_cap` | integer |  | 每周上限（期卡） |
| `grant.params.starts_on` | string |  | 固定有效期开始，YYYY-MM-DD（券；覆盖 valid_days） |
| `grant.params.ends_on` | string |  | 固定有效期结束，含当天 |
| `grant.params.form` | string |  | 券的形式：cash、percent、service、code |
| `grant.params.amount_off_cents` | integer |  | 满减金额（cash） |
| `grant.params.percent` | integer |  | 折扣：减多少个百分点，1 到 99（percent） |
| `grant.params.threshold_cents` | integer |  | 订单要达到的金额（cash、percent） |
| `grant.params.max_off_cents` | integer |  | 最多减多少，0 为不封顶（percent） |
| `grant.params.restore_on_refund` | boolean |  | 订单退款时把券还回去（由调用方读到后作废核销单） |
| `grant.params.description` | string |  | 奖品说明 |
| `grant.params.fulfilment` | string |  | 奖品如何交付（对平台不透明） |
| `grant.scope` | string[] | 是 | 范围标签 |
| `grant.transferable` | boolean | 是 | 可转让 |
| `grant.freezable` | boolean | 是 | 可冻结 |
| `grant.maxFreezeDays` | integer | 是 | 最多冻结天数 |
| `grant.holder` | string | 是 | 持有的客户 id |
| `grant.total` | integer | 是 | 总次数 |
| `grant.remaining` | integer | 是 | 剩余次数 |
| `grant.startsOn` | string | 是 | 开始日期 |
| `grant.endsOn` | string | 是 | 结束日期 |
| `grant.state` | string | 是 | 对它做过什么 |
| `grant.status` | string | 是 | 对人说的状态：active、not_started、frozen、expired、used_up、used、refunded、voided、transferred（按今天算） |
| `grant.frozenFrom` | string |  | 冻结开始 |
| `grant.frozenUntil` | string |  | 冻结到 |
| `grant.frozenDaysUsed` | integer | 是 | 已冻结天数 |
| `grant.source` | string | 是 | 来源 |
| `grant.paidCents` | integer | 是 | 付款 |
| `grant.refundedCents` | integer | 是 | 已退 |
| `grant.origin` | string |  | k3:// 引用 |
| `grant.attribution` | object |  | 归因 |
| `grant.code` | string |  | 券码 |
| `grant.transferredFrom` | string |  | 转让自哪一项 |
| `grant.held` | EntitlementHold |  | 一个未支付订单对权益的占用 |
| `grant.held.id` | string | 是 | 占用 id |
| `grant.held.grant` | string | 是 | 权益 id |
| `grant.held.ref` | string | 是 | 占用它的订单 |
| `grant.held.expiresAt` | string(date-time) | 是 | 占用到何时 |
| `grant.createdBy` | string | 是 | 发放者 |
| `grant.createdAt` | string(date-time) | 是 | 发放时间 |
| `grant.ledger` | EntitlementLedgerEntry[] |  | 只在读单项权益时有 |
| `grant.ledger[].id` | integer | 是 | 流水 id |
| `grant.ledger[].kind` | string | 是 | 变动的种类 |
| `grant.ledger[].delta` | integer | 是 | 次数的增减 |
| `grant.ledger[].balance` | integer | 是 | 变动后的余额 |
| `grant.ledger[].actorName` | string | 是 | 谁做的 |
| `grant.ledger[].reason` | string | 是 | 原因 |
| `grant.ledger[].ref` | string |  | 关联的订单或预约 |
| `grant.ledger[].redemptionId` | string |  | 关联的核销单 |
| `grant.ledger[].at` | string(date-time) | 是 | 时间 |
| `replay` | boolean | 是 | 同一个 key 已做过，没有再扣 |

### `POST /entitlements/redemptions/{id}/void`

作废一张核销单，次数退回

需要范围 `app.entitlements:write`。English: undoes a redemption of a grant of the app's own template; the times come back through the ledger.

核销单所用的权益不是本应用的是 403 `forbidden`。正文必须是 JSON 对象，`reason` 可省略。

- 认证：`bearer`
- 范围：`app.entitlements:write`
- operationId：`voidEntitlementRedemption`

**参数**

| 名称 | 位置 | 类型 | 必填 | 说明 |
|---|---|---|---|---|
| `id` | path | string | 是 | id |
| `K3-Dry-Run` | header | string |  | 传 1 时只预演：回答和真做一样，什么也不留下<br>可选值：`1` |
| `Idempotency-Key` | header | string |  | 每个 POST 都收。SDK 未传入时会生成 uuid v4。24 小时内同键同正文重放第一次的回答<br>最短 1，最长 255 |

**请求正文（必填）**（`application/json`）

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `reason` | string |  | 原因 |

**响应**

| 状态 | 说明 |
|---|---|
| 200 | 作废后的核销单 |
| 401 | 见 [Unauthorized](#resp-unauthorized) |
| 403 | 见 [Forbidden](#resp-forbidden) |
| 404 | `not_found`：对应的记录不存在（客户中心没有这个客户，访客、服务或所指的东西不存在）。见 [NotFound](#resp-notfound) |
| 409 | 见 [Conflict](#resp-conflict) |
| 413 | `payload_too_large`：正文太大。见 [PayloadTooLarge](#resp-payloadtoolarge) |
| 422 | 服务拒绝，或字段不被接受。见 [Refused](#resp-refused) |
| 429 | 见 [RateLimited](#resp-ratelimited) |
| 500 | 见 [Internal](#resp-internal) |
| 501 | `capability_off`：这个部署（或这个宿主）不提供这项能力，正文的 `capability` 是它的范围。见 [CapabilityOff](#resp-capabilityoff) |
| 503 | `unavailable`：暂时不可用（这个部署不提供的能力是 501 `capability_off`）。见 [Unavailable](#resp-unavailable) |

**200 响应正文**（`application/json`）

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `redemption` | EntitlementRedemption | 是 | 一次使用（核销单） |
| `redemption.id` | string | 是 | 核销单 id |
| `redemption.no` | string | 是 | 核销单号 |
| `redemption.grant` | string | 是 | 权益 id |
| `redemption.holder` | string | 是 | 客户 id |
| `redemption.times` | integer | 是 | 次数 |
| `redemption.discountCents` | integer | 是 | 券减掉的金额 |
| `redemption.state` | string | 是 | ok 或 void |
| `redemption.voidReason` | string |  | 作废原因 |
| `redemption.voidedAt` | string(date-time) |  | 作废时间 |
| `redemption.by` | string | 是 | 核销者 |
| `redemption.byName` | string | 是 | 核销者姓名 |
| `redemption.where` | string |  | 门店 |
| `redemption.ref` | string |  | 订单或预约 |
| `redemption.createdAt` | string(date-time) | 是 | 核销时间 |
| `grant` | EntitlementGrant | 是 | 一项权益 |
| `grant.id` | string | 是 | 权益 id |
| `grant.template` | string | 是 | 来源模板 id |
| `grant.ownerApp` | string | 是 | 模板归属的应用 |
| `grant.kind` | string | 是 | 种类 |
| `grant.name` | string | 是 | 名称 |
| `grant.params` | EntitlementParams | 是 | 种类相关的参数（字段名是 snake_case）；哪个种类读哪些字段由服务在保存模板时检查 |
| `grant.params.total_times` | integer |  | 次数（count、count_period） |
| `grant.params.valid_days` | integer |  | 有效天数（period、count_period 必填） |
| `grant.params.valid_from` | string |  | "grant"（默认）或 "first_use" |
| `grant.params.daily_cap` | integer |  | 每日上限（期卡） |
| `grant.params.weekly_cap` | integer |  | 每周上限（期卡） |
| `grant.params.starts_on` | string |  | 固定有效期开始，YYYY-MM-DD（券；覆盖 valid_days） |
| `grant.params.ends_on` | string |  | 固定有效期结束，含当天 |
| `grant.params.form` | string |  | 券的形式：cash、percent、service、code |
| `grant.params.amount_off_cents` | integer |  | 满减金额（cash） |
| `grant.params.percent` | integer |  | 折扣：减多少个百分点，1 到 99（percent） |
| `grant.params.threshold_cents` | integer |  | 订单要达到的金额（cash、percent） |
| `grant.params.max_off_cents` | integer |  | 最多减多少，0 为不封顶（percent） |
| `grant.params.restore_on_refund` | boolean |  | 订单退款时把券还回去（由调用方读到后作废核销单） |
| `grant.params.description` | string |  | 奖品说明 |
| `grant.params.fulfilment` | string |  | 奖品如何交付（对平台不透明） |
| `grant.scope` | string[] | 是 | 范围标签 |
| `grant.transferable` | boolean | 是 | 可转让 |
| `grant.freezable` | boolean | 是 | 可冻结 |
| `grant.maxFreezeDays` | integer | 是 | 最多冻结天数 |
| `grant.holder` | string | 是 | 持有的客户 id |
| `grant.total` | integer | 是 | 总次数 |
| `grant.remaining` | integer | 是 | 剩余次数 |
| `grant.startsOn` | string | 是 | 开始日期 |
| `grant.endsOn` | string | 是 | 结束日期 |
| `grant.state` | string | 是 | 对它做过什么 |
| `grant.status` | string | 是 | 对人说的状态：active、not_started、frozen、expired、used_up、used、refunded、voided、transferred（按今天算） |
| `grant.frozenFrom` | string |  | 冻结开始 |
| `grant.frozenUntil` | string |  | 冻结到 |
| `grant.frozenDaysUsed` | integer | 是 | 已冻结天数 |
| `grant.source` | string | 是 | 来源 |
| `grant.paidCents` | integer | 是 | 付款 |
| `grant.refundedCents` | integer | 是 | 已退 |
| `grant.origin` | string |  | k3:// 引用 |
| `grant.attribution` | object |  | 归因 |
| `grant.code` | string |  | 券码 |
| `grant.transferredFrom` | string |  | 转让自哪一项 |
| `grant.held` | EntitlementHold |  | 一个未支付订单对权益的占用 |
| `grant.held.id` | string | 是 | 占用 id |
| `grant.held.grant` | string | 是 | 权益 id |
| `grant.held.ref` | string | 是 | 占用它的订单 |
| `grant.held.expiresAt` | string(date-time) | 是 | 占用到何时 |
| `grant.createdBy` | string | 是 | 发放者 |
| `grant.createdAt` | string(date-time) | 是 | 发放时间 |
| `grant.ledger` | EntitlementLedgerEntry[] |  | 只在读单项权益时有 |
| `grant.ledger[].id` | integer | 是 | 流水 id |
| `grant.ledger[].kind` | string | 是 | 变动的种类 |
| `grant.ledger[].delta` | integer | 是 | 次数的增减 |
| `grant.ledger[].balance` | integer | 是 | 变动后的余额 |
| `grant.ledger[].actorName` | string | 是 | 谁做的 |
| `grant.ledger[].reason` | string | 是 | 原因 |
| `grant.ledger[].ref` | string |  | 关联的订单或预约 |
| `grant.ledger[].redemptionId` | string |  | 关联的核销单 |
| `grant.ledger[].at` | string(date-time) | 是 | 时间 |
| `replay` | boolean | 是 | 同一个 key 已做过，没有再扣 |

### `POST /entitlements/reservations`

预留一次使用（到时未确认自动释放）

需要范围 `app.entitlements:write`。English: keeps one use aside until it is confirmed, cancelled or expired.

正文是核销的字段加 `ttlSeconds`（5 到 86400，默认 900）。权益的选法同核销。预留期间别人不能用这份。同一个 `key` 返回同一个预留：200 且 `replay` 为 true；新预留 201。

- 认证：`bearer`
- 范围：`app.entitlements:write`
- operationId：`reserveEntitlement`

**参数**

| 名称 | 位置 | 类型 | 必填 | 说明 |
|---|---|---|---|---|
| `K3-Dry-Run` | header | string |  | 传 1 时只预演：回答和真做一样，什么也不留下<br>可选值：`1` |
| `Idempotency-Key` | header | string |  | 每个 POST 都收。SDK 未传入时会生成 uuid v4。24 小时内同键同正文重放第一次的回答<br>最短 1，最长 255 |

**请求正文（必填）**（`application/json`）

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `key` | string |  | 预留的幂等键；省略时用请求头 Idempotency-Key |
| `holder` | string |  | 从谁的权益里选 |
| `pick` | string |  | 指定权益 id |
| `scope` | string[] |  | 这次使用的范围标签 |
| `times` | integer |  | 次数，默认 1 |
| `by` | string |  | 默认调用者 |
| `where` | string |  | 门店 |
| `ref` | string |  | 所付的订单或预约 |
| `orderCents` | integer |  | 满减券和折扣券所用于的订单金额 |
| `ttlSeconds` | integer |  | 保留多少秒，5 到 86400，默认 900<br>最小 5，最大 86400 |

**响应**

| 状态 | 说明 |
|---|---|
| 200 | 同一个 key 已预留过：这是当时的预留（replay 为 true） |
| 201 | 已预留 |
| 401 | 见 [Unauthorized](#resp-unauthorized) |
| 403 | 见 [Forbidden](#resp-forbidden) |
| 404 | `not_found`：对应的记录不存在（客户中心没有这个客户，访客、服务或所指的东西不存在）。见 [NotFound](#resp-notfound) |
| 409 | 见 [Conflict](#resp-conflict) |
| 413 | `payload_too_large`：正文太大。见 [PayloadTooLarge](#resp-payloadtoolarge) |
| 422 | 服务拒绝，或字段不被接受。见 [Refused](#resp-refused) |
| 429 | 见 [RateLimited](#resp-ratelimited) |
| 500 | 见 [Internal](#resp-internal) |
| 501 | `capability_off`：这个部署（或这个宿主）不提供这项能力，正文的 `capability` 是它的范围。见 [CapabilityOff](#resp-capabilityoff) |
| 503 | `unavailable`：暂时不可用（这个部署不提供的能力是 501 `capability_off`）。见 [Unavailable](#resp-unavailable) |

**200 响应正文**（`application/json`）

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `id` | string | 是 | 预留 id |
| `grant` | string | 是 | 被留住的权益 id |
| `holder` | string | 是 | 客户 id |
| `times` | integer | 是 | 次数 |
| `scope` | string[] | 是 | 范围标签 |
| `orderCents` | integer | 是 | 订单金额 |
| `discountCents` | integer | 是 | 券减掉的金额 |
| `key` | string | 是 | 预留的键 |
| `ref` | string |  | 订单或预约 |
| `where` | string |  | 门店 |
| `by` | string | 是 | 谁预留的 |
| `byName` | string | 是 | 姓名 |
| `app` | string |  | 预留的应用 |
| `state` | string | 是 | live、confirmed、cancelled、expired |
| `expiresAt` | string(date-time) | 是 | 到期时间 |
| `redemption` | string |  | 已确认时的核销单 id |
| `reason` | string |  | 已取消时的原因 |
| `createdAt` | string(date-time) | 是 | 预留时间 |
| `replay` | boolean | 是 | 同一个 key 已预留过，这是当时的预留 |

**201 响应正文**（`application/json`）

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `id` | string | 是 | 预留 id |
| `grant` | string | 是 | 被留住的权益 id |
| `holder` | string | 是 | 客户 id |
| `times` | integer | 是 | 次数 |
| `scope` | string[] | 是 | 范围标签 |
| `orderCents` | integer | 是 | 订单金额 |
| `discountCents` | integer | 是 | 券减掉的金额 |
| `key` | string | 是 | 预留的键 |
| `ref` | string |  | 订单或预约 |
| `where` | string |  | 门店 |
| `by` | string | 是 | 谁预留的 |
| `byName` | string | 是 | 姓名 |
| `app` | string |  | 预留的应用 |
| `state` | string | 是 | live、confirmed、cancelled、expired |
| `expiresAt` | string(date-time) | 是 | 到期时间 |
| `redemption` | string |  | 已确认时的核销单 id |
| `reason` | string |  | 已取消时的原因 |
| `createdAt` | string(date-time) | 是 | 预留时间 |
| `replay` | boolean | 是 | 同一个 key 已预留过，这是当时的预留 |

### `GET /entitlements/reservations/{id}`

读一个预留

需要范围 `app.entitlements:read`。English: one reservation. A live one past its expiry reads as expired.

- 认证：`bearer`
- 范围：`app.entitlements:read`
- operationId：`getEntitlementReservation`

**参数**

| 名称 | 位置 | 类型 | 必填 | 说明 |
|---|---|---|---|---|
| `id` | path | string | 是 | id |

**响应**

| 状态 | 说明 |
|---|---|
| 200 | 预留 |
| 401 | 见 [Unauthorized](#resp-unauthorized) |
| 403 | 见 [Forbidden](#resp-forbidden) |
| 404 | `not_found`：对应的记录不存在（客户中心没有这个客户，访客、服务或所指的东西不存在）。见 [NotFound](#resp-notfound) |
| 422 | 服务拒绝，或字段不被接受。见 [Refused](#resp-refused) |
| 429 | 见 [RateLimited](#resp-ratelimited) |
| 500 | 见 [Internal](#resp-internal) |
| 501 | `capability_off`：这个部署（或这个宿主）不提供这项能力，正文的 `capability` 是它的范围。见 [CapabilityOff](#resp-capabilityoff) |
| 503 | `unavailable`：暂时不可用（这个部署不提供的能力是 501 `capability_off`）。见 [Unavailable](#resp-unavailable) |

**200 响应正文**（`application/json`）

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `id` | string | 是 | 预留 id |
| `grant` | string | 是 | 被留住的权益 id |
| `holder` | string | 是 | 客户 id |
| `times` | integer | 是 | 次数 |
| `scope` | string[] | 是 | 范围标签 |
| `orderCents` | integer | 是 | 订单金额 |
| `discountCents` | integer | 是 | 券减掉的金额 |
| `key` | string | 是 | 预留的键 |
| `ref` | string |  | 订单或预约 |
| `where` | string |  | 门店 |
| `by` | string | 是 | 谁预留的 |
| `byName` | string | 是 | 姓名 |
| `app` | string |  | 预留的应用 |
| `state` | string | 是 | live、confirmed、cancelled、expired |
| `expiresAt` | string(date-time) | 是 | 到期时间 |
| `redemption` | string |  | 已确认时的核销单 id |
| `reason` | string |  | 已取消时的原因 |
| `createdAt` | string(date-time) | 是 | 预留时间 |
| `replay` | boolean | 是 | 同一个 key 已预留过，这是当时的预留 |

### `POST /entitlements/reservations/{id}/confirm`

确认预留：做出核销（恰好一次）

需要范围 `app.entitlements:write`。English: makes the reservation's redemption, exactly once.

再次确认返回同一张核销单且 `replay` 为 true。预留已过期或已取消是 409 `conflict`。不需要正文。

- 认证：`bearer`
- 范围：`app.entitlements:write`
- operationId：`confirmEntitlementReservation`

**参数**

| 名称 | 位置 | 类型 | 必填 | 说明 |
|---|---|---|---|---|
| `id` | path | string | 是 | id |
| `K3-Dry-Run` | header | string |  | 传 1 时只预演：回答和真做一样，什么也不留下<br>可选值：`1` |
| `Idempotency-Key` | header | string |  | 每个 POST 都收。SDK 未传入时会生成 uuid v4。24 小时内同键同正文重放第一次的回答<br>最短 1，最长 255 |

**响应**

| 状态 | 说明 |
|---|---|
| 200 | 核销单和核销后的权益 |
| 401 | 见 [Unauthorized](#resp-unauthorized) |
| 403 | 见 [Forbidden](#resp-forbidden) |
| 404 | `not_found`：对应的记录不存在（客户中心没有这个客户，访客、服务或所指的东西不存在）。见 [NotFound](#resp-notfound) |
| 409 | 见 [Conflict](#resp-conflict) |
| 422 | 服务拒绝，或字段不被接受。见 [Refused](#resp-refused) |
| 429 | 见 [RateLimited](#resp-ratelimited) |
| 500 | 见 [Internal](#resp-internal) |
| 501 | `capability_off`：这个部署（或这个宿主）不提供这项能力，正文的 `capability` 是它的范围。见 [CapabilityOff](#resp-capabilityoff) |
| 503 | `unavailable`：暂时不可用（这个部署不提供的能力是 501 `capability_off`）。见 [Unavailable](#resp-unavailable) |

**200 响应正文**（`application/json`）

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `redemption` | EntitlementRedemption | 是 | 一次使用（核销单） |
| `redemption.id` | string | 是 | 核销单 id |
| `redemption.no` | string | 是 | 核销单号 |
| `redemption.grant` | string | 是 | 权益 id |
| `redemption.holder` | string | 是 | 客户 id |
| `redemption.times` | integer | 是 | 次数 |
| `redemption.discountCents` | integer | 是 | 券减掉的金额 |
| `redemption.state` | string | 是 | ok 或 void |
| `redemption.voidReason` | string |  | 作废原因 |
| `redemption.voidedAt` | string(date-time) |  | 作废时间 |
| `redemption.by` | string | 是 | 核销者 |
| `redemption.byName` | string | 是 | 核销者姓名 |
| `redemption.where` | string |  | 门店 |
| `redemption.ref` | string |  | 订单或预约 |
| `redemption.createdAt` | string(date-time) | 是 | 核销时间 |
| `grant` | EntitlementGrant | 是 | 一项权益 |
| `grant.id` | string | 是 | 权益 id |
| `grant.template` | string | 是 | 来源模板 id |
| `grant.ownerApp` | string | 是 | 模板归属的应用 |
| `grant.kind` | string | 是 | 种类 |
| `grant.name` | string | 是 | 名称 |
| `grant.params` | EntitlementParams | 是 | 种类相关的参数（字段名是 snake_case）；哪个种类读哪些字段由服务在保存模板时检查 |
| `grant.params.total_times` | integer |  | 次数（count、count_period） |
| `grant.params.valid_days` | integer |  | 有效天数（period、count_period 必填） |
| `grant.params.valid_from` | string |  | "grant"（默认）或 "first_use" |
| `grant.params.daily_cap` | integer |  | 每日上限（期卡） |
| `grant.params.weekly_cap` | integer |  | 每周上限（期卡） |
| `grant.params.starts_on` | string |  | 固定有效期开始，YYYY-MM-DD（券；覆盖 valid_days） |
| `grant.params.ends_on` | string |  | 固定有效期结束，含当天 |
| `grant.params.form` | string |  | 券的形式：cash、percent、service、code |
| `grant.params.amount_off_cents` | integer |  | 满减金额（cash） |
| `grant.params.percent` | integer |  | 折扣：减多少个百分点，1 到 99（percent） |
| `grant.params.threshold_cents` | integer |  | 订单要达到的金额（cash、percent） |
| `grant.params.max_off_cents` | integer |  | 最多减多少，0 为不封顶（percent） |
| `grant.params.restore_on_refund` | boolean |  | 订单退款时把券还回去（由调用方读到后作废核销单） |
| `grant.params.description` | string |  | 奖品说明 |
| `grant.params.fulfilment` | string |  | 奖品如何交付（对平台不透明） |
| `grant.scope` | string[] | 是 | 范围标签 |
| `grant.transferable` | boolean | 是 | 可转让 |
| `grant.freezable` | boolean | 是 | 可冻结 |
| `grant.maxFreezeDays` | integer | 是 | 最多冻结天数 |
| `grant.holder` | string | 是 | 持有的客户 id |
| `grant.total` | integer | 是 | 总次数 |
| `grant.remaining` | integer | 是 | 剩余次数 |
| `grant.startsOn` | string | 是 | 开始日期 |
| `grant.endsOn` | string | 是 | 结束日期 |
| `grant.state` | string | 是 | 对它做过什么 |
| `grant.status` | string | 是 | 对人说的状态：active、not_started、frozen、expired、used_up、used、refunded、voided、transferred（按今天算） |
| `grant.frozenFrom` | string |  | 冻结开始 |
| `grant.frozenUntil` | string |  | 冻结到 |
| `grant.frozenDaysUsed` | integer | 是 | 已冻结天数 |
| `grant.source` | string | 是 | 来源 |
| `grant.paidCents` | integer | 是 | 付款 |
| `grant.refundedCents` | integer | 是 | 已退 |
| `grant.origin` | string |  | k3:// 引用 |
| `grant.attribution` | object |  | 归因 |
| `grant.code` | string |  | 券码 |
| `grant.transferredFrom` | string |  | 转让自哪一项 |
| `grant.held` | EntitlementHold |  | 一个未支付订单对权益的占用 |
| `grant.held.id` | string | 是 | 占用 id |
| `grant.held.grant` | string | 是 | 权益 id |
| `grant.held.ref` | string | 是 | 占用它的订单 |
| `grant.held.expiresAt` | string(date-time) | 是 | 占用到何时 |
| `grant.createdBy` | string | 是 | 发放者 |
| `grant.createdAt` | string(date-time) | 是 | 发放时间 |
| `grant.ledger` | EntitlementLedgerEntry[] |  | 只在读单项权益时有 |
| `grant.ledger[].id` | integer | 是 | 流水 id |
| `grant.ledger[].kind` | string | 是 | 变动的种类 |
| `grant.ledger[].delta` | integer | 是 | 次数的增减 |
| `grant.ledger[].balance` | integer | 是 | 变动后的余额 |
| `grant.ledger[].actorName` | string | 是 | 谁做的 |
| `grant.ledger[].reason` | string | 是 | 原因 |
| `grant.ledger[].ref` | string |  | 关联的订单或预约 |
| `grant.ledger[].redemptionId` | string |  | 关联的核销单 |
| `grant.ledger[].at` | string(date-time) | 是 | 时间 |
| `replay` | boolean | 是 | 同一个 key 已做过，没有再扣 |

### `POST /entitlements/reservations/{id}/cancel`

取消预留，释放被留住的次数

需要范围 `app.entitlements:write`。English: gives a live reservation up; one that ended already is answered as it is.

已结束的预留原样返回且 `replay` 为 true。正文必须是 JSON 对象，`reason` 可省略。

- 认证：`bearer`
- 范围：`app.entitlements:write`
- operationId：`cancelEntitlementReservation`

**参数**

| 名称 | 位置 | 类型 | 必填 | 说明 |
|---|---|---|---|---|
| `id` | path | string | 是 | id |
| `K3-Dry-Run` | header | string |  | 传 1 时只预演：回答和真做一样，什么也不留下<br>可选值：`1` |
| `Idempotency-Key` | header | string |  | 每个 POST 都收。SDK 未传入时会生成 uuid v4。24 小时内同键同正文重放第一次的回答<br>最短 1，最长 255 |

**请求正文（必填）**（`application/json`）

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `reason` | string |  | 原因 |

**响应**

| 状态 | 说明 |
|---|---|
| 200 | 取消后的预留 |
| 401 | 见 [Unauthorized](#resp-unauthorized) |
| 403 | 见 [Forbidden](#resp-forbidden) |
| 404 | `not_found`：对应的记录不存在（客户中心没有这个客户，访客、服务或所指的东西不存在）。见 [NotFound](#resp-notfound) |
| 409 | 见 [Conflict](#resp-conflict) |
| 413 | `payload_too_large`：正文太大。见 [PayloadTooLarge](#resp-payloadtoolarge) |
| 422 | 服务拒绝，或字段不被接受。见 [Refused](#resp-refused) |
| 429 | 见 [RateLimited](#resp-ratelimited) |
| 500 | 见 [Internal](#resp-internal) |
| 501 | `capability_off`：这个部署（或这个宿主）不提供这项能力，正文的 `capability` 是它的范围。见 [CapabilityOff](#resp-capabilityoff) |
| 503 | `unavailable`：暂时不可用（这个部署不提供的能力是 501 `capability_off`）。见 [Unavailable](#resp-unavailable) |

**200 响应正文**（`application/json`）

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `id` | string | 是 | 预留 id |
| `grant` | string | 是 | 被留住的权益 id |
| `holder` | string | 是 | 客户 id |
| `times` | integer | 是 | 次数 |
| `scope` | string[] | 是 | 范围标签 |
| `orderCents` | integer | 是 | 订单金额 |
| `discountCents` | integer | 是 | 券减掉的金额 |
| `key` | string | 是 | 预留的键 |
| `ref` | string |  | 订单或预约 |
| `where` | string |  | 门店 |
| `by` | string | 是 | 谁预留的 |
| `byName` | string | 是 | 姓名 |
| `app` | string |  | 预留的应用 |
| `state` | string | 是 | live、confirmed、cancelled、expired |
| `expiresAt` | string(date-time) | 是 | 到期时间 |
| `redemption` | string |  | 已确认时的核销单 id |
| `reason` | string |  | 已取消时的原因 |
| `createdAt` | string(date-time) | 是 | 预留时间 |
| `replay` | boolean | 是 | 同一个 key 已预留过，这是当时的预留 |

### `GET /entitlements/granters`

列出其他应用声明的可发放项（granter）的名字

需要范围 `app.entitlements:read`。English: the names of what apps declare they can issue (`entitlements.grantable` in a manifest).

- 认证：`bearer`
- 范围：`app.entitlements:read`
- operationId：`listEntitlementGranters`

**响应**

| 状态 | 说明 |
|---|---|
| 200 | 名字 |
| 401 | 见 [Unauthorized](#resp-unauthorized) |
| 403 | 见 [Forbidden](#resp-forbidden) |
| 422 | 字段或参数不被接受（正文读不出来、有未知字段、缺必填、取值越界）。见 [Unprocessable](#resp-unprocessable) |
| 429 | 见 [RateLimited](#resp-ratelimited) |
| 500 | 见 [Internal](#resp-internal) |
| 501 | `capability_off`：这个部署（或这个宿主）不提供这项能力，正文的 `capability` 是它的范围。见 [CapabilityOff](#resp-capabilityoff) |
| 503 | `unavailable`：暂时不可用（这个部署不提供的能力是 501 `capability_off`）。见 [Unavailable](#resp-unavailable) |

**200 响应正文**（`application/json`）

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `names` | string[] | 是 | 已注册的名字，排序 |

### `POST /entitlements/granters/{name}/grants`

通过其他应用声明的 granter 发放

需要范围 `app.entitlements:write`。English: asks the app that declares the granter to issue something; at least once, once per key.

`key` 在正文里，没写时用请求头 `Idempotency-Key`，必填；`holder` 必填。没有应用声明这个名字是 404 `not_found`。`params` 是 granter 自己的词汇；`dry` 为 true 或带 `K3-Dry-Run: 1` 时只预演。返回 granter 给出的 `ref`。

- 认证：`bearer`
- 范围：`app.entitlements:write`
- operationId：`grantEntitlementVia`

**参数**

| 名称 | 位置 | 类型 | 必填 | 说明 |
|---|---|---|---|---|
| `name` | path | string | 是 | granter 名字（“应用.内容”） |
| `K3-Dry-Run` | header | string |  | 传 1 时只预演：回答和真做一样，什么也不留下<br>可选值：`1` |
| `Idempotency-Key` | header | string |  | 每个 POST 都收。SDK 未传入时会生成 uuid v4。24 小时内同键同正文重放第一次的回答<br>最短 1，最长 255 |

**请求正文（必填）**（`application/json`）

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `key` | string |  | 幂等键；省略时用请求头 Idempotency-Key。发放至少一次，同键必须得到同一个引用 |
| `holder` | string | 是 | 客户 id |
| `source` | string |  | 来源 |
| `origin` | string |  | k3:// 引用 |
| `paidCents` | integer |  | 付款 |
| `count` | integer |  | 数量 |
| `attribution` | object |  | 归因 |
| `params` | object |  | granter 自己的词汇：发什么 |
| `dry` | boolean |  | 只预演，什么也不留下 |

**响应**

| 状态 | 说明 |
|---|---|
| 200 | granter 的回答 |
| 401 | 见 [Unauthorized](#resp-unauthorized) |
| 403 | 见 [Forbidden](#resp-forbidden) |
| 404 | `not_found`：对应的记录不存在（客户中心没有这个客户，访客、服务或所指的东西不存在）。见 [NotFound](#resp-notfound) |
| 413 | `payload_too_large`：正文太大。见 [PayloadTooLarge](#resp-payloadtoolarge) |
| 422 | 服务拒绝，或字段不被接受。见 [Refused](#resp-refused) |
| 429 | 见 [RateLimited](#resp-ratelimited) |
| 500 | 见 [Internal](#resp-internal) |
| 501 | `capability_off`：这个部署（或这个宿主）不提供这项能力，正文的 `capability` 是它的范围。见 [CapabilityOff](#resp-capabilityoff) |
| 503 | `unavailable`：暂时不可用（这个部署不提供的能力是 501 `capability_off`）。见 [Unavailable](#resp-unavailable) |

**200 响应正文**（`application/json`）

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `granter` | string | 是 | granter 名字 |
| `ref` | string | 是 | 发出之物的 k3:// 引用 |

### `GET /entitlements/codes/{code}`

按券码读一项权益

需要范围 `app.entitlements:read`。English: the grant that carries the code.

不存在的券码和格式不对的券码得到同一个 404 `not_found`，所以猜码学不到任何东西。

- 认证：`bearer`
- 范围：`app.entitlements:read`
- operationId：`getEntitlementByCode`

**参数**

| 名称 | 位置 | 类型 | 必填 | 说明 |
|---|---|---|---|---|
| `code` | path | string | 是 | 券码 |

**响应**

| 状态 | 说明 |
|---|---|
| 200 | 权益 |
| 401 | 见 [Unauthorized](#resp-unauthorized) |
| 403 | 见 [Forbidden](#resp-forbidden) |
| 404 | `not_found`：对应的记录不存在（客户中心没有这个客户，访客、服务或所指的东西不存在）。见 [NotFound](#resp-notfound) |
| 429 | 见 [RateLimited](#resp-ratelimited) |
| 500 | 见 [Internal](#resp-internal) |
| 501 | `capability_off`：这个部署（或这个宿主）不提供这项能力，正文的 `capability` 是它的范围。见 [CapabilityOff](#resp-capabilityoff) |
| 503 | `unavailable`：暂时不可用（这个部署不提供的能力是 501 `capability_off`）。见 [Unavailable](#resp-unavailable) |

**200 响应正文**（`application/json`）

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `id` | string | 是 | 权益 id |
| `template` | string | 是 | 来源模板 id |
| `ownerApp` | string | 是 | 模板归属的应用 |
| `kind` | string | 是 | 种类 |
| `name` | string | 是 | 名称 |
| `params` | EntitlementParams | 是 | 种类相关的参数（字段名是 snake_case）；哪个种类读哪些字段由服务在保存模板时检查 |
| `params.total_times` | integer |  | 次数（count、count_period） |
| `params.valid_days` | integer |  | 有效天数（period、count_period 必填） |
| `params.valid_from` | string |  | "grant"（默认）或 "first_use" |
| `params.daily_cap` | integer |  | 每日上限（期卡） |
| `params.weekly_cap` | integer |  | 每周上限（期卡） |
| `params.starts_on` | string |  | 固定有效期开始，YYYY-MM-DD（券；覆盖 valid_days） |
| `params.ends_on` | string |  | 固定有效期结束，含当天 |
| `params.form` | string |  | 券的形式：cash、percent、service、code |
| `params.amount_off_cents` | integer |  | 满减金额（cash） |
| `params.percent` | integer |  | 折扣：减多少个百分点，1 到 99（percent） |
| `params.threshold_cents` | integer |  | 订单要达到的金额（cash、percent） |
| `params.max_off_cents` | integer |  | 最多减多少，0 为不封顶（percent） |
| `params.restore_on_refund` | boolean |  | 订单退款时把券还回去（由调用方读到后作废核销单） |
| `params.description` | string |  | 奖品说明 |
| `params.fulfilment` | string |  | 奖品如何交付（对平台不透明） |
| `scope` | string[] | 是 | 范围标签 |
| `transferable` | boolean | 是 | 可转让 |
| `freezable` | boolean | 是 | 可冻结 |
| `maxFreezeDays` | integer | 是 | 最多冻结天数 |
| `holder` | string | 是 | 持有的客户 id |
| `total` | integer | 是 | 总次数 |
| `remaining` | integer | 是 | 剩余次数 |
| `startsOn` | string | 是 | 开始日期 |
| `endsOn` | string | 是 | 结束日期 |
| `state` | string | 是 | 对它做过什么 |
| `status` | string | 是 | 对人说的状态：active、not_started、frozen、expired、used_up、used、refunded、voided、transferred（按今天算） |
| `frozenFrom` | string |  | 冻结开始 |
| `frozenUntil` | string |  | 冻结到 |
| `frozenDaysUsed` | integer | 是 | 已冻结天数 |
| `source` | string | 是 | 来源 |
| `paidCents` | integer | 是 | 付款 |
| `refundedCents` | integer | 是 | 已退 |
| `origin` | string |  | k3:// 引用 |
| `attribution` | object |  | 归因 |
| `code` | string |  | 券码 |
| `transferredFrom` | string |  | 转让自哪一项 |
| `held` | EntitlementHold |  | 一个未支付订单对权益的占用 |
| `held.id` | string | 是 | 占用 id |
| `held.grant` | string | 是 | 权益 id |
| `held.ref` | string | 是 | 占用它的订单 |
| `held.expiresAt` | string(date-time) | 是 | 占用到何时 |
| `createdBy` | string | 是 | 发放者 |
| `createdAt` | string(date-time) | 是 | 发放时间 |
| `ledger` | EntitlementLedgerEntry[] |  | 只在读单项权益时有 |
| `ledger[].id` | integer | 是 | 流水 id |
| `ledger[].kind` | string | 是 | 变动的种类 |
| `ledger[].delta` | integer | 是 | 次数的增减 |
| `ledger[].balance` | integer | 是 | 变动后的余额 |
| `ledger[].actorName` | string | 是 | 谁做的 |
| `ledger[].reason` | string | 是 | 原因 |
| `ledger[].ref` | string |  | 关联的订单或预约 |
| `ledger[].redemptionId` | string |  | 关联的核销单 |
| `ledger[].at` | string(date-time) | 是 | 时间 |

### `GET /entitlements/granters/{name}/choices`

列出一个 granter 能发放的东西

需要范围 `app.entitlements:read`。English: what the granter can issue, for a picker.

没有应用声明这个名字，或它不列出，`data` 是 `[]`。`priceCents` 在 granter 按价出售时给出。

- 认证：`bearer`
- 范围：`app.entitlements:read`
- operationId：`listEntitlementGranterChoices`

**参数**

| 名称 | 位置 | 类型 | 必填 | 说明 |
|---|---|---|---|---|
| `name` | path | string | 是 | granter 名字（“应用.内容”） |

**响应**

| 状态 | 说明 |
|---|---|
| 200 | 可发放项 |
| 401 | 见 [Unauthorized](#resp-unauthorized) |
| 403 | 见 [Forbidden](#resp-forbidden) |
| 429 | 见 [RateLimited](#resp-ratelimited) |
| 500 | 见 [Internal](#resp-internal) |
| 501 | `capability_off`：这个部署（或这个宿主）不提供这项能力，正文的 `capability` 是它的范围。见 [CapabilityOff](#resp-capabilityoff) |
| 503 | `unavailable`：暂时不可用（这个部署不提供的能力是 501 `capability_off`）。见 [Unavailable](#resp-unavailable) |

**200 响应正文**（`application/json`）

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `data` | GranterChoice[] | 是 |  |
| `data[].param` | string | 是 | 放进 params 的哪个键 |
| `data[].value` | string | 是 | 键的值 |
| `data[].label` | string | 是 | 给人看的名字 |
| `data[].summary` | string |  |  |
| `data[].priceCents` | integer(int64) |  | 按价出售时的价格；不按价出售时省略 |

### `POST /entitlements/granters/{name}/refunds`

请 granter 收回它发出的东西

需要范围 `app.entitlements:write`。English: asks the app that declares the granter to take back what it issued (an order refunds what it collected); idempotent by key.

`ref` 是发出之物的 `k3://` 引用。`amountCents` 省略时按 granter 自己的规则算现在值多少并收回，写了就按这个数收回。`key` 在正文里，没写时用请求头 `Idempotency-Key`，同键重来返回第一次的结果、不再收回。`preview` 为 true 或带 `K3-Dry-Run: 1` 时只回答值多少，什么也不收。没有应用声明这个名字是 404 `not_found`；granter 不收回是 409（错误码 not_refundable）；被拒绝时 422 用话说明原因。

- 认证：`bearer`
- 范围：`app.entitlements:write`
- operationId：`refundEntitlementVia`

**参数**

| 名称 | 位置 | 类型 | 必填 | 说明 |
|---|---|---|---|---|
| `name` | path | string | 是 | granter 名字（“应用.内容”） |
| `K3-Dry-Run` | header | string |  | 传 1 时只预演：回答和真做一样，什么也不留下<br>可选值：`1` |
| `Idempotency-Key` | header | string |  | 每个 POST 都收。SDK 未传入时会生成 uuid v4。24 小时内同键同正文重放第一次的回答<br>最短 1，最长 255 |

**请求正文（必填）**（`application/json`）

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `key` | string |  | 幂等键；省略时用请求头 Idempotency-Key |
| `preview` | boolean |  | 只回答值多少，什么也不收 |
| `ref` | string |  | 发出之物的 k3:// 引用 |
| `amountCents` | integer(int64) |  | 按这个数收回；省略时按 granter 自己的规则算 |
| `reason` | string |  |  |

**响应**

| 状态 | 说明 |
|---|---|
| 200 | 收回的结果 |
| 401 | 见 [Unauthorized](#resp-unauthorized) |
| 403 | 见 [Forbidden](#resp-forbidden) |
| 404 | `not_found`：对应的记录不存在（客户中心没有这个客户，访客、服务或所指的东西不存在）。见 [NotFound](#resp-notfound) |
| 409 | 见 [Conflict](#resp-conflict) |
| 413 | `payload_too_large`：正文太大。见 [PayloadTooLarge](#resp-payloadtoolarge) |
| 422 | 服务拒绝，或字段不被接受。见 [Refused](#resp-refused) |
| 429 | 见 [RateLimited](#resp-ratelimited) |
| 500 | 见 [Internal](#resp-internal) |
| 501 | `capability_off`：这个部署（或这个宿主）不提供这项能力，正文的 `capability` 是它的范围。见 [CapabilityOff](#resp-capabilityoff) |
| 503 | `unavailable`：暂时不可用（这个部署不提供的能力是 501 `capability_off`）。见 [Unavailable](#resp-unavailable) |

**200 响应正文**（`application/json`）

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `amountCents` | integer(int64) |  | 现在值多少 |
| `effect` | string |  | 给员工看的话 |

## secrets

平台密钥库：用每个主体一把的钥匙加密、解密、销毁本应用的个人数据

### `POST /secrets/encrypt`

用这个主体的钥匙加密本应用的一段个人数据

需要范围 `app.secrets:use`。English: encrypts personal data under the key of one subject of the calling app.

`subject` 是本应用自己的 `<种类>:<id>`（种类是小写字母、数字和 -，以字母开头，最多 40 字符；id 最多 200 字符），否则 422 `invalid`。平台把它归到本应用和令牌的组织下，应用碰不到别的应用、别的组织的钥匙。`plaintext` 是 base64。正文最多 64 KiB（明文约 48 KiB），超出是 413 `payload_too_large`。返回的 `ciphertext`（base64）由应用自己保存。平台没有密钥库时是 503。

- 认证：`bearer`
- 范围：`app.secrets:use`
- operationId：`encryptSecret`

**参数**

| 名称 | 位置 | 类型 | 必填 | 说明 |
|---|---|---|---|---|
| `Idempotency-Key` | header | string |  | 每个 POST 都收。SDK 未传入时会生成 uuid v4。24 小时内同键同正文重放第一次的回答<br>最短 1，最长 255 |

**请求正文（必填）**（`application/json`）

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `subject` | string | 是 | 本应用的主体 &lt;种类>:&lt;id>，例如 recipient:f_123 |
| `plaintext` | string(byte) | 是 | 明文，base64；正文最多 64 KiB（明文约 48 KiB） |

**响应**

| 状态 | 说明 |
|---|---|
| 200 | 密文 |
| 401 | 见 [Unauthorized](#resp-unauthorized) |
| 403 | 见 [Forbidden](#resp-forbidden) |
| 413 | `payload_too_large`：正文太大。见 [PayloadTooLarge](#resp-payloadtoolarge) |
| 422 | 字段或参数不被接受（正文读不出来、有未知字段、缺必填、取值越界）。见 [Unprocessable](#resp-unprocessable) |
| 429 | 见 [RateLimited](#resp-ratelimited) |
| 500 | 见 [Internal](#resp-internal) |
| 501 | `capability_off`：这个部署（或这个宿主）不提供这项能力，正文的 `capability` 是它的范围。见 [CapabilityOff](#resp-capabilityoff) |
| 503 | `unavailable`：暂时不可用（这个部署不提供的能力是 501 `capability_off`）。见 [Unavailable](#resp-unavailable) |

**200 响应正文**（`application/json`）

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `ciphertext` | string(byte) | 是 | 密文，base64；由应用保存 |

### `POST /secrets/decrypt`

用这个主体的钥匙解密本应用保存的密文

需要范围 `app.secrets:use`。English: decrypts a ciphertext of one subject of the calling app; the answer is not cached (Cache-Control: no-store).

`subject` 同加密时一样。`ciphertext` 是 base64，正文最多 64 KiB。钥匙已销毁，或密文不是这个主体的，都是 409 `conflict`，不说是哪一种。回答带 `Cache-Control: no-store`。解密的回答不为 `Idempotency-Key` 保存（带了也忽略，明文不留在平台）：它不改变任何东西，重试就是再问一次。

- 认证：`bearer`
- 范围：`app.secrets:use`
- operationId：`decryptSecret`

**参数**

| 名称 | 位置 | 类型 | 必填 | 说明 |
|---|---|---|---|---|
| `Idempotency-Key` | header | string |  | 每个 POST 都收。SDK 未传入时会生成 uuid v4。24 小时内同键同正文重放第一次的回答<br>最短 1，最长 255 |

**请求正文（必填）**（`application/json`）

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `subject` | string | 是 | 加密时的主体 &lt;种类>:&lt;id> |
| `ciphertext` | string(byte) | 是 | 加密时得到的密文，base64 |

**响应**

| 状态 | 说明 |
|---|---|
| 200 | 明文 |
| 401 | 见 [Unauthorized](#resp-unauthorized) |
| 403 | 见 [Forbidden](#resp-forbidden) |
| 409 | 见 [Conflict](#resp-conflict) |
| 413 | `payload_too_large`：正文太大。见 [PayloadTooLarge](#resp-payloadtoolarge) |
| 422 | 字段或参数不被接受（正文读不出来、有未知字段、缺必填、取值越界）。见 [Unprocessable](#resp-unprocessable) |
| 429 | 见 [RateLimited](#resp-ratelimited) |
| 500 | 见 [Internal](#resp-internal) |
| 501 | `capability_off`：这个部署（或这个宿主）不提供这项能力，正文的 `capability` 是它的范围。见 [CapabilityOff](#resp-capabilityoff) |
| 503 | `unavailable`：暂时不可用（这个部署不提供的能力是 501 `capability_off`）。见 [Unavailable](#resp-unavailable) |

**200 响应正文**（`application/json`）

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `plaintext` | string(byte) | 是 | 明文，base64 |

### `POST /secrets/shred`

永久销毁这个主体的钥匙（它的密文从此解不开）

需要范围 `app.secrets:use`。English: destroys the key of one subject of the calling app for good; its ciphertexts can no longer be decrypted.

`subject` 同加密时一样。销毁不能撤回；之后解密这个主体的密文是 409 `conflict`。成功回答 204，没有正文。

- 认证：`bearer`
- 范围：`app.secrets:use`
- operationId：`shredSecret`

**参数**

| 名称 | 位置 | 类型 | 必填 | 说明 |
|---|---|---|---|---|
| `Idempotency-Key` | header | string |  | 每个 POST 都收。SDK 未传入时会生成 uuid v4。24 小时内同键同正文重放第一次的回答<br>最短 1，最长 255 |

**请求正文（必填）**（`application/json`）

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `subject` | string | 是 | 本应用的主体 &lt;种类>:&lt;id> |

**响应**

| 状态 | 说明 |
|---|---|
| 204 | 已销毁。没有正文 |
| 401 | 见 [Unauthorized](#resp-unauthorized) |
| 403 | 见 [Forbidden](#resp-forbidden) |
| 413 | `payload_too_large`：正文太大。见 [PayloadTooLarge](#resp-payloadtoolarge) |
| 422 | 字段或参数不被接受（正文读不出来、有未知字段、缺必填、取值越界）。见 [Unprocessable](#resp-unprocessable) |
| 429 | 见 [RateLimited](#resp-ratelimited) |
| 500 | 见 [Internal](#resp-internal) |
| 501 | `capability_off`：这个部署（或这个宿主）不提供这项能力，正文的 `capability` 是它的范围。见 [CapabilityOff](#resp-capabilityoff) |
| 503 | `unavailable`：暂时不可用（这个部署不提供的能力是 501 `capability_off`）。见 [Unavailable](#resp-unavailable) |

## sites

企业站点的名称、状态与访问量

### `POST /sites/summaries`

读企业站点的名称、状态和一段日期里的访问量

需要范围 `app.sites:read`。English: the name, state and request count of the tenant's sites, one answer per request, in order.

`requests` 最多 200 条，超出是 422。每条问一个站点从 `from` 那天到 `to` 那天（省略：到今天）的访问量。不存在的站点回答空名字、0 次访问、没有 `state`。平台没有接上站点时是 501 `capability_off`。

- 认证：`bearer`
- 范围：`app.sites:read`
- operationId：`summarizeSites`

**参数**

| 名称 | 位置 | 类型 | 必填 | 说明 |
|---|---|---|---|---|
| `Idempotency-Key` | header | string |  | 每个 POST 都收。SDK 未传入时会生成 uuid v4。24 小时内同键同正文重放第一次的回答<br>最短 1，最长 255 |

**请求正文（必填）**（`application/json`）

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `requests` | SiteUsageRequest[] | 是 | 最多 200 项 |
| `requests[].site` | string | 是 | 站点 id |
| `requests[].from` | string(date-time) | 是 | 从这一天（含） |
| `requests[].to` | string(date-time) |  | 到这一天（含）；省略为到今天 |

**响应**

| 状态 | 说明 |
|---|---|
| 200 | 每条请求一个回答，顺序相同 |
| 401 | 见 [Unauthorized](#resp-unauthorized) |
| 403 | 见 [Forbidden](#resp-forbidden) |
| 413 | `payload_too_large`：正文太大。见 [PayloadTooLarge](#resp-payloadtoolarge) |
| 422 | 字段或参数不被接受（正文读不出来、有未知字段、缺必填、取值越界）。见 [Unprocessable](#resp-unprocessable) |
| 429 | 见 [RateLimited](#resp-ratelimited) |
| 500 | 见 [Internal](#resp-internal) |
| 501 | `capability_off`：这个部署（或这个宿主）不提供这项能力，正文的 `capability` 是它的范围。见 [CapabilityOff](#resp-capabilityoff) |
| 503 | `unavailable`：暂时不可用（这个部署不提供的能力是 501 `capability_off`）。见 [Unavailable](#resp-unavailable) |

**200 响应正文**（`application/json`）

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `summaries` | SiteSummary[] | 是 |  |
| `summaries[].site` | string | 是 | 站点 id |
| `summaries[].name` | string | 是 | 站点名称；站点不存在时为空 |
| `summaries[].requests` | integer(int64) | 是 | 这段日期里的访问次数 |
| `summaries[].state` | string |  | 访客现在看到什么：live 在线（liveNumber 是在线的版本），unpublished 从没发布过，ended 已下线（显示活动已结束，自 endedAt 起），disabled 地址打不开。站点不存在时没有<br>可选值：`live`、`unpublished`、`ended`、`disabled` |
| `summaries[].liveNumber` | integer |  | 在线的版本号 |
| `summaries[].endedAt` | string(date-time) |  | 下线时间 |

## stores

组织里的门店，以及某人所在或管理的门店（只读）

### `GET /stores`

列出组织里的门店，或某人所在、所管的门店

需要范围 `app.stores:read`。English: every store of the tenant in the tree's order, closed ones included; with `of`, the stores that person works at or manages.

省略 `of`：组织的全部门店（含已关闭的），按组织树的顺序。`of` 是人员的 subject：他所属或被任命的每个部门及其下面的门店（店员是自己的店，任命在大区的经理是大区下的每家店）。门店由平台维护，应用只读，用 `id`（部门 id）引用。平台没有接上门店时是 501 `capability_off`。

- 认证：`bearer`
- 范围：`app.stores:read`
- operationId：`listStores`

**参数**

| 名称 | 位置 | 类型 | 必填 | 说明 |
|---|---|---|---|---|
| `of` | query | string |  | 人员的 subject；省略为全部门店 |

**响应**

| 状态 | 说明 |
|---|---|
| 200 | 门店 |
| 401 | 见 [Unauthorized](#resp-unauthorized) |
| 403 | 见 [Forbidden](#resp-forbidden) |
| 422 | 字段或参数不被接受（正文读不出来、有未知字段、缺必填、取值越界）。见 [Unprocessable](#resp-unprocessable) |
| 429 | 见 [RateLimited](#resp-ratelimited) |
| 500 | 见 [Internal](#resp-internal) |
| 501 | `capability_off`：这个部署（或这个宿主）不提供这项能力，正文的 `capability` 是它的范围。见 [CapabilityOff](#resp-capabilityoff) |
| 503 | `unavailable`：暂时不可用（这个部署不提供的能力是 501 `capability_off`）。见 [Unavailable](#resp-unavailable) |

**200 响应正文**（`application/json`）

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `stores` | Store[] | 是 |  |
| `stores[].id` | string | 是 | 部门 id；应用用它引用门店 |
| `stores[].code` | string |  | 门店编码 |
| `stores[].name` | string | 是 |  |
| `stores[].parentId` | string |  | 上级部门 id |
| `stores[].path` | string | 是 | 部门在组织树里的路径 |
| `stores[].region` | string |  | 门店上面的部门链，最上层在前，例如 山东省 / 鲁东区 / 济南 |
| `stores[].province` | string |  |  |
| `stores[].city` | string |  |  |
| `stores[].district` | string |  |  |
| `stores[].address` | string |  |  |
| `stores[].lat` | number(double) |  |  |
| `stores[].lng` | number(double) |  |  |
| `stores[].phone` | string |  |  |
| `stores[].hours` | string |  | 营业时间 |
| `stores[].status` | string | 是 | planning、open、paused 或 closed；门店资料还没填时为空 |
| `stores[].openedOn` | string |  | 开业日期 |

## realtime

实时文档：本应用在清单里声明的协同文档（创建、服务端改动、投影、删除），同步连接由平台在应用自己的域名上提供

### `POST /realtime/docs`

创建一份实时文档

需要范围 `app.realtime:write`。English: creates a real-time document of one of the kinds the app declares in `realtime.docs[]`, and registers it as an instance of the kind's resource type.

`kind` 必须是本应用清单 `realtime.docs[]` 里声明的种类，否则 422 `invalid`。`resource` 是它所属资源的 id（资源类型由种类声明），文档的权限就是调用者在这个资源上的级别（见 `resources` 接口）。`id` 可省略，平台取 `rd_…`；自己取的 id 只能用字母、数字、`-` 和 `_`，最多 64 字符。创建是幂等的：同一命名空间里已有同 id、同种类、同资源类型的文档时返回 200 和它的文档头（应用可以在自己提交之前先建、失败后重试）；同 id 的其他文档是 409 `conflict`。
初始内容按种类的编码给：`yjs-v1` 给 `doc`（ProseMirror 文档，省略是空文档），`ymap-v1` 给 `data`（JSON 对象，必填），`board-v1` 给 `board`（看板，必填），Office 编码给 `state`（base64，由读过文件的浏览器生成，必填）。正文最大 8 MiB。
返回 201 和文档头。页面会话只能用自己有权的资源；应用自己的令牌不受级别限制（文档归应用）。

- 认证：`bearer`
- 范围：`app.realtime:write`
- operationId：`createRealtimeDoc`

**参数**

| 名称 | 位置 | 类型 | 必填 | 说明 |
|---|---|---|---|---|
| `Idempotency-Key` | header | string |  | 每个 POST 都收。SDK 未传入时会生成 uuid v4。24 小时内同键同正文重放第一次的回答<br>最短 1，最长 255 |

**请求正文（必填）**（`application/json`）

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `kind` | string | 是 | 本应用清单 realtime.docs[] 里声明的种类 |
| `id` | string |  | 文档 id，字母、数字、- 和 _；省略由平台取<br>最长 64 |
| `resource` | string | 是 | 所属资源的 id（资源类型由种类声明）<br>最长 200 |
| `doc` | object |  | yjs-v1：ProseMirror 文档（省略是空文档） |
| `data` | object |  | ymap-v1：JSON 对象（必填） |
| `board` | object |  | board-v1：看板（必填） |
| `state` | string |  | Office 编码：base64 的初始状态（必填） |

**响应**

| 状态 | 说明 |
|---|---|
| 200 | 同 id、同种类的文档已在：它的文档头 |
| 201 | 文档已创建 |
| 401 | 见 [Unauthorized](#resp-unauthorized) |
| 403 | 见 [Forbidden](#resp-forbidden) |
| 404 | `not_found`：对应的记录不存在（客户中心没有这个客户，访客、服务或所指的东西不存在）。见 [NotFound](#resp-notfound) |
| 409 | 见 [Conflict](#resp-conflict) |
| 413 | `payload_too_large`：正文太大。见 [PayloadTooLarge](#resp-payloadtoolarge) |
| 422 | 字段或参数不被接受（正文读不出来、有未知字段、缺必填、取值越界）。见 [Unprocessable](#resp-unprocessable) |
| 429 | 见 [RateLimited](#resp-ratelimited) |
| 500 | 见 [Internal](#resp-internal) |
| 501 | `capability_off`：这个部署（或这个宿主）不提供这项能力，正文的 `capability` 是它的范围。见 [CapabilityOff](#resp-capabilityoff) |
| 503 | `unavailable`：暂时不可用（这个部署不提供的能力是 501 `capability_off`）。见 [Unavailable](#resp-unavailable) |

**200 响应正文**（`application/json`）

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `id` | string | 是 |  |
| `kind` | string | 是 | 清单声明的种类 |
| `codec` | string | 是 | 编码：yjs-v1、ymap-v1、board-v1 或 Office 编码 |
| `epoch` | integer | 是 | 纪元；每次重新开始加 1 |
| `schemaVer` | integer | 是 | 文档结构的版本 |
| `headSeq` | integer(int64) | 是 | 日志头的序号 |
| `projSeq` | integer(int64) | 是 | 最近一次投影所在的序号 |
| `resource` | RealtimeDocResource | 是 | 文档所属的资源 |
| `resource.type` | string | 是 | 资源类型，{应用 id}.{名字} |
| `resource.id` | string | 是 | 资源 id |
| `updatedAt` | string(date-time) | 是 |  |

**201 响应正文**（`application/json`）

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `id` | string | 是 |  |
| `kind` | string | 是 | 清单声明的种类 |
| `codec` | string | 是 | 编码：yjs-v1、ymap-v1、board-v1 或 Office 编码 |
| `epoch` | integer | 是 | 纪元；每次重新开始加 1 |
| `schemaVer` | integer | 是 | 文档结构的版本 |
| `headSeq` | integer(int64) | 是 | 日志头的序号 |
| `projSeq` | integer(int64) | 是 | 最近一次投影所在的序号 |
| `resource` | RealtimeDocResource | 是 | 文档所属的资源 |
| `resource.type` | string | 是 | 资源类型，{应用 id}.{名字} |
| `resource.id` | string | 是 | 资源 id |
| `updatedAt` | string(date-time) | 是 |  |

### `GET /realtime/docs/{id}`

读一份实时文档的头信息

需要范围 `app.realtime:read`。English: the header of one document: codec, epoch, schema version, the sequence of the log head and of the last projection, and its resource.

页面会话需要对所属资源至少有查看级别，没有时一律是 404 `not_found`（不暴露文档是否存在）。别的应用的文档、平台内置应用的文档也是 404。

- 认证：`bearer`
- 范围：`app.realtime:read`
- operationId：`getRealtimeDoc`

**参数**

| 名称 | 位置 | 类型 | 必填 | 说明 |
|---|---|---|---|---|
| `id` | path | string | 是 | 文档 id（`rd_…`，或创建时自己取的 id） |

**响应**

| 状态 | 说明 |
|---|---|
| 200 | 文档头 |
| 401 | 见 [Unauthorized](#resp-unauthorized) |
| 403 | 见 [Forbidden](#resp-forbidden) |
| 404 | `not_found`：对应的记录不存在（客户中心没有这个客户，访客、服务或所指的东西不存在）。见 [NotFound](#resp-notfound) |
| 429 | 见 [RateLimited](#resp-ratelimited) |
| 500 | 见 [Internal](#resp-internal) |
| 501 | `capability_off`：这个部署（或这个宿主）不提供这项能力，正文的 `capability` 是它的范围。见 [CapabilityOff](#resp-capabilityoff) |
| 503 | `unavailable`：暂时不可用（这个部署不提供的能力是 501 `capability_off`）。见 [Unavailable](#resp-unavailable) |

**200 响应正文**（`application/json`）

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `id` | string | 是 |  |
| `kind` | string | 是 | 清单声明的种类 |
| `codec` | string | 是 | 编码：yjs-v1、ymap-v1、board-v1 或 Office 编码 |
| `epoch` | integer | 是 | 纪元；每次重新开始加 1 |
| `schemaVer` | integer | 是 | 文档结构的版本 |
| `headSeq` | integer(int64) | 是 | 日志头的序号 |
| `projSeq` | integer(int64) | 是 | 最近一次投影所在的序号 |
| `resource` | RealtimeDocResource | 是 | 文档所属的资源 |
| `resource.type` | string | 是 | 资源类型，{应用 id}.{名字} |
| `resource.id` | string | 是 | 资源 id |
| `updatedAt` | string(date-time) | 是 |  |

### `DELETE /realtime/docs/{id}`

删除一份实时文档

需要范围 `app.realtime:write`。English: deletes the document, its log and its projection, and closes the open sync connections.

页面会话需要对所属资源有编辑级别以上，否则 404 `not_found`。资源的登记（访问名单）不随文档删除，由应用用 `DELETE /access/{type}/{id}` 另行删除。返回 204。

- 认证：`bearer`
- 范围：`app.realtime:write`
- operationId：`deleteRealtimeDoc`

**参数**

| 名称 | 位置 | 类型 | 必填 | 说明 |
|---|---|---|---|---|
| `id` | path | string | 是 | 文档 id（`rd_…`，或创建时自己取的 id） |

**响应**

| 状态 | 说明 |
|---|---|
| 204 | 已删除 |
| 401 | 见 [Unauthorized](#resp-unauthorized) |
| 403 | 见 [Forbidden](#resp-forbidden) |
| 404 | `not_found`：对应的记录不存在（客户中心没有这个客户，访客、服务或所指的东西不存在）。见 [NotFound](#resp-notfound) |
| 409 | 见 [Conflict](#resp-conflict) |
| 429 | 见 [RateLimited](#resp-ratelimited) |
| 500 | 见 [Internal](#resp-internal) |
| 501 | `capability_off`：这个部署（或这个宿主）不提供这项能力，正文的 `capability` 是它的范围。见 [CapabilityOff](#resp-capabilityoff) |
| 503 | `unavailable`：暂时不可用（这个部署不提供的能力是 501 `capability_off`）。见 [Unavailable](#resp-unavailable) |

### `POST /realtime/docs/{id}/edits`

由服务端改一份实时文档

需要范围 `app.realtime:write`。English: applies one server-side edit to the document as the caller, returns the sequence it landed at and the projection after it.

`edit` 里写且只写一种改动，按顺序取第一个出现的：`replace`（整份 ProseMirror 文档）、`append`（追加的块）、`title`、`data`（`ymap-v1`：整个对象）、`board`（`board-v1`：整个看板）。什么也没写是 422 `invalid`。`origin` 是 `draft`、`restore` 或 `system`（默认 `system`），说明这次改动的来历。`dry` 为 true 只预演：回答和真做一样，什么也不留下。
声明了 `guard` 的种类，写入前会先问应用（清单 `providers.realtimeGuard`），应用拒绝是 422 `invalid`。需要编辑级别；代理（agent）只读，是 422 `invalid`。正文最大 8 MiB。

- 认证：`bearer`
- 范围：`app.realtime:write`
- operationId：`editRealtimeDoc`

**参数**

| 名称 | 位置 | 类型 | 必填 | 说明 |
|---|---|---|---|---|
| `id` | path | string | 是 | 文档 id（`rd_…`，或创建时自己取的 id） |
| `Idempotency-Key` | header | string |  | 每个 POST 都收。SDK 未传入时会生成 uuid v4。24 小时内同键同正文重放第一次的回答<br>最短 1，最长 255 |

**请求正文（必填）**（`application/json`）

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `edit` | RealtimeEditBody | 是 | 一种改动。只写一项：replace、append、title、data、board 之一 |
| `edit.replace` | object |  | 整份 ProseMirror 文档 |
| `edit.append` | object[] |  | 追加到末尾的 ProseMirror 块 |
| `edit.title` | string |  | 新标题 |
| `edit.data` | object |  | ymap-v1：整个对象 |
| `edit.board` | object |  | board-v1：整个看板 |
| `origin` | string |  | 改动的来历，默认 system<br>可选值：`draft`、`restore`、`system` |
| `dry` | boolean |  | 为 true 只预演 |

**响应**

| 状态 | 说明 |
|---|---|
| 200 | 改动落在的序号和之后的投影 |
| 401 | 见 [Unauthorized](#resp-unauthorized) |
| 403 | 见 [Forbidden](#resp-forbidden) |
| 404 | `not_found`：对应的记录不存在（客户中心没有这个客户，访客、服务或所指的东西不存在）。见 [NotFound](#resp-notfound) |
| 409 | 见 [Conflict](#resp-conflict) |
| 413 | `payload_too_large`：正文太大。见 [PayloadTooLarge](#resp-payloadtoolarge) |
| 422 | 字段或参数不被接受（正文读不出来、有未知字段、缺必填、取值越界）。见 [Unprocessable](#resp-unprocessable) |
| 429 | 见 [RateLimited](#resp-ratelimited) |
| 500 | 见 [Internal](#resp-internal) |
| 501 | `capability_off`：这个部署（或这个宿主）不提供这项能力，正文的 `capability` 是它的范围。见 [CapabilityOff](#resp-capabilityoff) |
| 503 | `unavailable`：暂时不可用（这个部署不提供的能力是 501 `capability_off`）。见 [Unavailable](#resp-unavailable) |

**200 响应正文**（`application/json`）

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `seq` | integer(int64) | 是 | 改动落在的序号（预演时是将落在的序号） |
| `projection` | RealtimeProjection | 是 | 文档投影出的内容。doc、data、board 按编码出现一个 |
| `projection.seq` | integer(int64) | 是 | 投影所在的序号 |
| `projection.title` | string | 是 |  |
| `projection.plain` | string | 是 | 纯文本 |
| `projection.words` | integer | 是 | 字数 |
| `projection.refs` | RealtimeRef[] | 是 |  |
| `projection.refs[].ref` | string | 是 | 被引用的对象（k3:// 引用、页面链接或网址） |
| `projection.refs[].kind` | string |  |  |
| `projection.refs[].app` | string |  |  |
| `projection.refs[].block` | string |  | 所在顶层块的 blockId |
| `projection.refs[].via` | string | 是 | ref、refCard、pageLink 或 link |
| `projection.doc` | object |  | yjs-v1 与 Office 编码：ProseMirror JSON |
| `projection.data` | object |  | ymap-v1：对象 |
| `projection.board` | object |  | board-v1：看板 |

### `POST /realtime/docs/{id}/epoch`

让一份实时文档从新的内容重新开始（换纪元）

需要范围 `app.realtime:write`。English: starts the document over from the given content; epoch + 1, so open editors reconnect from the new state. Use it for restore and import.

只有 ProseMirror 文档（`yjs-v1`）能这样重开，`doc` 省略是空文档；其他编码是 422 `invalid`。需要编辑级别。

- 认证：`bearer`
- 范围：`app.realtime:write`
- operationId：`restartRealtimeDoc`

**参数**

| 名称 | 位置 | 类型 | 必填 | 说明 |
|---|---|---|---|---|
| `id` | path | string | 是 | 文档 id（`rd_…`，或创建时自己取的 id） |
| `Idempotency-Key` | header | string |  | 每个 POST 都收。SDK 未传入时会生成 uuid v4。24 小时内同键同正文重放第一次的回答<br>最短 1，最长 255 |

**请求正文（必填）**（`application/json`）

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `doc` | object |  | 新的 ProseMirror 文档；省略是空文档 |

**响应**

| 状态 | 说明 |
|---|---|
| 200 | 新的纪元 |
| 401 | 见 [Unauthorized](#resp-unauthorized) |
| 403 | 见 [Forbidden](#resp-forbidden) |
| 404 | `not_found`：对应的记录不存在（客户中心没有这个客户，访客、服务或所指的东西不存在）。见 [NotFound](#resp-notfound) |
| 409 | 见 [Conflict](#resp-conflict) |
| 413 | `payload_too_large`：正文太大。见 [PayloadTooLarge](#resp-payloadtoolarge) |
| 422 | 字段或参数不被接受（正文读不出来、有未知字段、缺必填、取值越界）。见 [Unprocessable](#resp-unprocessable) |
| 429 | 见 [RateLimited](#resp-ratelimited) |
| 500 | 见 [Internal](#resp-internal) |
| 501 | `capability_off`：这个部署（或这个宿主）不提供这项能力，正文的 `capability` 是它的范围。见 [CapabilityOff](#resp-capabilityoff) |
| 503 | `unavailable`：暂时不可用（这个部署不提供的能力是 501 `capability_off`）。见 [Unavailable](#resp-unavailable) |

**200 响应正文**（`application/json`）

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `epoch` | integer | 是 | 新的纪元 |

### `GET /realtime/docs/{id}/projection`

读一份实时文档现在的投影

需要范围 `app.realtime:read`。English: the projection now: title, plain text, word count, references, and the content in the codec's form.

内容的形状随编码：`yjs-v1` 与 Office 编码给 `doc`（ProseMirror JSON），`ymap-v1` 给 `data`，`board-v1` 给 `board`。需要查看级别，没有时 404 `not_found`。投影完成后平台会把事件 `realtime.doc.projected` 送给应用（见 webhooks）。
带 `minSeq` 时，平台先让投影至少到这个序号（必要时当场投影），最多等 `wait` 毫秒（不超过 5000）；到时还没到是 409 `behind`，稍后再试。English: with `minSeq`, the platform first brings the projection to at least that sequence, waiting up to `wait` ms (at most 5000); 409 `behind` when it is not there in time.

- 认证：`bearer`
- 范围：`app.realtime:read`
- operationId：`getRealtimeProjection`

**参数**

| 名称 | 位置 | 类型 | 必填 | 说明 |
|---|---|---|---|---|
| `id` | path | string | 是 | 文档 id（`rd_…`，或创建时自己取的 id） |
| `minSeq` | query | integer(int64) |  | 投影至少要到的序号<br>最小 0 |
| `wait` | query | integer |  | 等投影的最长时间（毫秒，最多 5000）<br>最小 0，最大 5000 |

**响应**

| 状态 | 说明 |
|---|---|
| 200 | 投影 |
| 401 | 见 [Unauthorized](#resp-unauthorized) |
| 403 | 见 [Forbidden](#resp-forbidden) |
| 404 | `not_found`：对应的记录不存在（客户中心没有这个客户，访客、服务或所指的东西不存在）。见 [NotFound](#resp-notfound) |
| 409 | 见 [Conflict](#resp-conflict) |
| 429 | 见 [RateLimited](#resp-ratelimited) |
| 500 | 见 [Internal](#resp-internal) |
| 501 | `capability_off`：这个部署（或这个宿主）不提供这项能力，正文的 `capability` 是它的范围。见 [CapabilityOff](#resp-capabilityoff) |
| 503 | `unavailable`：暂时不可用（这个部署不提供的能力是 501 `capability_off`）。见 [Unavailable](#resp-unavailable) |

**200 响应正文**（`application/json`）

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `seq` | integer(int64) | 是 | 投影所在的序号 |
| `title` | string | 是 |  |
| `plain` | string | 是 | 纯文本 |
| `words` | integer | 是 | 字数 |
| `refs` | RealtimeRef[] | 是 |  |
| `refs[].ref` | string | 是 | 被引用的对象（k3:// 引用、页面链接或网址） |
| `refs[].kind` | string |  |  |
| `refs[].app` | string |  |  |
| `refs[].block` | string |  | 所在顶层块的 blockId |
| `refs[].via` | string | 是 | ref、refCard、pageLink 或 link |
| `doc` | object |  | yjs-v1 与 Office 编码：ProseMirror JSON |
| `data` | object |  | ymap-v1：对象 |
| `board` | object |  | board-v1：看板 |

### `GET /realtime/docs/{id}/state`

读一份实时文档完整的 CRDT 状态

需要范围 `app.realtime:read`。English: the document's whole CRDT state as it is now (its snapshot with every logged update merged), base64, and the sequence it is at.

应用把它存成一个版本（发布的副本），或交给只读的查看页。页面会话需要查看级别，没有时 404 `not_found`。

- 认证：`bearer`
- 范围：`app.realtime:read`
- operationId：`getRealtimeState`

**参数**

| 名称 | 位置 | 类型 | 必填 | 说明 |
|---|---|---|---|---|
| `id` | path | string | 是 | 文档 id（`rd_…`，或创建时自己取的 id） |

**响应**

| 状态 | 说明 |
|---|---|
| 200 | 完整状态 |
| 401 | 见 [Unauthorized](#resp-unauthorized) |
| 403 | 见 [Forbidden](#resp-forbidden) |
| 404 | `not_found`：对应的记录不存在（客户中心没有这个客户，访客、服务或所指的东西不存在）。见 [NotFound](#resp-notfound) |
| 429 | 见 [RateLimited](#resp-ratelimited) |
| 500 | 见 [Internal](#resp-internal) |
| 501 | `capability_off`：这个部署（或这个宿主）不提供这项能力，正文的 `capability` 是它的范围。见 [CapabilityOff](#resp-capabilityoff) |
| 503 | `unavailable`：暂时不可用（这个部署不提供的能力是 501 `capability_off`）。见 [Unavailable](#resp-unavailable) |

**200 响应正文**（`application/json`）

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `seq` | integer(int64) | 是 | 状态所在的序号（日志头） |
| `codec` | string | 是 | 编码 |
| `state` | string | 是 | base64 的状态 |

### `POST /realtime/docs/{id}/scrub`

把一份实时文档整理到平台现在的结构

需要范围 `app.realtime:write`。English: brings the document to the server's schema: removes from the CRDT what the schema no longer stores (a repair update) and compacts the log.

只收应用自己的令牌（页面会话是 403 `forbidden`）；每个应用在每个企业有调用额度（每秒 20 次，峰值 200），超过是 429，带 `Retry-After`。文档正在别处投影或压缩时是 409 `conflict`，稍后再试。

- 认证：`bearer`
- 范围：`app.realtime:write`
- operationId：`scrubRealtimeDoc`

**参数**

| 名称 | 位置 | 类型 | 必填 | 说明 |
|---|---|---|---|---|
| `id` | path | string | 是 | 文档 id（`rd_…`，或创建时自己取的 id） |
| `Idempotency-Key` | header | string |  | 每个 POST 都收。SDK 未传入时会生成 uuid v4。24 小时内同键同正文重放第一次的回答<br>最短 1，最长 255 |

**响应**

| 状态 | 说明 |
|---|---|
| 200 | 整理的结果 |
| 401 | 见 [Unauthorized](#resp-unauthorized) |
| 403 | 见 [Forbidden](#resp-forbidden) |
| 404 | `not_found`：对应的记录不存在（客户中心没有这个客户，访客、服务或所指的东西不存在）。见 [NotFound](#resp-notfound) |
| 409 | 见 [Conflict](#resp-conflict) |
| 429 | 见 [RateLimited](#resp-ratelimited) |
| 500 | 见 [Internal](#resp-internal) |
| 501 | `capability_off`：这个部署（或这个宿主）不提供这项能力，正文的 `capability` 是它的范围。见 [CapabilityOff](#resp-capabilityoff) |
| 503 | `unavailable`：暂时不可用（这个部署不提供的能力是 501 `capability_off`）。见 [Unavailable](#resp-unavailable) |

**200 响应正文**（`application/json`）

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `repaired` | boolean | 是 | 写了一次修复（CRDT 里有结构不再保存的内容） |
| `compacted` | boolean | 是 | 日志压缩成了快照 |

### `POST /realtime/docs/invalidate`

让打开这些文档的连接重新核对权限

需要范围 `app.realtime:write`。English: re-authorises the sync connections that have these documents open (after a permission change, a move or a restore).

只收应用自己的令牌（页面会话是 403 `forbidden`）；每个应用在每个企业有调用额度（每秒 20 次，峰值 200），超过是 429，带 `Retry-After`。`ids` 1 到 100 个，都必须是这个种类在它的命名空间里的文档，否则 404 `not_found`（别的应用的文档不在这里）。返回 202。

- 认证：`bearer`
- 范围：`app.realtime:write`
- operationId：`invalidateRealtimeDocs`

**参数**

| 名称 | 位置 | 类型 | 必填 | 说明 |
|---|---|---|---|---|
| `Idempotency-Key` | header | string |  | 每个 POST 都收。SDK 未传入时会生成 uuid v4。24 小时内同键同正文重放第一次的回答<br>最短 1，最长 255 |

**请求正文（必填）**（`application/json`）

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `kind` | string | 是 | 本应用清单 realtime.docs[] 里声明的种类 |
| `ids` | string[] | 是 | 文档 id<br>最多 100 项 |

**响应**

| 状态 | 说明 |
|---|---|
| 202 | 已通知 |
| 401 | 见 [Unauthorized](#resp-unauthorized) |
| 403 | 见 [Forbidden](#resp-forbidden) |
| 404 | `not_found`：对应的记录不存在（客户中心没有这个客户，访客、服务或所指的东西不存在）。见 [NotFound](#resp-notfound) |
| 422 | 字段或参数不被接受（正文读不出来、有未知字段、缺必填、取值越界）。见 [Unprocessable](#resp-unprocessable) |
| 429 | 见 [RateLimited](#resp-ratelimited) |
| 500 | 见 [Internal](#resp-internal) |
| 501 | `capability_off`：这个部署（或这个宿主）不提供这项能力，正文的 `capability` 是它的范围。见 [CapabilityOff](#resp-capabilityoff) |
| 503 | `unavailable`：暂时不可用（这个部署不提供的能力是 501 `capability_off`）。见 [Unavailable](#resp-unavailable) |

### `POST /realtime/docs/revoke`

从所有连接上收回这些文档

需要范围 `app.realtime:write`。English: takes these documents away from every sync connection, with a reason: forbidden, trashed or deleted.

只收应用自己的令牌（页面会话是 403 `forbidden`）；每个应用在每个企业有调用额度（每秒 20 次，峰值 200），超过是 429，带 `Retry-After`。`reason` 是 `forbidden`（默认）、`trashed` 或 `deleted`。之后应用还要让这些文档的资源对人不再可见（否则重新连上又能打开）；`DELETE /realtime/docs/{id}` 已经收回了被删的文档。`ids` 的规则同上。返回 202。

- 认证：`bearer`
- 范围：`app.realtime:write`
- operationId：`revokeRealtimeDocs`

**参数**

| 名称 | 位置 | 类型 | 必填 | 说明 |
|---|---|---|---|---|
| `Idempotency-Key` | header | string |  | 每个 POST 都收。SDK 未传入时会生成 uuid v4。24 小时内同键同正文重放第一次的回答<br>最短 1，最长 255 |

**请求正文（必填）**（`application/json`）

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `kind` | string | 是 | 本应用清单 realtime.docs[] 里声明的种类 |
| `ids` | string[] | 是 | 文档 id<br>最多 100 项 |
| `reason` | string |  | 原因，默认 forbidden<br>可选值：`forbidden`、`trashed`、`deleted` |

**响应**

| 状态 | 说明 |
|---|---|
| 202 | 已收回 |
| 401 | 见 [Unauthorized](#resp-unauthorized) |
| 403 | 见 [Forbidden](#resp-forbidden) |
| 404 | `not_found`：对应的记录不存在（客户中心没有这个客户，访客、服务或所指的东西不存在）。见 [NotFound](#resp-notfound) |
| 422 | 字段或参数不被接受（正文读不出来、有未知字段、缺必填、取值越界）。见 [Unprocessable](#resp-unprocessable) |
| 429 | 见 [RateLimited](#resp-ratelimited) |
| 500 | 见 [Internal](#resp-internal) |
| 501 | `capability_off`：这个部署（或这个宿主）不提供这项能力，正文的 `capability` 是它的范围。见 [CapabilityOff](#resp-capabilityoff) |
| 503 | `unavailable`：暂时不可用（这个部署不提供的能力是 501 `capability_off`）。见 [Unavailable](#resp-unavailable) |

### `POST /realtime/topics/{topic}/publish`

告诉一个主题的订阅者它变了

需要范围 `app.realtime:write`。English: tells the sync clients subscribed to a topic that it changed (they refetch over REST); fire and forget.

只收应用自己的令牌（页面会话是 403 `forbidden`）；每个应用在每个企业有调用额度（每秒 20 次，峰值 200），超过是 429，带 `Retry-After`。`topic` 是 `<资源类型>:<id>`（`<资源类型>:*` 是这个类型的列表），类型必须在这个种类的 `topics` 里声明，否则 404 `not_found`（别的应用的类型也是）。`version` 原样交给订阅者。返回 202。

- 认证：`bearer`
- 范围：`app.realtime:write`
- operationId：`publishRealtimeTopic`

**参数**

| 名称 | 位置 | 类型 | 必填 | 说明 |
|---|---|---|---|---|
| `topic` | path | string | 是 | 主题：`<资源类型>:<id>` |
| `Idempotency-Key` | header | string |  | 每个 POST 都收。SDK 未传入时会生成 uuid v4。24 小时内同键同正文重放第一次的回答<br>最短 1，最长 255 |

**请求正文（必填）**（`application/json`）

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `kind` | string | 是 | 声明了这个主题类型（topics）的种类 |
| `version` | integer(int64) |  | 交给订阅者的版本号 |

**响应**

| 状态 | 说明 |
|---|---|
| 202 | 已发出 |
| 401 | 见 [Unauthorized](#resp-unauthorized) |
| 403 | 见 [Forbidden](#resp-forbidden) |
| 404 | `not_found`：对应的记录不存在（客户中心没有这个客户，访客、服务或所指的东西不存在）。见 [NotFound](#resp-notfound) |
| 422 | 字段或参数不被接受（正文读不出来、有未知字段、缺必填、取值越界）。见 [Unprocessable](#resp-unprocessable) |
| 429 | 见 [RateLimited](#resp-ratelimited) |
| 500 | 见 [Internal](#resp-internal) |
| 501 | `capability_off`：这个部署（或这个宿主）不提供这项能力，正文的 `capability` 是它的范围。见 [CapabilityOff](#resp-capabilityoff) |
| 503 | `unavailable`：暂时不可用（这个部署不提供的能力是 501 `capability_off`）。见 [Unavailable](#resp-unavailable) |

### `POST /realtime/connections/invalidate`

让本企业的连接重新核对权限

需要范围 `app.realtime:write`。English: re-authorises every sync connection of the tenant (roles, departments or members changed), of one kind or, when kind is omitted, of every kind.

只收应用自己的令牌（页面会话是 403 `forbidden`）；每个应用在每个企业有调用额度（每秒 20 次，峰值 200），超过是 429，带 `Retry-After`。正文可省略（省略即所有种类）。返回 202。

- 认证：`bearer`
- 范围：`app.realtime:write`
- operationId：`invalidateRealtimeConnections`

**参数**

| 名称 | 位置 | 类型 | 必填 | 说明 |
|---|---|---|---|---|
| `Idempotency-Key` | header | string |  | 每个 POST 都收。SDK 未传入时会生成 uuid v4。24 小时内同键同正文重放第一次的回答<br>最短 1，最长 255 |

**请求正文**（`application/json`）

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `kind` | string |  | 种类；省略是所有种类 |

**响应**

| 状态 | 说明 |
|---|---|
| 202 | 已通知 |
| 401 | 见 [Unauthorized](#resp-unauthorized) |
| 403 | 见 [Forbidden](#resp-forbidden) |
| 404 | `not_found`：对应的记录不存在（客户中心没有这个客户，访客、服务或所指的东西不存在）。见 [NotFound](#resp-notfound) |
| 422 | 字段或参数不被接受（正文读不出来、有未知字段、缺必填、取值越界）。见 [Unprocessable](#resp-unprocessable) |
| 429 | 见 [RateLimited](#resp-ratelimited) |
| 500 | 见 [Internal](#resp-internal) |
| 501 | `capability_off`：这个部署（或这个宿主）不提供这项能力，正文的 `capability` 是它的范围。见 [CapabilityOff](#resp-capabilityoff) |
| 503 | `unavailable`：暂时不可用（这个部署不提供的能力是 501 `capability_off`）。见 [Unavailable](#resp-unavailable) |

### `POST /realtime/connections/uninstalled`

关闭本应用在本企业的所有连接

需要范围 `app.realtime:write`。English: closes every sync connection of the app in the tenant (close code 4004), of every kind; another app sharing a namespace keeps its own.

只收应用自己的令牌（页面会话是 403 `forbidden`）；每个应用在每个企业有调用额度（每秒 20 次，峰值 200），超过是 429，带 `Retry-After`。返回 202。

- 认证：`bearer`
- 范围：`app.realtime:write`
- operationId：`closeRealtimeConnections`

**参数**

| 名称 | 位置 | 类型 | 必填 | 说明 |
|---|---|---|---|---|
| `Idempotency-Key` | header | string |  | 每个 POST 都收。SDK 未传入时会生成 uuid v4。24 小时内同键同正文重放第一次的回答<br>最短 1，最长 255 |

**响应**

| 状态 | 说明 |
|---|---|
| 202 | 已关闭 |
| 401 | 见 [Unauthorized](#resp-unauthorized) |
| 403 | 见 [Forbidden](#resp-forbidden) |
| 404 | `not_found`：对应的记录不存在（客户中心没有这个客户，访客、服务或所指的东西不存在）。见 [NotFound](#resp-notfound) |
| 429 | 见 [RateLimited](#resp-ratelimited) |
| 500 | 见 [Internal](#resp-internal) |
| 501 | `capability_off`：这个部署（或这个宿主）不提供这项能力，正文的 `capability` 是它的范围。见 [CapabilityOff](#resp-capabilityoff) |
| 503 | `unavailable`：暂时不可用（这个部署不提供的能力是 501 `capability_off`）。见 [Unavailable](#resp-unavailable) |

### `POST /realtime/codecs/{codec}/meta`

用平台的编码读一份 Office 状态的属性

需要范围 `app.realtime:read`。English: reads the properties of an Office state (the title, the hashes of the file's preserved parts, the engine) with the platform's codec. Codecs run only in the platform, so an app outside it checks an import here: the browser built the state from the file, and the app compares the state's properties with the file before it creates the document.

只收应用自己的令牌（页面会话是 403 `forbidden`）；每个应用在每个企业有调用额度（每秒 20 次，峰值 200），超过是 429，带 `Retry-After`。`codec` 必须是本应用清单 `realtime.docs[]` 里某个种类的编码，否则 404 `not_found`。只有 Office 编码（`quire-v1`、`ledger-v1`、`lectern-v1`）有属性；别的编码、读不了的状态是 422 `invalid`。`state` 是 base64，请求正文最大 24 MiB。English: the app's own token only, within the control budget; the codec must be one the app's manifest declares; only the Office codecs have properties; the body is at most 24 MiB.

- 认证：`bearer`
- 范围：`app.realtime:read`
- operationId：`readRealtimeCodecMeta`

**参数**

| 名称 | 位置 | 类型 | 必填 | 说明 |
|---|---|---|---|---|
| `codec` | path | string | 是 | 编码：`quire-v1`（Word）、`ledger-v1`（Excel）、`lectern-v1`（PowerPoint） |
| `Idempotency-Key` | header | string |  | 每个 POST 都收。SDK 未传入时会生成 uuid v4。24 小时内同键同正文重放第一次的回答<br>最短 1，最长 255 |

**请求正文（必填）**（`application/json`）

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `state` | string | 是 | base64 的状态（Yjs update v1，浏览器读文件时建的） |

**响应**

| 状态 | 说明 |
|---|---|
| 200 | 状态的属性 |
| 401 | 见 [Unauthorized](#resp-unauthorized) |
| 403 | 见 [Forbidden](#resp-forbidden) |
| 404 | `not_found`：对应的记录不存在（客户中心没有这个客户，访客、服务或所指的东西不存在）。见 [NotFound](#resp-notfound) |
| 413 | `payload_too_large`：正文太大。见 [PayloadTooLarge](#resp-payloadtoolarge) |
| 422 | 字段或参数不被接受（正文读不出来、有未知字段、缺必填、取值越界）。见 [Unprocessable](#resp-unprocessable) |
| 429 | 见 [RateLimited](#resp-ratelimited) |
| 500 | 见 [Internal](#resp-internal) |
| 501 | `capability_off`：这个部署（或这个宿主）不提供这项能力，正文的 `capability` 是它的范围。见 [CapabilityOff](#resp-capabilityoff) |
| 503 | `unavailable`：暂时不可用（这个部署不提供的能力是 501 `capability_off`）。见 [Unavailable](#resp-unavailable) |

**200 响应正文**（`application/json`）

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `meta` | map[string, string] | 是 | 属性：`title`、`base` / `fragments`（文件保留部分的 SHA-256）、`engine` 等 |

## resources

资源与分享：本应用声明的资源类型，由平台保存所有者、名单、通用访问、开关和申请，并批量回答谁能看；带 parent 的类型走树模型（继承的节点名单、状态、集合）

### `GET /access/{type}/{id}`

读一个资源的访问名单（按调用者能看到的）

需要范围 `app.resources:read`。English: the list of one resource as the caller may see it: owner, general access, switches, grants, the caller's level.

正文与规则同平台内置应用的 `/api/access/{type}/{id}…`（同一套访问内核：规则、审计、通知都一样）。只收页面会话：应用自己的令牌是 403 `forbidden`（分享是人做的事）。类型不是本应用声明的，或资源没登记，一律 404 `not_found`。名单变了以后，平台把事件 `access.changed` 送给应用（见 webhooks）。

平铺类型上，应用自己的令牌可以读整个名单（包括已过期的条目，应用按自己的时钟判断）：`me` 为 `manage`。用于应用按自己的权限点决定谁能授权的类型（例如会员的敏感记录）。English: on a flat type the app's own token reads the whole list, expired grants included.

调用者对资源没有查看级别时是 404 `not_found`，要申请访问见 `POST …/requests`。

树类型：返回 `AccessTreeView`：显式名单 `entries`、根或节点指向的集合 `set`、调用者经状态限制后的级别 `level` 和原因 `reason`。页面会话没有查看级别时 404 `not_found`；应用自己的令牌可带 `user` 查某人的级别（不带就只有名单和集合），根或节点不存在时 404 `not_found`。English: on a tree type the answer is the explicit entries, the set and the caller's level after the states.

- 认证：`bearer`
- 范围：`app.resources:read`
- operationId：`getResourceAccess`

**参数**

| 名称 | 位置 | 类型 | 必填 | 说明 |
|---|---|---|---|---|
| `type` | path | string | 是 | 资源类型，本应用在清单 `resources[]` 里声明的 `{应用 id}.{名字}` |
| `id` | path | string | 是 | 资源 id（应用自己的记录 id，最多 200 字符） |
| `user` | query | string |  | 树类型、应用自己的令牌：查谁的级别（可省略） |
| `agent` | query | boolean |  | 树类型：按这个人的智能体算级别（最多 comment） |

**响应**

| 状态 | 说明 |
|---|---|
| 200 | 名单 |
| 401 | 见 [Unauthorized](#resp-unauthorized) |
| 403 | 见 [Forbidden](#resp-forbidden) |
| 404 | `not_found`：对应的记录不存在（客户中心没有这个客户，访客、服务或所指的东西不存在）。见 [NotFound](#resp-notfound) |
| 429 | 见 [RateLimited](#resp-ratelimited) |
| 500 | 见 [Internal](#resp-internal) |
| 501 | `capability_off`：这个部署（或这个宿主）不提供这项能力，正文的 `capability` 是它的范围。见 [CapabilityOff](#resp-capabilityoff) |
| 503 | `unavailable`：暂时不可用（这个部署不提供的能力是 501 `capability_off`）。见 [Unavailable](#resp-unavailable) |

**200 响应正文**（`application/json`）

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `type` | string | 是 |  |
| `id` | string | 是 |  |
| `owner` | string | 是 | 所有者的 user id |
| `ownerName` | string | 是 | 所有者的显示名；不知道时为空 |
| `general` | string | 是 | 通用访问：空字符串、view、comment 或 edit。空字符串是只有名单上的人 |
| `toggles` | map[string, boolean] | 是 | 类型声明的每个开关和它现在的值 |
| `grants` | ResourceGrant[] | 是 |  |
| `grants[].kind` | string | 是 | user、unit（部门）、role 或 everyone |
| `grants[].principal` | string | 是 | 对象 id；everyone 为空字符串 |
| `grants[].name` | string | 是 | 显示名 |
| `grants[].level` | string | 是 | 访问级别，由低到高。类型可以不开放 comment 或 manage 以上的级别<br>可选值：`view`、`comment`、`edit`、`manage` |
| `grants[].note` | string |  | 说明（为什么给），没有时省略 |
| `grants[].expiresAt` | string(date-time) |  |  |
| `grants[].createdBy` | string | 是 |  |
| `grants[].createdAt` | string(date-time) | 是 |  |
| `me` | string | 是 | 调用者的级别 |
| `canShare` | boolean | 是 |  |
| `isOwner` | boolean | 是 |  |
| `pending` | integer | 是 | 等着处理的申请数（只有能处理的人看得到） |
| `managers` | string[] | 是 | 能管理的人的显示名（所有者在前，最多 5 个），让只读的人知道该找谁 |
| `levels` | string[] | 是 | 这个类型提供的级别，由低到高 |

### `POST /access/{type}/{id}`

登记一个资源（写下它的所有者）

需要范围 `app.resources:write`。English: registers a resource of a declared type with its owner; idempotent.

`type` 必须是本应用清单 `resources[]` 里声明的类型（以 `{本应用 id}.` 开头），否则 404 `not_found`。页面会话登记的资源，所有者就是这个人；应用自己的令牌必须在正文里写 `owner`。正文可省略。重复登记同一个资源不会出错。登记后文档、记录的名单、通用访问、开关都由平台保存。

树类型（清单里带 `parent` 的类型和被它当作 parent 的类型，规则见 `POST …/check`）：正文是 `AccessTreePut`。根类型写 `archived`、`publishOnly`；节点类型写 `root`（在根的顶层时必填，有 `parent` 时以父节点的根为准）、`parent`、`inherit`（省略为 true）、`trashed`。移到别的父节点或别的根时整棵子树跟着走，平台重新计算受影响节点的集合。只收应用自己的令牌（页面会话是 403 `forbidden`：移动会改变节点继承的名单，树的形状由应用按自己的规则维护）。返回 `AccessTreeChanged`，`changed` 是集合变了的节点 id 到新集合 id。一次改动最多重新计算 5000 个节点（整个根 20000 个），超过是 422 `invalid`。English: on a tree type the body puts a root's state or a node's place and state (idempotent, the app's token only); the answer lists the nodes whose set changed.

- 认证：`bearer`
- 范围：`app.resources:write`
- operationId：`registerResource`

**参数**

| 名称 | 位置 | 类型 | 必填 | 说明 |
|---|---|---|---|---|
| `type` | path | string | 是 | 资源类型，本应用在清单 `resources[]` 里声明的 `{应用 id}.{名字}` |
| `id` | path | string | 是 | 资源 id（应用自己的记录 id，最多 200 字符） |
| `Idempotency-Key` | header | string |  | 每个 POST 都收。SDK 未传入时会生成 uuid v4。24 小时内同键同正文重放第一次的回答<br>最短 1，最长 255 |

**请求正文**（`application/json`）

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `owner` | string |  | 所有者的 user id。应用自己的令牌必填（平铺类型） |
| `root` | string |  | 树类型的节点：所在的根（顶层节点必填） |
| `parent` | string |  | 树类型的节点：父节点；省略为根的顶层 |
| `inherit` | boolean |  | 树类型的节点：是否继承上级名单，省略为 true |
| `trashed` | boolean |  | 树类型的节点：在应用的回收站里 |
| `archived` | boolean |  | 树类型的根：已归档 |
| `publishOnly` | boolean |  | 树类型的根：edit 以下按已发布版本读 |

**响应**

| 状态 | 说明 |
|---|---|
| 200 | 已登记 |
| 401 | 见 [Unauthorized](#resp-unauthorized) |
| 403 | 见 [Forbidden](#resp-forbidden) |
| 404 | `not_found`：对应的记录不存在（客户中心没有这个客户，访客、服务或所指的东西不存在）。见 [NotFound](#resp-notfound) |
| 409 | 见 [Conflict](#resp-conflict) |
| 413 | `payload_too_large`：正文太大。见 [PayloadTooLarge](#resp-payloadtoolarge) |
| 422 | 字段或参数不被接受（正文读不出来、有未知字段、缺必填、取值越界）。见 [Unprocessable](#resp-unprocessable) |
| 429 | 见 [RateLimited](#resp-ratelimited) |
| 500 | 见 [Internal](#resp-internal) |
| 501 | `capability_off`：这个部署（或这个宿主）不提供这项能力，正文的 `capability` 是它的范围。见 [CapabilityOff](#resp-capabilityoff) |
| 503 | `unavailable`：暂时不可用（这个部署不提供的能力是 501 `capability_off`）。见 [Unavailable](#resp-unavailable) |

**200 响应正文**（`application/json`）

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `type` | string | 是 |  |
| `id` | string | 是 |  |
| `owner` | string | 是 |  |

### `DELETE /access/{type}/{id}`

删除一个资源的登记（连同名单和申请）

需要范围 `app.resources:write`。English: drops the resource's row: its list and its requests.

页面会话需要对这个资源有管理级别，否则 404 `not_found`；应用自己的令牌不受级别限制。返回 204。

树类型：只收应用自己的令牌（页面会话是 403 `forbidden`）。删除节点只删这一个（应用写出整棵子树的每个节点），删除根连同它下面的全部节点和名单。English: on a tree type only the app's token removes; a node is removed alone (the app names the whole subtree), a root with every node under it.

- 认证：`bearer`
- 范围：`app.resources:write`
- operationId：`dropResource`

**参数**

| 名称 | 位置 | 类型 | 必填 | 说明 |
|---|---|---|---|---|
| `type` | path | string | 是 | 资源类型，本应用在清单 `resources[]` 里声明的 `{应用 id}.{名字}` |
| `id` | path | string | 是 | 资源 id（应用自己的记录 id，最多 200 字符） |

**响应**

| 状态 | 说明 |
|---|---|
| 204 | 已删除 |
| 401 | 见 [Unauthorized](#resp-unauthorized) |
| 403 | 见 [Forbidden](#resp-forbidden) |
| 404 | `not_found`：对应的记录不存在（客户中心没有这个客户，访客、服务或所指的东西不存在）。见 [NotFound](#resp-notfound) |
| 429 | 见 [RateLimited](#resp-ratelimited) |
| 500 | 见 [Internal](#resp-internal) |
| 501 | `capability_off`：这个部署（或这个宿主）不提供这项能力，正文的 `capability` 是它的范围。见 [CapabilityOff](#resp-capabilityoff) |
| 503 | `unavailable`：暂时不可用（这个部署不提供的能力是 501 `capability_off`）。见 [Unavailable](#resp-unavailable) |

### `PUT /access/{type}/{id}/grants`

整体替换树类型一个根或节点的显式名单

需要范围 `app.resources:write`。English: replaces every explicit entry of a root or node of a tree type and recomputes the sets that changes.

只用于树类型（平铺类型是 404 `not_found`）。`entries` 是全部条目：`kind` 为 `user`、`unit`（部门，含下级）、`role`、`everyone`（`principal` 为空字符串），`level` 为 `view`、`comment`、`edit`、`manage`，`name` 是显示名的快照。应用自己的令牌可以调用，并可用 `by` 写明替谁改；页面会话需要管理级别（名单给出的级别），看不到是 404 `not_found`，不能管理是 403 `forbidden`。返回 `AccessTreeChanged`；平台把事件 `access.changed` 送给应用，并记审计 access.tree.entries。

- 认证：`bearer`
- 范围：`app.resources:write`
- operationId：`replaceResourceEntries`

**参数**

| 名称 | 位置 | 类型 | 必填 | 说明 |
|---|---|---|---|---|
| `type` | path | string | 是 | 资源类型，本应用在清单 `resources[]` 里声明的 `{应用 id}.{名字}` |
| `id` | path | string | 是 | 资源 id（应用自己的记录 id，最多 200 字符） |

**请求正文（必填）**（`application/json`）

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `entries` | AccessTreeEntry[] | 是 |  |
| `entries[].kind` | string | 是 | user、unit（部门，含下级）、role 或 everyone<br>可选值：`user`、`unit`、`role`、`everyone` |
| `entries[].principal` | string | 是 | 对象 id；everyone 为空字符串 |
| `entries[].level` | string | 是 | 访问级别，由低到高。类型可以不开放 comment 或 manage 以上的级别<br>可选值：`view`、`comment`、`edit`、`manage` |
| `entries[].name` | string |  | 显示名的快照；判断只看 kind 和 principal |
| `entries[].at` | string(date-time) |  | 这条写入的时间（显式名单才有；有效名单里为空） |
| `entries[].expiresAt` | string(date-time)，可为 null |  | 到期时间：之后这条不再给任何权限，平台把它清掉并重新计算集合；省略或 null 是不限时；已经过去的时间是 422 invalid（有效名单里没有） |
| `by` | string |  | 应用自己的令牌：替谁改，记在名单上；页面会话忽略 |

**响应**

| 状态 | 说明 |
|---|---|
| 200 | 集合变了的节点 |
| 401 | 见 [Unauthorized](#resp-unauthorized) |
| 403 | 见 [Forbidden](#resp-forbidden) |
| 404 | `not_found`：对应的记录不存在（客户中心没有这个客户，访客、服务或所指的东西不存在）。见 [NotFound](#resp-notfound) |
| 422 | 字段或参数不被接受（正文读不出来、有未知字段、缺必填、取值越界）。见 [Unprocessable](#resp-unprocessable) |
| 429 | 见 [RateLimited](#resp-ratelimited) |
| 500 | 见 [Internal](#resp-internal) |
| 501 | `capability_off`：这个部署（或这个宿主）不提供这项能力，正文的 `capability` 是它的范围。见 [CapabilityOff](#resp-capabilityoff) |
| 503 | `unavailable`：暂时不可用（这个部署不提供的能力是 501 `capability_off`）。见 [Unavailable](#resp-unavailable) |

**200 响应正文**（`application/json`）

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `type` | string |  |  |
| `id` | string |  |  |
| `changed` | map[string, string] | 是 | 节点 id 到它现在的集合 id（重新鉴权它打开的文档，更新应用保存的集合 id） |

示例：

```json
{
  "type": "wiki.page",
  "id": "pg_1",
  "changed": {
    "pg_1": "5f0c2a9e41d7b3c8a6e2f104",
    "pg_2": "5f0c2a9e41d7b3c8a6e2f104"
  }
}
```

### `POST /access/{type}/{id}/grants`

分享给一个人、部门、角色或所有人

需要范围 `app.resources:write`。English: adds or changes one grant on the resource.

正文与规则同平台内置应用的 `/api/access/{type}/{id}…`（同一套访问内核：规则、审计、通知都一样）。只收页面会话：应用自己的令牌是 403 `forbidden`（分享是人做的事）。类型不是本应用声明的，或资源没登记，一律 404 `not_found`。名单变了以后，平台把事件 `access.changed` 送给应用（见 webhooks）。

平铺类型上，应用自己的令牌也可以调用：以应用的名义加一条或改一条（不看调用者的级别，规则由应用自己定，例如会员的敏感记录只有持有管理授权权限的人能授），资源没登记时一并登记（所有者为 `app:{应用 id}`）；`by` 写明替谁授（名单上的 `createdBy`），`note` 是说明。级别、对象和到期时间的规则同上；不通知对方。审计动作 access.share。English: on a flat type the app's own token adds or replaces a grant as the app (registering the resource), with `by` and `note`.

`kind` 是 `user`、`unit`（部门）、`role`、`everyone`（所有人）之一，`level` 是 `view`、`comment`、`edit` 或 `manage`，`expiresAt` 省略为不过期。调用者需要有分享的权力（管理级别，或类型开着允许分享的开关）。给人的分享会通知对方。

树类型：在根或节点的显式名单上加一条或改一条（同一 `kind` + `principal` 只有一条），正文同上，`expiresAt` 是这一条的到期时间。级别不能超过类型声明的 `levels`。应用自己的令牌可以调用；页面会话需要对这个根或节点有管理级别（名单给出的级别，不看状态），或者在开了 `reshare` 的根下能编辑这个节点（只能给到、改到 edit），看不到是 404 `not_found`，看得到但不能管理是 403 `forbidden`。返回 `AccessTreeChanged`。审计动作 access.tree.entries。English: on a tree type adds or changes one explicit entry; a person needs manage.

- 认证：`bearer`
- 范围：`app.resources:write`
- operationId：`shareResource`

**参数**

| 名称 | 位置 | 类型 | 必填 | 说明 |
|---|---|---|---|---|
| `type` | path | string | 是 | 资源类型，本应用在清单 `resources[]` 里声明的 `{应用 id}.{名字}` |
| `id` | path | string | 是 | 资源 id（应用自己的记录 id，最多 200 字符） |
| `Idempotency-Key` | header | string |  | 每个 POST 都收。SDK 未传入时会生成 uuid v4。24 小时内同键同正文重放第一次的回答<br>最短 1，最长 255 |

**请求正文（必填）**（`application/json`）

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `kind` | string | 是 | user、unit（部门）、role 或 everyone |
| `principal` | string | 是 | 对象 id |
| `name` | string |  | 显示名，留在名单上 |
| `level` | string | 是 | 访问级别，由低到高。类型可以不开放 comment 或 manage 以上的级别<br>可选值：`view`、`comment`、`edit`、`manage` |
| `expiresAt` | string(date-time) |  | 省略为不过期 |
| `note` | string |  | 说明，留在名单上（为什么给）<br>最长 200 |
| `by` | string |  | 只用于应用自己的令牌：替谁授（名单上的 createdBy）；省略为应用自己 |

**响应**

| 状态 | 说明 |
|---|---|
| 200 | 写入的分享 |
| 401 | 见 [Unauthorized](#resp-unauthorized) |
| 403 | 见 [Forbidden](#resp-forbidden) |
| 404 | `not_found`：对应的记录不存在（客户中心没有这个客户，访客、服务或所指的东西不存在）。见 [NotFound](#resp-notfound) |
| 409 | 见 [Conflict](#resp-conflict) |
| 413 | `payload_too_large`：正文太大。见 [PayloadTooLarge](#resp-payloadtoolarge) |
| 422 | 字段或参数不被接受（正文读不出来、有未知字段、缺必填、取值越界）。见 [Unprocessable](#resp-unprocessable) |
| 429 | 见 [RateLimited](#resp-ratelimited) |
| 500 | 见 [Internal](#resp-internal) |
| 501 | `capability_off`：这个部署（或这个宿主）不提供这项能力，正文的 `capability` 是它的范围。见 [CapabilityOff](#resp-capabilityoff) |
| 503 | `unavailable`：暂时不可用（这个部署不提供的能力是 501 `capability_off`）。见 [Unavailable](#resp-unavailable) |

**200 响应正文**（`application/json`）

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `kind` | string | 是 | user、unit（部门）、role 或 everyone |
| `principal` | string | 是 | 对象 id；everyone 为空字符串 |
| `name` | string | 是 | 显示名 |
| `level` | string | 是 | 访问级别，由低到高。类型可以不开放 comment 或 manage 以上的级别<br>可选值：`view`、`comment`、`edit`、`manage` |
| `note` | string |  | 说明（为什么给），没有时省略 |
| `expiresAt` | string(date-time) |  |  |
| `createdBy` | string | 是 |  |
| `createdAt` | string(date-time) | 是 |  |

### `DELETE /access/{type}/{id}/grants/{kind}/{principal}`

撤销一条分享

需要范围 `app.resources:write`。English: removes one grant.

正文与规则同平台内置应用的 `/api/access/{type}/{id}…`（同一套访问内核：规则、审计、通知都一样）。只收页面会话：应用自己的令牌是 403 `forbidden`（分享是人做的事）。类型不是本应用声明的，或资源没登记，一律 404 `not_found`。名单变了以后，平台把事件 `access.changed` 送给应用（见 webhooks）。

平铺类型上，应用自己的令牌也可以调用：以应用的名义删掉一条（没有这条是 404 `not_found`）。审计动作 access.revoke。English: on a flat type the app's own token removes a grant as the app.

对象是 `everyone` 的分享，`principal` 写 `-`。

树类型：从根或节点的显式名单删掉一条（没有这条是 404 `not_found`），权限规则同 `POST …/grants`，返回 `AccessTreeChanged`。English: on a tree type removes one explicit entry.

- 认证：`bearer`
- 范围：`app.resources:write`
- operationId：`revokeResourceGrant`

**参数**

| 名称 | 位置 | 类型 | 必填 | 说明 |
|---|---|---|---|---|
| `type` | path | string | 是 | 资源类型，本应用在清单 `resources[]` 里声明的 `{应用 id}.{名字}` |
| `id` | path | string | 是 | 资源 id（应用自己的记录 id，最多 200 字符） |
| `kind` | path | string | 是 | 分享对象的种类：`user`、`unit`、`role`、`everyone` |
| `principal` | path | string | 是 | 分享对象的 id（everyone 写 `-`） |

**响应**

| 状态 | 说明 |
|---|---|
| 200 | 已撤销 |
| 401 | 见 [Unauthorized](#resp-unauthorized) |
| 403 | 见 [Forbidden](#resp-forbidden) |
| 404 | `not_found`：对应的记录不存在（客户中心没有这个客户，访客、服务或所指的东西不存在）。见 [NotFound](#resp-notfound) |
| 409 | 见 [Conflict](#resp-conflict) |
| 422 | 字段或参数不被接受（正文读不出来、有未知字段、缺必填、取值越界）。见 [Unprocessable](#resp-unprocessable) |
| 429 | 见 [RateLimited](#resp-ratelimited) |
| 500 | 见 [Internal](#resp-internal) |
| 501 | `capability_off`：这个部署（或这个宿主）不提供这项能力，正文的 `capability` 是它的范围。见 [CapabilityOff](#resp-capabilityoff) |
| 503 | `unavailable`：暂时不可用（这个部署不提供的能力是 501 `capability_off`）。见 [Unavailable](#resp-unavailable) |

**200 响应正文**（`application/json`）

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `ok` | boolean | 是 |  |

### `PUT /access/{type}/{id}/general`

设置通用访问（企业内知道链接的人能做什么）

需要范围 `app.resources:write`。English: sets the general access of the resource.

正文与规则同平台内置应用的 `/api/access/{type}/{id}…`（同一套访问内核：规则、审计、通知都一样）。只收页面会话：应用自己的令牌是 403 `forbidden`（分享是人做的事）。类型不是本应用声明的，或资源没登记，一律 404 `not_found`。名单变了以后，平台把事件 `access.changed` 送给应用（见 webhooks）。

`level` 是空字符串（只有名单上的人）、`view`、`comment` 或 `edit`；类型不开放评论时不能设 `comment`。

- 认证：`bearer`
- 范围：`app.resources:write`
- operationId：`setResourceGeneral`

**参数**

| 名称 | 位置 | 类型 | 必填 | 说明 |
|---|---|---|---|---|
| `type` | path | string | 是 | 资源类型，本应用在清单 `resources[]` 里声明的 `{应用 id}.{名字}` |
| `id` | path | string | 是 | 资源 id（应用自己的记录 id，最多 200 字符） |

**请求正文（必填）**（`application/json`）

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `level` | string | 是 | 空字符串：只有名单上的人<br>可选值：``、`view`、`comment`、`edit` |

**响应**

| 状态 | 说明 |
|---|---|
| 200 | 新的通用访问 |
| 401 | 见 [Unauthorized](#resp-unauthorized) |
| 403 | 见 [Forbidden](#resp-forbidden) |
| 404 | `not_found`：对应的记录不存在（客户中心没有这个客户，访客、服务或所指的东西不存在）。见 [NotFound](#resp-notfound) |
| 409 | 见 [Conflict](#resp-conflict) |
| 413 | `payload_too_large`：正文太大。见 [PayloadTooLarge](#resp-payloadtoolarge) |
| 422 | 字段或参数不被接受（正文读不出来、有未知字段、缺必填、取值越界）。见 [Unprocessable](#resp-unprocessable) |
| 429 | 见 [RateLimited](#resp-ratelimited) |
| 500 | 见 [Internal](#resp-internal) |
| 501 | `capability_off`：这个部署（或这个宿主）不提供这项能力，正文的 `capability` 是它的范围。见 [CapabilityOff](#resp-capabilityoff) |
| 503 | `unavailable`：暂时不可用（这个部署不提供的能力是 501 `capability_off`）。见 [Unavailable](#resp-unavailable) |

**200 响应正文**（`application/json`）

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `general` | string | 是 | 设置后的通用访问 |

### `PUT /access/{type}/{id}/toggles`

设置资源的开关

需要范围 `app.resources:write`。English: sets the switches the type declares (`resources[].switches`), e.g. `explore`, and returns the list.

正文与规则同平台内置应用的 `/api/access/{type}/{id}…`（同一套访问内核：规则、审计、通知都一样）。只收页面会话：应用自己的令牌是 403 `forbidden`（分享是人做的事）。类型不是本应用声明的，或资源没登记，一律 404 `not_found`。名单变了以后，平台把事件 `access.changed` 送给应用（见 webhooks）。

正文是 `{开关名: true|false}`，只能写类型声明过的开关。

- 认证：`bearer`
- 范围：`app.resources:write`
- operationId：`setResourceToggles`

**参数**

| 名称 | 位置 | 类型 | 必填 | 说明 |
|---|---|---|---|---|
| `type` | path | string | 是 | 资源类型，本应用在清单 `resources[]` 里声明的 `{应用 id}.{名字}` |
| `id` | path | string | 是 | 资源 id（应用自己的记录 id，最多 200 字符） |

**请求正文（必填）**（`application/json`）

类型 `ResourceToggles`。开关名到值，只能写类型声明过的开关

**响应**

| 状态 | 说明 |
|---|---|
| 200 | 更新后的名单 |
| 401 | 见 [Unauthorized](#resp-unauthorized) |
| 403 | 见 [Forbidden](#resp-forbidden) |
| 404 | `not_found`：对应的记录不存在（客户中心没有这个客户，访客、服务或所指的东西不存在）。见 [NotFound](#resp-notfound) |
| 409 | 见 [Conflict](#resp-conflict) |
| 413 | `payload_too_large`：正文太大。见 [PayloadTooLarge](#resp-payloadtoolarge) |
| 422 | 字段或参数不被接受（正文读不出来、有未知字段、缺必填、取值越界）。见 [Unprocessable](#resp-unprocessable) |
| 429 | 见 [RateLimited](#resp-ratelimited) |
| 500 | 见 [Internal](#resp-internal) |
| 501 | `capability_off`：这个部署（或这个宿主）不提供这项能力，正文的 `capability` 是它的范围。见 [CapabilityOff](#resp-capabilityoff) |
| 503 | `unavailable`：暂时不可用（这个部署不提供的能力是 501 `capability_off`）。见 [Unavailable](#resp-unavailable) |

**200 响应正文**（`application/json`）

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `type` | string | 是 |  |
| `id` | string | 是 |  |
| `owner` | string | 是 | 所有者的 user id |
| `ownerName` | string | 是 | 所有者的显示名；不知道时为空 |
| `general` | string | 是 | 通用访问：空字符串、view、comment 或 edit。空字符串是只有名单上的人 |
| `toggles` | map[string, boolean] | 是 | 类型声明的每个开关和它现在的值 |
| `grants` | ResourceGrant[] | 是 |  |
| `grants[].kind` | string | 是 | user、unit（部门）、role 或 everyone |
| `grants[].principal` | string | 是 | 对象 id；everyone 为空字符串 |
| `grants[].name` | string | 是 | 显示名 |
| `grants[].level` | string | 是 | 访问级别，由低到高。类型可以不开放 comment 或 manage 以上的级别<br>可选值：`view`、`comment`、`edit`、`manage` |
| `grants[].note` | string |  | 说明（为什么给），没有时省略 |
| `grants[].expiresAt` | string(date-time) |  |  |
| `grants[].createdBy` | string | 是 |  |
| `grants[].createdAt` | string(date-time) | 是 |  |
| `me` | string | 是 | 调用者的级别 |
| `canShare` | boolean | 是 |  |
| `isOwner` | boolean | 是 |  |
| `pending` | integer | 是 | 等着处理的申请数（只有能处理的人看得到） |
| `managers` | string[] | 是 | 能管理的人的显示名（所有者在前，最多 5 个），让只读的人知道该找谁 |
| `levels` | string[] | 是 | 这个类型提供的级别，由低到高 |

### `POST /access/{type}/{id}/owner`

转让资源的所有权

需要范围 `app.resources:write`。English: transfers the owner of the resource to another person; notifies them.

正文与规则同平台内置应用的 `/api/access/{type}/{id}…`（同一套访问内核：规则、审计、通知都一样）。只收页面会话：应用自己的令牌是 403 `forbidden`（分享是人做的事）。类型不是本应用声明的，或资源没登记，一律 404 `not_found`。名单变了以后，平台把事件 `access.changed` 送给应用（见 webhooks）。

只有所有者（或管理员）能转让。`to` 必填，`name` 是对方的显示名。

- 认证：`bearer`
- 范围：`app.resources:write`
- operationId：`transferResource`

**参数**

| 名称 | 位置 | 类型 | 必填 | 说明 |
|---|---|---|---|---|
| `type` | path | string | 是 | 资源类型，本应用在清单 `resources[]` 里声明的 `{应用 id}.{名字}` |
| `id` | path | string | 是 | 资源 id（应用自己的记录 id，最多 200 字符） |
| `Idempotency-Key` | header | string |  | 每个 POST 都收。SDK 未传入时会生成 uuid v4。24 小时内同键同正文重放第一次的回答<br>最短 1，最长 255 |

**请求正文（必填）**（`application/json`）

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `to` | string | 是 | 新所有者的 user id |
| `name` | string |  | 新所有者的显示名 |

**响应**

| 状态 | 说明 |
|---|---|
| 200 | 新的所有者 |
| 401 | 见 [Unauthorized](#resp-unauthorized) |
| 403 | 见 [Forbidden](#resp-forbidden) |
| 404 | `not_found`：对应的记录不存在（客户中心没有这个客户，访客、服务或所指的东西不存在）。见 [NotFound](#resp-notfound) |
| 409 | 见 [Conflict](#resp-conflict) |
| 413 | `payload_too_large`：正文太大。见 [PayloadTooLarge](#resp-payloadtoolarge) |
| 422 | 字段或参数不被接受（正文读不出来、有未知字段、缺必填、取值越界）。见 [Unprocessable](#resp-unprocessable) |
| 429 | 见 [RateLimited](#resp-ratelimited) |
| 500 | 见 [Internal](#resp-internal) |
| 501 | `capability_off`：这个部署（或这个宿主）不提供这项能力，正文的 `capability` 是它的范围。见 [CapabilityOff](#resp-capabilityoff) |
| 503 | `unavailable`：暂时不可用（这个部署不提供的能力是 501 `capability_off`）。见 [Unavailable](#resp-unavailable) |

**200 响应正文**（`application/json`）

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `owner` | string | 是 | 新的所有者 |

### `GET /access/{type}/{id}/requests`

读调用者自己对一个资源的申请

需要范围 `app.resources:read`。English: the caller's own requests on the resource, newest first, at most 100.

正文与规则同平台内置应用的 `/api/access/{type}/{id}…`（同一套访问内核：规则、审计、通知都一样）。只收页面会话：应用自己的令牌是 403 `forbidden`（分享是人做的事）。类型不是本应用声明的，或资源没登记，一律 404 `not_found`。名单变了以后，平台把事件 `access.changed` 送给应用（见 webhooks）。

- 认证：`bearer`
- 范围：`app.resources:read`
- operationId：`listResourceRequests`

**参数**

| 名称 | 位置 | 类型 | 必填 | 说明 |
|---|---|---|---|---|
| `type` | path | string | 是 | 资源类型，本应用在清单 `resources[]` 里声明的 `{应用 id}.{名字}` |
| `id` | path | string | 是 | 资源 id（应用自己的记录 id，最多 200 字符） |

**响应**

| 状态 | 说明 |
|---|---|
| 200 | 申请 |
| 401 | 见 [Unauthorized](#resp-unauthorized) |
| 403 | 见 [Forbidden](#resp-forbidden) |
| 404 | `not_found`：对应的记录不存在（客户中心没有这个客户，访客、服务或所指的东西不存在）。见 [NotFound](#resp-notfound) |
| 429 | 见 [RateLimited](#resp-ratelimited) |
| 500 | 见 [Internal](#resp-internal) |
| 501 | `capability_off`：这个部署（或这个宿主）不提供这项能力，正文的 `capability` 是它的范围。见 [CapabilityOff](#resp-capabilityoff) |
| 503 | `unavailable`：暂时不可用（这个部署不提供的能力是 501 `capability_off`）。见 [Unavailable](#resp-unavailable) |

**200 响应正文**（`application/json`）

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `requests` | ResourceRequest[] | 是 |  |
| `requests[].id` | string | 是 |  |
| `requests[].type` | string | 是 |  |
| `requests[].resource` | string | 是 |  |
| `requests[].requester` | string | 是 |  |
| `requests[].requesterName` | string | 是 |  |
| `requests[].note` | string | 是 |  |
| `requests[].status` | string | 是 | 可选值：`pending`、`approved`、`denied`、`cancelled` |
| `requests[].decidedBy` | string |  |  |
| `requests[].decidedByName` | string |  |  |
| `requests[].expiresAt` | string(date-time) |  |  |
| `requests[].createdAt` | string(date-time) | 是 |  |
| `requests[].decidedAt` | string(date-time) |  |  |
| `requests[].reason` | string |  | 拒绝的原因 |
| `requests[].app` | string |  | 申请从哪个应用提出 |
| `requests[].origin` | string |  | 应用里的哪个页面 |
| `requests[].level` | string | 是 | 申请的级别 |
| `requests[].granted` | string |  | 批准的级别 |
| `requests[].retryAfter` | string(date-time) |  | 被拒绝后 24 小时的冷却何时结束 |
| `requests[].title` | string |  | 资源的标题，只给能处理它的人 |

### `POST /access/{type}/{id}/requests`

为自己申请访问一个资源

需要范围 `app.resources:write`。English: files a request for access (or a higher level) on the resource, as the caller; notifies the people who decide.

正文与规则同平台内置应用的 `/api/access/{type}/{id}…`（同一套访问内核：规则、审计、通知都一样）。只收页面会话：应用自己的令牌是 403 `forbidden`（分享是人做的事）。类型不是本应用声明的，或资源没登记，一律 404 `not_found`。名单变了以后，平台把事件 `access.changed` 送给应用（见 webhooks）。

类型没有开放申请，或调用者已经有这个级别，是 409 `conflict`；被拒绝后 24 小时内不能再申请（`retryAfter`）。`level` 省略是 `view`。

- 认证：`bearer`
- 范围：`app.resources:write`
- operationId：`requestResourceAccess`

**参数**

| 名称 | 位置 | 类型 | 必填 | 说明 |
|---|---|---|---|---|
| `type` | path | string | 是 | 资源类型，本应用在清单 `resources[]` 里声明的 `{应用 id}.{名字}` |
| `id` | path | string | 是 | 资源 id（应用自己的记录 id，最多 200 字符） |
| `Idempotency-Key` | header | string |  | 每个 POST 都收。SDK 未传入时会生成 uuid v4。24 小时内同键同正文重放第一次的回答<br>最短 1，最长 255 |

**请求正文**（`application/json`）

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `level` | string |  | 访问级别，由低到高。类型可以不开放 comment 或 manage 以上的级别<br>可选值：`view`、`comment`、`edit`、`manage` |
| `note` | string |  | 给处理的人看的话 |

**响应**

| 状态 | 说明 |
|---|---|
| 200 | 申请 |
| 401 | 见 [Unauthorized](#resp-unauthorized) |
| 403 | 见 [Forbidden](#resp-forbidden) |
| 404 | `not_found`：对应的记录不存在（客户中心没有这个客户，访客、服务或所指的东西不存在）。见 [NotFound](#resp-notfound) |
| 409 | 见 [Conflict](#resp-conflict) |
| 413 | `payload_too_large`：正文太大。见 [PayloadTooLarge](#resp-payloadtoolarge) |
| 422 | 字段或参数不被接受（正文读不出来、有未知字段、缺必填、取值越界）。见 [Unprocessable](#resp-unprocessable) |
| 429 | 见 [RateLimited](#resp-ratelimited) |
| 500 | 见 [Internal](#resp-internal) |
| 501 | `capability_off`：这个部署（或这个宿主）不提供这项能力，正文的 `capability` 是它的范围。见 [CapabilityOff](#resp-capabilityoff) |
| 503 | `unavailable`：暂时不可用（这个部署不提供的能力是 501 `capability_off`）。见 [Unavailable](#resp-unavailable) |

**200 响应正文**（`application/json`）

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `id` | string | 是 |  |
| `type` | string | 是 |  |
| `resource` | string | 是 |  |
| `requester` | string | 是 |  |
| `requesterName` | string | 是 |  |
| `note` | string | 是 |  |
| `status` | string | 是 | 可选值：`pending`、`approved`、`denied`、`cancelled` |
| `decidedBy` | string |  |  |
| `decidedByName` | string |  |  |
| `expiresAt` | string(date-time) |  |  |
| `createdAt` | string(date-time) | 是 |  |
| `decidedAt` | string(date-time) |  |  |
| `reason` | string |  | 拒绝的原因 |
| `app` | string |  | 申请从哪个应用提出 |
| `origin` | string |  | 应用里的哪个页面 |
| `level` | string | 是 | 申请的级别 |
| `granted` | string |  | 批准的级别 |
| `retryAfter` | string(date-time) |  | 被拒绝后 24 小时的冷却何时结束 |
| `title` | string |  | 资源的标题，只给能处理它的人 |

### `GET /access/requests`

读调用者能处理的待处理申请

需要范围 `app.resources:read`。English: every pending request on this app's types that the caller may decide, with the resource's title, oldest first.

正文与规则同平台内置应用的 `/api/access/requests…`。只收页面会话：应用自己的令牌是 403 `forbidden`（申请和处理都是人做的事）。只含本应用在清单里声明、开放申请（`requestable`）的类型。平面类型由访问内核的名单决定谁处理（所有者和管理者）；树类型（知识库页面）的名单是应用自己的树，由应用通过提供者 `accessRequests` 回答调用者在资源上的级别、通知谁、批准怎么写入；应用不提供时，树类型不收申请（404）。

- 认证：`bearer`
- 范围：`app.resources:read`
- operationId：`listDecidableResourceRequests`

**响应**

| 状态 | 说明 |
|---|---|
| 200 | 申请列表 |
| 401 | 见 [Unauthorized](#resp-unauthorized) |
| 403 | 见 [Forbidden](#resp-forbidden) |
| 429 | 见 [RateLimited](#resp-ratelimited) |
| 500 | 见 [Internal](#resp-internal) |
| 501 | `capability_off`：这个部署（或这个宿主）不提供这项能力，正文的 `capability` 是它的范围。见 [CapabilityOff](#resp-capabilityoff) |
| 503 | `unavailable`：暂时不可用（这个部署不提供的能力是 501 `capability_off`）。见 [Unavailable](#resp-unavailable) |

**200 响应正文**（`application/json`）

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `requests` | ResourceRequest[] | 是 |  |
| `requests[].id` | string | 是 |  |
| `requests[].type` | string | 是 |  |
| `requests[].resource` | string | 是 |  |
| `requests[].requester` | string | 是 |  |
| `requests[].requesterName` | string | 是 |  |
| `requests[].note` | string | 是 |  |
| `requests[].status` | string | 是 | 可选值：`pending`、`approved`、`denied`、`cancelled` |
| `requests[].decidedBy` | string |  |  |
| `requests[].decidedByName` | string |  |  |
| `requests[].expiresAt` | string(date-time) |  |  |
| `requests[].createdAt` | string(date-time) | 是 |  |
| `requests[].decidedAt` | string(date-time) |  |  |
| `requests[].reason` | string |  | 拒绝的原因 |
| `requests[].app` | string |  | 申请从哪个应用提出 |
| `requests[].origin` | string |  | 应用里的哪个页面 |
| `requests[].level` | string | 是 | 申请的级别 |
| `requests[].granted` | string |  | 批准的级别 |
| `requests[].retryAfter` | string(date-time) |  | 被拒绝后 24 小时的冷却何时结束 |
| `requests[].title` | string |  | 资源的标题，只给能处理它的人 |

### `GET /access/requests/mine`

读调用者自己的申请

需要范围 `app.resources:read`。English: the caller's own requests on this app's types, newest first, at most 100 (no titles).

正文与规则同平台内置应用的 `/api/access/requests…`。只收页面会话：应用自己的令牌是 403 `forbidden`（申请和处理都是人做的事）。只含本应用在清单里声明、开放申请（`requestable`）的类型。平面类型由访问内核的名单决定谁处理（所有者和管理者）；树类型（知识库页面）的名单是应用自己的树，由应用通过提供者 `accessRequests` 回答调用者在资源上的级别、通知谁、批准怎么写入；应用不提供时，树类型不收申请（404）。

- 认证：`bearer`
- 范围：`app.resources:read`
- operationId：`listMyResourceRequests`

**响应**

| 状态 | 说明 |
|---|---|
| 200 | 申请列表 |
| 401 | 见 [Unauthorized](#resp-unauthorized) |
| 403 | 见 [Forbidden](#resp-forbidden) |
| 429 | 见 [RateLimited](#resp-ratelimited) |
| 500 | 见 [Internal](#resp-internal) |
| 501 | `capability_off`：这个部署（或这个宿主）不提供这项能力，正文的 `capability` 是它的范围。见 [CapabilityOff](#resp-capabilityoff) |
| 503 | `unavailable`：暂时不可用（这个部署不提供的能力是 501 `capability_off`）。见 [Unavailable](#resp-unavailable) |

**200 响应正文**（`application/json`）

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `requests` | ResourceRequest[] | 是 |  |
| `requests[].id` | string | 是 |  |
| `requests[].type` | string | 是 |  |
| `requests[].resource` | string | 是 |  |
| `requests[].requester` | string | 是 |  |
| `requests[].requesterName` | string | 是 |  |
| `requests[].note` | string | 是 |  |
| `requests[].status` | string | 是 | 可选值：`pending`、`approved`、`denied`、`cancelled` |
| `requests[].decidedBy` | string |  |  |
| `requests[].decidedByName` | string |  |  |
| `requests[].expiresAt` | string(date-time) |  |  |
| `requests[].createdAt` | string(date-time) | 是 |  |
| `requests[].decidedAt` | string(date-time) |  |  |
| `requests[].reason` | string |  | 拒绝的原因 |
| `requests[].app` | string |  | 申请从哪个应用提出 |
| `requests[].origin` | string |  | 应用里的哪个页面 |
| `requests[].level` | string | 是 | 申请的级别 |
| `requests[].granted` | string |  | 批准的级别 |
| `requests[].retryAfter` | string(date-time) |  | 被拒绝后 24 小时的冷却何时结束 |
| `requests[].title` | string |  | 资源的标题，只给能处理它的人 |

### `POST /access/requests/{rid}/approve`

批准一条申请

需要范围 `app.resources:write`。English: approves a pending request the caller may decide: the level (at most what was asked; omitted: what was asked), optionally until expiresAt.

正文与规则同平台内置应用的 `/api/access/requests…`。只收页面会话：应用自己的令牌是 403 `forbidden`（申请和处理都是人做的事）。只含本应用在清单里声明、开放申请（`requestable`）的类型。平面类型由访问内核的名单决定谁处理（所有者和管理者）；树类型（知识库页面）的名单是应用自己的树，由应用通过提供者 `accessRequests` 回答调用者在资源上的级别、通知谁、批准怎么写入；应用不提供时，树类型不收申请（404）。调用者不能处理是 404；已处理过是 409。树类型由应用写入批准，应用拒绝（例如超出处理人能给的级别）时原样返回它的状态和说明。

- 认证：`bearer`
- 范围：`app.resources:write`
- operationId：`approveResourceRequest`

**参数**

| 名称 | 位置 | 类型 | 必填 | 说明 |
|---|---|---|---|---|
| `rid` | path | string | 是 | 申请 id |
| `Idempotency-Key` | header | string |  | 每个 POST 都收。SDK 未传入时会生成 uuid v4。24 小时内同键同正文重放第一次的回答<br>最短 1，最长 255 |

**请求正文（必填）**（`application/json`）

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `level` | string |  | 批准的级别（view、comment、edit），省略为申请的级别 |
| `expiresAt` | string(date-time) |  | 到期时间，省略为长期 |

**响应**

| 状态 | 说明 |
|---|---|
| 200 | 申请 |
| 401 | 见 [Unauthorized](#resp-unauthorized) |
| 403 | 见 [Forbidden](#resp-forbidden) |
| 404 | `not_found`：对应的记录不存在（客户中心没有这个客户，访客、服务或所指的东西不存在）。见 [NotFound](#resp-notfound) |
| 409 | 见 [Conflict](#resp-conflict) |
| 422 | 字段或参数不被接受（正文读不出来、有未知字段、缺必填、取值越界）。见 [Unprocessable](#resp-unprocessable) |
| 429 | 见 [RateLimited](#resp-ratelimited) |
| 500 | 见 [Internal](#resp-internal) |
| 501 | `capability_off`：这个部署（或这个宿主）不提供这项能力，正文的 `capability` 是它的范围。见 [CapabilityOff](#resp-capabilityoff) |
| 503 | `unavailable`：暂时不可用（这个部署不提供的能力是 501 `capability_off`）。见 [Unavailable](#resp-unavailable) |

**200 响应正文**（`application/json`）

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `id` | string | 是 |  |
| `type` | string | 是 |  |
| `resource` | string | 是 |  |
| `requester` | string | 是 |  |
| `requesterName` | string | 是 |  |
| `note` | string | 是 |  |
| `status` | string | 是 | 可选值：`pending`、`approved`、`denied`、`cancelled` |
| `decidedBy` | string |  |  |
| `decidedByName` | string |  |  |
| `expiresAt` | string(date-time) |  |  |
| `createdAt` | string(date-time) | 是 |  |
| `decidedAt` | string(date-time) |  |  |
| `reason` | string |  | 拒绝的原因 |
| `app` | string |  | 申请从哪个应用提出 |
| `origin` | string |  | 应用里的哪个页面 |
| `level` | string | 是 | 申请的级别 |
| `granted` | string |  | 批准的级别 |
| `retryAfter` | string(date-time) |  | 被拒绝后 24 小时的冷却何时结束 |
| `title` | string |  | 资源的标题，只给能处理它的人 |

### `POST /access/requests/{rid}/deny`

拒绝一条申请

需要范围 `app.resources:write`。English: refuses a pending request the caller may decide, with the reason the requester is told (required, at most 200 characters).

正文与规则同平台内置应用的 `/api/access/requests…`。只收页面会话：应用自己的令牌是 403 `forbidden`（申请和处理都是人做的事）。只含本应用在清单里声明、开放申请（`requestable`）的类型。平面类型由访问内核的名单决定谁处理（所有者和管理者）；树类型（知识库页面）的名单是应用自己的树，由应用通过提供者 `accessRequests` 回答调用者在资源上的级别、通知谁、批准怎么写入；应用不提供时，树类型不收申请（404）。

- 认证：`bearer`
- 范围：`app.resources:write`
- operationId：`denyResourceRequest`

**参数**

| 名称 | 位置 | 类型 | 必填 | 说明 |
|---|---|---|---|---|
| `rid` | path | string | 是 | 申请 id |
| `Idempotency-Key` | header | string |  | 每个 POST 都收。SDK 未传入时会生成 uuid v4。24 小时内同键同正文重放第一次的回答<br>最短 1，最长 255 |

**请求正文（必填）**（`application/json`）

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `reason` | string | 是 | 拒绝的原因，申请人会看到<br>最长 200 |

**响应**

| 状态 | 说明 |
|---|---|
| 200 | 申请 |
| 401 | 见 [Unauthorized](#resp-unauthorized) |
| 403 | 见 [Forbidden](#resp-forbidden) |
| 404 | `not_found`：对应的记录不存在（客户中心没有这个客户，访客、服务或所指的东西不存在）。见 [NotFound](#resp-notfound) |
| 409 | 见 [Conflict](#resp-conflict) |
| 422 | 字段或参数不被接受（正文读不出来、有未知字段、缺必填、取值越界）。见 [Unprocessable](#resp-unprocessable) |
| 429 | 见 [RateLimited](#resp-ratelimited) |
| 500 | 见 [Internal](#resp-internal) |
| 501 | `capability_off`：这个部署（或这个宿主）不提供这项能力，正文的 `capability` 是它的范围。见 [CapabilityOff](#resp-capabilityoff) |
| 503 | `unavailable`：暂时不可用（这个部署不提供的能力是 501 `capability_off`）。见 [Unavailable](#resp-unavailable) |

**200 响应正文**（`application/json`）

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `id` | string | 是 |  |
| `type` | string | 是 |  |
| `resource` | string | 是 |  |
| `requester` | string | 是 |  |
| `requesterName` | string | 是 |  |
| `note` | string | 是 |  |
| `status` | string | 是 | 可选值：`pending`、`approved`、`denied`、`cancelled` |
| `decidedBy` | string |  |  |
| `decidedByName` | string |  |  |
| `expiresAt` | string(date-time) |  |  |
| `createdAt` | string(date-time) | 是 |  |
| `decidedAt` | string(date-time) |  |  |
| `reason` | string |  | 拒绝的原因 |
| `app` | string |  | 申请从哪个应用提出 |
| `origin` | string |  | 应用里的哪个页面 |
| `level` | string | 是 | 申请的级别 |
| `granted` | string |  | 批准的级别 |
| `retryAfter` | string(date-time) |  | 被拒绝后 24 小时的冷却何时结束 |
| `title` | string |  | 资源的标题，只给能处理它的人 |

### `DELETE /access/requests/{rid}`

撤回自己的申请

需要范围 `app.resources:write`。English: withdraws the caller's own pending request.

正文与规则同平台内置应用的 `/api/access/requests…`。只收页面会话：应用自己的令牌是 403 `forbidden`（申请和处理都是人做的事）。只含本应用在清单里声明、开放申请（`requestable`）的类型。平面类型由访问内核的名单决定谁处理（所有者和管理者）；树类型（知识库页面）的名单是应用自己的树，由应用通过提供者 `accessRequests` 回答调用者在资源上的级别、通知谁、批准怎么写入；应用不提供时，树类型不收申请（404）。

- 认证：`bearer`
- 范围：`app.resources:write`
- operationId：`cancelResourceRequest`

**参数**

| 名称 | 位置 | 类型 | 必填 | 说明 |
|---|---|---|---|---|
| `rid` | path | string | 是 | 申请 id |

**响应**

| 状态 | 说明 |
|---|---|
| 200 | 申请 |
| 401 | 见 [Unauthorized](#resp-unauthorized) |
| 403 | 见 [Forbidden](#resp-forbidden) |
| 404 | `not_found`：对应的记录不存在（客户中心没有这个客户，访客、服务或所指的东西不存在）。见 [NotFound](#resp-notfound) |
| 409 | 见 [Conflict](#resp-conflict) |
| 429 | 见 [RateLimited](#resp-ratelimited) |
| 500 | 见 [Internal](#resp-internal) |
| 501 | `capability_off`：这个部署（或这个宿主）不提供这项能力，正文的 `capability` 是它的范围。见 [CapabilityOff](#resp-capabilityoff) |
| 503 | `unavailable`：暂时不可用（这个部署不提供的能力是 501 `capability_off`）。见 [Unavailable](#resp-unavailable) |

**200 响应正文**（`application/json`）

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `id` | string | 是 |  |
| `type` | string | 是 |  |
| `resource` | string | 是 |  |
| `requester` | string | 是 |  |
| `requesterName` | string | 是 |  |
| `note` | string | 是 |  |
| `status` | string | 是 | 可选值：`pending`、`approved`、`denied`、`cancelled` |
| `decidedBy` | string |  |  |
| `decidedByName` | string |  |  |
| `expiresAt` | string(date-time) |  |  |
| `createdAt` | string(date-time) | 是 |  |
| `decidedAt` | string(date-time) |  |  |
| `reason` | string |  | 拒绝的原因 |
| `app` | string |  | 申请从哪个应用提出 |
| `origin` | string |  | 应用里的哪个页面 |
| `level` | string | 是 | 申请的级别 |
| `granted` | string |  | 批准的级别 |
| `retryAfter` | string(date-time) |  | 被拒绝后 24 小时的冷却何时结束 |
| `title` | string |  | 资源的标题，只给能处理它的人 |

### `GET /access/{type}`

一个资源类型的声明

需要范围 `app.resources:read`。English: the declared kind as the platform keeps it: what one resource of it is called (`name` in Chinese, `nameEn` in English, from the manifest's `resources[]`; empty when not declared), its levels and switches, whether it takes requests or can be published, and whether it is a tree type (`tree`, with `parent` on a node type).

平台的分享对话框用 `name` / `nameEn` 称呼这个资源（「这个笔记」）；没写时说「这个内容」。`levels` 没声明时是全部四级，`switches` 没声明时是空对象。本应用当前安装的版本没有声明这个类型（或类型不以 `{本应用 id}.` 开头）是 404 `not_found`：分享对话框据此区分「类型没登记」和「这一项找不到」。

- 认证：`bearer`
- 范围：`app.resources:read`
- operationId：`getResourceKind`

**参数**

| 名称 | 位置 | 类型 | 必填 | 说明 |
|---|---|---|---|---|
| `type` | path | string | 是 | 资源类型，本应用在清单 `resources[]` 里声明的 `{应用 id}.{名字}` |

**响应**

| 状态 | 说明 |
|---|---|
| 200 | 类型的声明 |
| 401 | 见 [Unauthorized](#resp-unauthorized) |
| 403 | 见 [Forbidden](#resp-forbidden) |
| 404 | `not_found`：对应的记录不存在（客户中心没有这个客户，访客、服务或所指的东西不存在）。见 [NotFound](#resp-notfound) |
| 429 | 见 [RateLimited](#resp-ratelimited) |
| 500 | 见 [Internal](#resp-internal) |
| 501 | `capability_off`：这个部署（或这个宿主）不提供这项能力，正文的 `capability` 是它的范围。见 [CapabilityOff](#resp-capabilityoff) |
| 503 | `unavailable`：暂时不可用（这个部署不提供的能力是 501 `capability_off`）。见 [Unavailable](#resp-unavailable) |

**200 响应正文**（`application/json`）

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `type` | string | 是 | `{应用 id}.{名字}` |
| `name` | string | 是 | 一个这种资源的叫法（中文，最多 20 字，如「笔记」）；没声明时为空字符串 |
| `nameEn` | string | 是 | 同一个叫法的英文（如 note）；没声明时为空字符串 |
| `parent` | string |  | 节点类型所在的根类型；平铺类型和根类型省略 |
| `tree` | boolean | 是 | 树类型（带 parent 的类型，或被别的类型当作 parent 的类型） |
| `levels` | string[] | 是 | 这个类型提供的级别，由低到高；没声明时是全部四级 |
| `switches` | map[string, boolean] | 是 | 类型声明的开关和默认值 |
| `requestable` | boolean | 是 | 开放访问申请 |
| `publishable` | boolean | 是 | 可以发布给名单之外的读者 |

### `POST /access/{type}/check`

批量查一个人对多个资源的级别

需要范围 `app.resources:read`。English: the level of a person on each of up to 500 resources of one type (replaces a visibility SQL join).

`ids` 1 到 500 个。`level` 可省略；写了就另外给出每个 id 是否达到了这个级别（`allowed`）。页面会话查的是自己；应用自己的令牌必须在 `user` 写明查谁，否则 422 `invalid`。未登记的资源，级别是空字符串。单次读取要鉴权时用它，列表过滤用 `GET …/visible`。

树类型（清单里带 `parent` 的节点类型，和被它当作 parent 的根类型）：平台保存根和节点、它们的显式名单和状态，并把每个根和节点的有效名单存成不可变、去重的集合。节点继承父节点（顶层继承根）的名单再加上自己的条目；`inherit` 为 false 的节点只看自己的条目。每个 id 的级别是名单给出的级别经状态限制后的结果，`reasons` 给出原因：节点在回收站（trashed）时管理级别以下没有权限（原因 trashed）；根归档（archived）时最多 comment（原因 archived）；根只读发布版（publishOnly）时 edit 以下和智能体读已发布版本（原因 published_only，级别不变）。智能体最多 comment（正文 `agent: true`）。企业管理员绕过名单，等于处处 manage，同样受状态限制。不存在的根或节点级别为空字符串。English: on a tree type each id's level is the lists' level clamped by the states (trashed: nothing below manage; archived: at most comment; publish-only: read as published below edit and for an agent), with the reason.

- 认证：`bearer`
- 范围：`app.resources:read`
- operationId：`checkResourceAccess`

**参数**

| 名称 | 位置 | 类型 | 必填 | 说明 |
|---|---|---|---|---|
| `type` | path | string | 是 | 资源类型，本应用在清单 `resources[]` 里声明的 `{应用 id}.{名字}` |
| `Idempotency-Key` | header | string |  | 每个 POST 都收。SDK 未传入时会生成 uuid v4。24 小时内同键同正文重放第一次的回答<br>最短 1，最长 255 |

**请求正文（必填）**（`application/json`）

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `ids` | string[] | 是 | 要查的资源 id<br>最多 500 项 |
| `level` | string |  | 可省略；写了就另外给出是否达到 |
| `user` | string |  | 应用自己的令牌必填：查谁 |
| `agent` | boolean |  | 树类型：按这个人的智能体算（最多 comment，publishOnly 时读发布版） |

**响应**

| 状态 | 说明 |
|---|---|
| 200 | 每个 id 的级别 |
| 401 | 见 [Unauthorized](#resp-unauthorized) |
| 403 | 见 [Forbidden](#resp-forbidden) |
| 404 | `not_found`：对应的记录不存在（客户中心没有这个客户，访客、服务或所指的东西不存在）。见 [NotFound](#resp-notfound) |
| 413 | `payload_too_large`：正文太大。见 [PayloadTooLarge](#resp-payloadtoolarge) |
| 422 | 字段或参数不被接受（正文读不出来、有未知字段、缺必填、取值越界）。见 [Unprocessable](#resp-unprocessable) |
| 429 | 见 [RateLimited](#resp-ratelimited) |
| 500 | 见 [Internal](#resp-internal) |
| 501 | `capability_off`：这个部署（或这个宿主）不提供这项能力，正文的 `capability` 是它的范围。见 [CapabilityOff](#resp-capabilityoff) |
| 503 | `unavailable`：暂时不可用（这个部署不提供的能力是 501 `capability_off`）。见 [Unavailable](#resp-unavailable) |

**200 响应正文**（`application/json`）

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `levels` | map[string, string] | 是 | id 到级别；空字符串是没有权限或没登记 |
| `allowed` | map[string, boolean] |  | id 到是否达到所问的级别；只在请求写了 level 时出现 |
| `reasons` | map[string, string] |  | 树类型：id 到状态给的原因（trashed、archived、published_only）；没有原因的 id 不出现 |

### `GET /access/{type}/visible`

分页列出一个人能看到的资源 id

需要范围 `app.resources:read`。English: the ids of the type's resources the person may see at least at the level, in id order, paged by cursor.

`level` 默认 `view`，`limit` 默认 200、最多 1000，`cursor` 用上一页的 `next`。页面会话列的是自己；应用自己的令牌必须带 `user`，否则 422 `invalid`。应用若维护自己的可见性镜像，用它刷新（再配合事件 `access.changed`），最终一致；单次读取仍应用 `check`。

树类型：列出这个类型（根类型列根、节点类型列节点）里此人经状态限制后达到 `level` 的 id，按 id 分页；企业管理员看到全部（仍受状态限制）。English: on a tree type the nodes (or roots) the person sees at the level after the states.

- 认证：`bearer`
- 范围：`app.resources:read`
- operationId：`listVisibleResources`

**参数**

| 名称 | 位置 | 类型 | 必填 | 说明 |
|---|---|---|---|---|
| `type` | path | string | 是 | 资源类型，本应用在清单 `resources[]` 里声明的 `{应用 id}.{名字}` |
| `level` | query | string |  | 最低级别：`view`（默认）、`comment`、`edit`、`manage` |
| `limit` | query | integer |  | 每页条数，默认 200，最多 1000 |
| `cursor` | query | string |  | 上一页的 `next` |
| `user` | query | string |  | 应用自己的令牌必须带：查谁 |
| `agent` | query | boolean |  | 树类型：按这个人的智能体算（最多 comment） |
| `raw` | query | boolean |  | 树类型：只按名单算，不套状态（回收站、归档、只读发布版）也不套企业管理员的越权，`agent` 不起作用；回收站里的节点也列出，每个 id 的级别在 `levels`。给在自己的 SQL 里过滤列表、自己套状态和越权的应用（不留节点集合 id 的副本）。English: the lists alone, before the states and the bypass, with each id's level in levels |

**响应**

| 状态 | 说明 |
|---|---|
| 200 | 一页 id |
| 401 | 见 [Unauthorized](#resp-unauthorized) |
| 403 | 见 [Forbidden](#resp-forbidden) |
| 404 | `not_found`：对应的记录不存在（客户中心没有这个客户，访客、服务或所指的东西不存在）。见 [NotFound](#resp-notfound) |
| 422 | 字段或参数不被接受（正文读不出来、有未知字段、缺必填、取值越界）。见 [Unprocessable](#resp-unprocessable) |
| 429 | 见 [RateLimited](#resp-ratelimited) |
| 500 | 见 [Internal](#resp-internal) |
| 501 | `capability_off`：这个部署（或这个宿主）不提供这项能力，正文的 `capability` 是它的范围。见 [CapabilityOff](#resp-capabilityoff) |
| 503 | `unavailable`：暂时不可用（这个部署不提供的能力是 501 `capability_off`）。见 [Unavailable](#resp-unavailable) |

**200 响应正文**（`application/json`）

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `ids` | string[] | 是 | 按 id 排序的一页 |
| `next` | string | 是 | 下一页的 cursor；空字符串是到头了 |
| `levels` | map[string, string] |  | 只在树类型带 `raw=true` 时有：id → 名单给出的级别（`view`、`comment`、`edit`、`manage`） |

### `POST /access/{type}/naming`

列出点到某个人的分享（按资源）

需要范围 `app.resources:read`。English: the grants of a flat type that name a person, their departments or their roles (expired ones too), by resource id; for an app that keeps the rules over the kernel's list (a member's sensitive records) and works a person's explicit level out before its own transaction.

只收应用自己的令牌（页面会话是 403 `forbidden`）。`user` 必填；`units` 是此人所在的部门（含上级：部门的条目也给下级）、`roles` 是此人的角色（企业角色 id，平台角色写作 `platform:<角色>`），都由应用自己算好传来，每项最多 500 个。`ids` 可省略：省略时查这个类型的全部资源，写了就只查这些（最多 500 个）。返回 `grants`：资源 id 到点到此人的分享（user 本人、所在部门、角色或 everyone），过期的也在内，由应用按自己的时钟判断；没有分享的资源不出现。企业管理员不因身份多出条目（这里只列名单，不做判断）。树类型 404 `not_found`。English: the app's own token only; units and roles are the app's own reading of the person; ids optional (absent: every resource of the type).

- 认证：`bearer`
- 范围：`app.resources:read`
- operationId：`listResourceGrantsNaming`

**参数**

| 名称 | 位置 | 类型 | 必填 | 说明 |
|---|---|---|---|---|
| `type` | path | string | 是 | 资源类型，本应用在清单 `resources[]` 里声明的 `{应用 id}.{名字}` |
| `Idempotency-Key` | header | string |  | 每个 POST 都收。SDK 未传入时会生成 uuid v4。24 小时内同键同正文重放第一次的回答<br>最短 1，最长 255 |

**请求正文（必填）**（`application/json`）

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `user` | string | 是 | 查谁（user id） |
| `units` | string[] |  | 此人所在的部门，含上级<br>最多 500 项 |
| `roles` | string[] |  | 此人的角色：企业角色 id，平台角色写作 platform:&lt;角色><br>最多 500 项 |
| `ids` | string[] |  | 可省略：只查这些资源<br>最多 500 项 |

示例：

```json
{
  "user": "u_1",
  "units": [
    "d_ops"
  ],
  "roles": [
    "platform:tenant.member"
  ],
  "ids": [
    "mb_1"
  ]
}
```

**响应**

| 状态 | 说明 |
|---|---|
| 200 | 按资源列出的分享 |
| 401 | 见 [Unauthorized](#resp-unauthorized) |
| 403 | 见 [Forbidden](#resp-forbidden) |
| 404 | `not_found`：对应的记录不存在（客户中心没有这个客户，访客、服务或所指的东西不存在）。见 [NotFound](#resp-notfound) |
| 413 | `payload_too_large`：正文太大。见 [PayloadTooLarge](#resp-payloadtoolarge) |
| 422 | 字段或参数不被接受（正文读不出来、有未知字段、缺必填、取值越界）。见 [Unprocessable](#resp-unprocessable) |
| 429 | 见 [RateLimited](#resp-ratelimited) |
| 500 | 见 [Internal](#resp-internal) |
| 501 | `capability_off`：这个部署（或这个宿主）不提供这项能力，正文的 `capability` 是它的范围。见 [CapabilityOff](#resp-capabilityoff) |
| 503 | `unavailable`：暂时不可用（这个部署不提供的能力是 501 `capability_off`）。见 [Unavailable](#resp-unavailable) |

**200 响应正文**（`application/json`）

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `grants` | map[string, ResourceGrant[]] | 是 | 资源 id 到点到此人的分享；没有的资源不出现 |

### `POST /access/{type}/departure`

收回点到一个离开的人的全部分享

需要范围 `app.resources:write`。English: the offboarding handover's part of a flat type whose rules are the app's: every grant naming the person (kind user, expired ones too) goes, and their pending access requests on the type are withdrawn (decided by `to`).

只收应用自己的令牌（页面会话是 403 `forbidden`）。`user` 和 `to` 必填且不能相同，否则 422 `invalid`。重复调用没有副作用。名单变了的每个资源都会发事件 `access.changed`，并写审计 `access.departure`。树类型 404 `not_found`。English: the app's own token only; idempotent; access.changed for every resource whose list changed.

- 认证：`bearer`
- 范围：`app.resources:write`
- operationId：`departResourceGrants`

**参数**

| 名称 | 位置 | 类型 | 必填 | 说明 |
|---|---|---|---|---|
| `type` | path | string | 是 | 资源类型，本应用在清单 `resources[]` 里声明的 `{应用 id}.{名字}` |
| `Idempotency-Key` | header | string |  | 每个 POST 都收。SDK 未传入时会生成 uuid v4。24 小时内同键同正文重放第一次的回答<br>最短 1，最长 255 |

**请求正文（必填）**（`application/json`）

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `user` | string | 是 | 离开的人 |
| `to` | string | 是 | 接手的人（记为撤回申请的人） |

示例：

```json
{
  "user": "u_1",
  "to": "u_2"
}
```

**响应**

| 状态 | 说明 |
|---|---|
| 200 | 已收回 |
| 401 | 见 [Unauthorized](#resp-unauthorized) |
| 403 | 见 [Forbidden](#resp-forbidden) |
| 404 | `not_found`：对应的记录不存在（客户中心没有这个客户，访客、服务或所指的东西不存在）。见 [NotFound](#resp-notfound) |
| 413 | `payload_too_large`：正文太大。见 [PayloadTooLarge](#resp-payloadtoolarge) |
| 422 | 字段或参数不被接受（正文读不出来、有未知字段、缺必填、取值越界）。见 [Unprocessable](#resp-unprocessable) |
| 429 | 见 [RateLimited](#resp-ratelimited) |
| 500 | 见 [Internal](#resp-internal) |
| 501 | `capability_off`：这个部署（或这个宿主）不提供这项能力，正文的 `capability` 是它的范围。见 [CapabilityOff](#resp-capabilityoff) |
| 503 | `unavailable`：暂时不可用（这个部署不提供的能力是 501 `capability_off`）。见 [Unavailable](#resp-unavailable) |

**200 响应正文**（`application/json`）

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `ok` | boolean | 是 |  |

### `GET /access/{type}/mine`

一个人在树模型每个集合里的级别

需要范围 `app.resources:read`。English: the person's level in every set of the tree model that names them, a page at a time (the app filters its own lists by the set id it keeps for each node).

`type` 是本应用的一个树类型，或 `*`（集合对所有类型是同一份）。页面会话查的是自己；应用自己的令牌必须带 `user`，否则 422 `invalid`。一页最多 10000 个集合，`truncated` 为 true 时用 `next` 作 `after` 再取。企业管理员绕过名单，集合里不体现：`bypass` 为 true 时应用把此人当作处处 manage（再受状态限制）。

- 认证：`bearer`
- 范围：`app.resources:read`
- operationId：`mineResourceSets`

**参数**

| 名称 | 位置 | 类型 | 必填 | 说明 |
|---|---|---|---|---|
| `type` | path | string | 是 | 本应用的树类型 `{应用 id}.{名字}`，或 `*` |
| `after` | query | string |  | 上一页的 `next` |
| `user` | query | string |  | 应用自己的令牌必须带：查谁 |

**响应**

| 状态 | 说明 |
|---|---|
| 200 | 集合 id 到级别 |
| 401 | 见 [Unauthorized](#resp-unauthorized) |
| 403 | 见 [Forbidden](#resp-forbidden) |
| 404 | `not_found`：对应的记录不存在（客户中心没有这个客户，访客、服务或所指的东西不存在）。见 [NotFound](#resp-notfound) |
| 422 | 字段或参数不被接受（正文读不出来、有未知字段、缺必填、取值越界）。见 [Unprocessable](#resp-unprocessable) |
| 429 | 见 [RateLimited](#resp-ratelimited) |
| 500 | 见 [Internal](#resp-internal) |
| 501 | `capability_off`：这个部署（或这个宿主）不提供这项能力，正文的 `capability` 是它的范围。见 [CapabilityOff](#resp-capabilityoff) |
| 503 | `unavailable`：暂时不可用（这个部署不提供的能力是 501 `capability_off`）。见 [Unavailable](#resp-unavailable) |

**200 响应正文**（`application/json`）

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `sets` | map[string, string] | 是 | 集合 id 到级别（view、comment、edit、manage） |
| `truncated` | boolean | 是 | 还有下一页 |
| `next` | string | 是 | 下一页的 after |
| `bypass` | boolean | 是 | 企业管理员：集合里不体现，应用按处处 manage 处理（再受状态限制） |

示例：

```json
{
  "sets": {
    "5f0c2a9e41d7b3c8a6e2f104": "edit"
  },
  "truncated": false,
  "next": "",
  "bypass": false
}
```

### `PUT /access/{type}/{id}/inherit`

节点停止或恢复继承上面的名单

需要范围 `app.resources:write`。English: a node of a tree type stops or restores inheriting what is above it (the platform's share dialog: 「仅名单上的人可访问」). Its place does not change; moving stays the app's.

只用于树类型的节点类型。页面会话的人要能管理这个节点（状态限制之前的级别），应用自己的令牌不受限。停止继承时，节点现在的有效名单成为它自己的名单（名字从写下每一条的地方取），停止的人保留管理级别，所以开关本身不会让谁失去访问；没有管理者时是 422。恢复继承时，去掉上面已经以相同或更高级别给出的条目。和现在相同时什么也不改。之后发 access.changed，记审计 access.tree.inherit。返回集合变了的节点。

- 认证：`bearer`
- 范围：`app.resources:write`
- operationId：`setResourceInherit`

**参数**

| 名称 | 位置 | 类型 | 必填 | 说明 |
|---|---|---|---|---|
| `type` | path | string | 是 | 本应用的树类型的节点类型 `{应用 id}.{名字}` |
| `id` | path | string | 是 | 节点 id |
| `Idempotency-Key` | header | string |  | 每个 POST 都收。SDK 未传入时会生成 uuid v4。24 小时内同键同正文重放第一次的回答<br>最短 1，最长 255 |

**请求正文（必填）**（`application/json`）

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `inherit` | boolean | 是 |  |

示例：

```json
{
  "inherit": false
}
```

**响应**

| 状态 | 说明 |
|---|---|
| 200 | 集合变了的节点 |
| 401 | 见 [Unauthorized](#resp-unauthorized) |
| 403 | 见 [Forbidden](#resp-forbidden) |
| 404 | `not_found`：对应的记录不存在（客户中心没有这个客户，访客、服务或所指的东西不存在）。见 [NotFound](#resp-notfound) |
| 422 | 字段或参数不被接受（正文读不出来、有未知字段、缺必填、取值越界）。见 [Unprocessable](#resp-unprocessable) |
| 429 | 见 [RateLimited](#resp-ratelimited) |
| 500 | 见 [Internal](#resp-internal) |
| 501 | `capability_off`：这个部署（或这个宿主）不提供这项能力，正文的 `capability` 是它的范围。见 [CapabilityOff](#resp-capabilityoff) |
| 503 | `unavailable`：暂时不可用（这个部署不提供的能力是 501 `capability_off`）。见 [Unavailable](#resp-unavailable) |

**200 响应正文**（`application/json`）

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `type` | string |  |  |
| `id` | string |  |  |
| `changed` | map[string, string] | 是 | 节点 id 到它现在的集合 id（重新鉴权它打开的文档，更新应用保存的集合 id） |

示例：

```json
{
  "type": "wiki.page",
  "id": "pg_1",
  "changed": {
    "pg_1": "5f0c2a9e41d7b3c8a6e2f104",
    "pg_2": "5f0c2a9e41d7b3c8a6e2f104"
  }
}
```

### `POST /access/{type}/{id}/reconcile`

用应用自己的树校正平台里一个根下的节点

需要范围 `app.resources:write`。English: makes the kernel's nodes of one root what the app's tree says (an app outside the platform writes the kernel first and its own rows after; a periodic reconcile repairs a failure between the two).

只用于树类型的根类型（`id` 是根），只收应用自己的令牌（页面会话是 403 `forbidden`）。`spec` 是根的状态，`nodes` 是这个根下应用的全部节点（`inherit` 省略为 true）：没列出的节点被删除，其余的按位置、继承和回收站状态写入。显式名单不校正（只存在平台里）。返回改了几个节点；大于 0 时记审计 access.tree.reconcile。

- 认证：`bearer`
- 范围：`app.resources:write`
- operationId：`reconcileResourceTree`

**参数**

| 名称 | 位置 | 类型 | 必填 | 说明 |
|---|---|---|---|---|
| `type` | path | string | 是 | 资源类型，本应用在清单 `resources[]` 里声明的 `{应用 id}.{名字}` |
| `id` | path | string | 是 | 根的 id |
| `Idempotency-Key` | header | string |  | 每个 POST 都收。SDK 未传入时会生成 uuid v4。24 小时内同键同正文重放第一次的回答<br>最短 1，最长 255 |

**请求正文（必填）**（`application/json`）

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `spec` | object |  | 根的状态 |
| `spec.archived` | boolean |  |  |
| `spec.publishOnly` | boolean |  |  |
| `nodes` | object[] | 是 |  |
| `nodes[].id` | string | 是 |  |
| `nodes[].root` | string |  |  |
| `nodes[].parent` | string |  | 省略为根的顶层 |
| `nodes[].inherit` | boolean |  | 省略为 true |
| `nodes[].trashed` | boolean |  |  |

**响应**

| 状态 | 说明 |
|---|---|
| 200 | 改了几个节点 |
| 401 | 见 [Unauthorized](#resp-unauthorized) |
| 403 | 见 [Forbidden](#resp-forbidden) |
| 404 | `not_found`：对应的记录不存在（客户中心没有这个客户，访客、服务或所指的东西不存在）。见 [NotFound](#resp-notfound) |
| 413 | `payload_too_large`：正文太大。见 [PayloadTooLarge](#resp-payloadtoolarge) |
| 422 | 字段或参数不被接受（正文读不出来、有未知字段、缺必填、取值越界）。见 [Unprocessable](#resp-unprocessable) |
| 429 | 见 [RateLimited](#resp-ratelimited) |
| 500 | 见 [Internal](#resp-internal) |
| 501 | `capability_off`：这个部署（或这个宿主）不提供这项能力，正文的 `capability` 是它的范围。见 [CapabilityOff](#resp-capabilityoff) |
| 503 | `unavailable`：暂时不可用（这个部署不提供的能力是 501 `capability_off`）。见 [Unavailable](#resp-unavailable) |

**200 响应正文**（`application/json`）

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `changed` | integer | 是 | 改了几个节点（写入的加删除的） |

## errors

本应用的错误：脱敏后给开发者组织（开发者平台「错误」、k3 logs --errors）

### `POST /errors`

向开发者平台报告本应用的错误

需要范围 `app.errors:report`。English: reports the app's own errors to its developer organisation (ADR-0130 H8).

平台在保存前脱敏：`message` 与 `frames` 里的值（引号里的内容、数字、邮箱、网址、令牌、`key=value` 的值，以及 `message` 里所有非 ASCII 的文字，如中文）换成占位符；想让开发者读得懂，就报 `code` + 固定的 `template`（应用代码里的常量，原文保留）+ `params`，`route` 里像值的段换成 `:id`，`code` 与 `kind` 只收标识符。开发者平台只看到脱敏后的样子，看不到企业；`params` 是消息引用的值，永远不进开发者视图，只在企业批准的限时诊断里（去掉密钥后）给开发者看。一次 1–50 条，回答 202。每个应用每分钟最多保存 120 条，超过的不保存。

- 认证：`bearer`
- 范围：`app.errors:report`
- operationId：`reportErrors`

**参数**

| 名称 | 位置 | 类型 | 必填 | 说明 |
|---|---|---|---|---|
| `Idempotency-Key` | header | string |  | 每个 POST 都收。SDK 未传入时会生成 uuid v4。24 小时内同键同正文重放第一次的回答<br>最短 1，最长 255 |

**请求正文（必填）**（`application/json`）

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `errors` | ErrorOccurrence[] | 是 | 最多 50 项 |
| `errors[].kind` | string |  | 错误类型（异常类名等标识符） |
| `errors[].code` | string |  | 应用自己的错误码（标识符，最长 64） |
| `errors[].template` | string |  | 固定模板：应用代码里的常量（如「会员不存在：{name}」），开发者看到原文（其中的数字、引号内容等仍脱敏）；值放进 params |
| `errors[].message` | string |  | 自由文本：开发者看到的是脱敏后的样子，值换成占位符，所有非 ASCII 的文字（中文等）也换成 &lt;text>；想让开发者读得懂，用 template + params |
| `errors[].params` | string[] |  | 消息引用的值：不进开发者视图<br>最多 10 项 |
| `errors[].frames` | string[] |  | 调用栈，最上面一帧在前<br>最多 20 项 |
| `errors[].method` | string |  |  |
| `errors[].route` | string |  | 出错的路径；查询串去掉，像值的段换成 :id |
| `errors[].requestId` | string |  |  |
| `errors[].version` | string |  | 应用版本 |
| `errors[].at` | string(date-time) |  | 发生时间；缺省或差一天以上用平台时间 |

**响应**

| 状态 | 说明 |
|---|---|
| 202 | 已收下 |
| 401 | 见 [Unauthorized](#resp-unauthorized) |
| 403 | 见 [Forbidden](#resp-forbidden) |
| 413 | `payload_too_large`：正文太大。见 [PayloadTooLarge](#resp-payloadtoolarge) |
| 422 | 字段或参数不被接受（正文读不出来、有未知字段、缺必填、取值越界）。见 [Unprocessable](#resp-unprocessable) |
| 429 | 见 [RateLimited](#resp-ratelimited) |
| 500 | 见 [Internal](#resp-internal) |
| 501 | `capability_off`：这个部署（或这个宿主）不提供这项能力，正文的 `capability` 是它的范围。见 [CapabilityOff](#resp-capabilityoff) |
| 503 | `unavailable`：暂时不可用（这个部署不提供的能力是 501 `capability_off`）。见 [Unavailable](#resp-unavailable) |

**202 响应正文**（`application/json`）

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `accepted` | integer | 是 |  |

## logs

本应用主动写的结构化日志：按企业脱敏后留 24 小时，给开发者组织实时看（开发者平台「实时日志」、k3 tail）

### `POST /logs`

写本应用的结构化日志

需要范围 `app.logs:write`。English: writes the app's own structured log lines for its developer organisation (ADR-0149 §3); the developer reads them live (`k3 tail`, GET /api/dev/apps/{id}/tail). The process's stdout and stderr never reach the developer: only what the app writes here.

平台在保存前按企业脱敏：开发者组织自己的测试企业保留原文，只去掉密钥一类（令牌、`sk-live-…` 之类的密钥、`password=…`）；其他企业的 `message` 和每个字段值都脱敏（手机号、身份证、邮箱、长数字、引号里的值、`key=value` 的值、所有非 ASCII 文字换成占位符），字段名保留（不是标识符的字段名也脱敏），企业只给不透明的 `tenantRef`，原文不落库。`template: true` 声明消息是固定模板（应用代码里的常量）：模板里的中文保留，值仍脱敏，字段值照样脱敏。一次 1–100 行；`message` 最多 2 KiB、字段最多 20 个、每个值最多 256 字节，超出截断并标记。每个应用每秒最多保存 50 行，超出的丢弃并计数（开发者在实时日志里看到「丢了 N 行」），回答里的 `dropped` 是这一批丢了几行。回答 202。

- 认证：`bearer`
- 范围：`app.logs:write`
- operationId：`writeLogs`

**参数**

| 名称 | 位置 | 类型 | 必填 | 说明 |
|---|---|---|---|---|
| `Idempotency-Key` | header | string |  | 每个 POST 都收。SDK 未传入时会生成 uuid v4。24 小时内同键同正文重放第一次的回答<br>最短 1，最长 255 |

**请求正文（必填）**（`application/json`）

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `version` | string |  | 应用版本（托管进程的 K3_VERSION）；行里的 version 优先 |
| `lines` | LogLine[] | 是 | 最多 100 项 |
| `lines[].level` | string |  | 级别；warning 当 warn，fatal 当 error，其他当 info<br>可选值：`debug`、`info`、`warn`、`error` |
| `lines[].message` | string | 是 | 消息，最多 2 KiB；线上企业的脱敏后保存 |
| `lines[].fields` | map[string, string] |  | 字段：名保留，值按文本保存（最多 256 字节；数字和布尔值也收，按 JSON 的写法存成文本），线上企业的值脱敏 |
| `lines[].template` | boolean |  | 消息是固定模板（应用代码里的常量）：模板里的中文保留，值仍脱敏 |
| `lines[].version` | string |  | 这一行的应用版本 |
| `lines[].traceId` | string |  | 写这一行时所在请求的追踪 id（traceparent 里的；SDK 自动带上），实时日志据此链接到那次请求；不合格式的丢弃<br>格式 `^[0-9a-f]{32}$` |
| `lines[].at` | string(date-time) |  | 发生时间；缺省或差一天以上用平台时间 |

**响应**

| 状态 | 说明 |
|---|---|
| 202 | 已收下 |
| 401 | 见 [Unauthorized](#resp-unauthorized) |
| 403 | 见 [Forbidden](#resp-forbidden) |
| 413 | `payload_too_large`：正文太大。见 [PayloadTooLarge](#resp-payloadtoolarge) |
| 422 | 字段或参数不被接受（正文读不出来、有未知字段、缺必填、取值越界）。见 [Unprocessable](#resp-unprocessable) |
| 429 | 见 [RateLimited](#resp-ratelimited) |
| 500 | 见 [Internal](#resp-internal) |
| 501 | `capability_off`：这个部署（或这个宿主）不提供这项能力，正文的 `capability` 是它的范围。见 [CapabilityOff](#resp-capabilityoff) |
| 503 | `unavailable`：暂时不可用（这个部署不提供的能力是 501 `capability_off`）。见 [Unavailable](#resp-unavailable) |

**202 响应正文**（`application/json`）

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `accepted` | integer | 是 | 保存的行数 |
| `dropped` | integer | 是 | 超过每秒 50 行丢弃的行数 |
| `truncated` | integer | 是 | 截断过的行数 |

## traces

本应用对请求追踪加的段：处理请求的那一段和应用自己加的步骤（ADR-0155 §2）

### `POST /traces`

写本应用的请求追踪段

需要范围 `app.traces:write`。English: sends the app's own spans of the platform's request traces (ADR-0155 §2): the handler of a request (kind `server`, the SDK's middleware) and the steps the app adds (kind `internal`, `appkit.Trace.Span`). The platform records the edge's, the gateway's and every `/app/v1` call's span itself.

平台的网关为托管应用的每个请求开一个追踪，交给应用 `traceparent` 和带平台标记的 `tracestate`（`raise=…`）。每个段要带上它所在请求的 `traceId` 和这个标记（`mark`）：标记由平台为「本应用 + 这个令牌的企业 + 这个追踪」签发，对不上的段拒收（`refused`）。平台在尾部决定保留哪些追踪（所有 5xx、慢于本应用 p99 的，其余线上 1 %，测试企业按开发者的设置），段先缓冲 30 秒等决定。段名是应用代码里的常量：线上企业的段名保留文字、值脱敏；属性只收登记过的名字（`http.method` `http.route` `http.status_code` `k3.version` `k3.op` `k3.dataset` `k3.table` `k3.event` `k3.service` `k3.provider` `k3.items` `db.system` `db.operation` `error.code` `error.type` `peer.service` `cache.hit` `retry.count` `messaging.operation` `rpc.method`），值按企业脱敏，其他名字丢弃并计入 `truncated`。一次 1–200 段；每个应用在每个副本上每秒最多缓冲 500 段，超出的丢弃并计数。回答 202。

- 认证：`bearer`
- 范围：`app.traces:write`
- operationId：`writeTraceSpans`

**参数**

| 名称 | 位置 | 类型 | 必填 | 说明 |
|---|---|---|---|---|
| `Idempotency-Key` | header | string |  | 每个 POST 都收。SDK 未传入时会生成 uuid v4。24 小时内同键同正文重放第一次的回答<br>最短 1，最长 255 |

**请求正文（必填）**（`application/json`）

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `version` | string |  | 应用版本（托管进程的 K3_VERSION） |
| `spans` | TraceSpan[] | 是 | 最多 200 项 |
| `spans[].traceId` | string | 是 | 请求的 traceparent 里的追踪 id<br>格式 `^[0-9a-f]{32}$` |
| `spans[].spanId` | string | 是 | 这一段的 id（应用生成，随机）<br>格式 `^[0-9a-f]{16}$` |
| `spans[].parentId` | string |  | 上一层段的 id：处理请求的段的上一层是 traceparent 里的父段<br>格式 `^[0-9a-f]{16}$` |
| `spans[].mark` | string | 是 | 请求的 tracestate 里平台成员 raise= 的值 |
| `spans[].name` | string | 是 | 段名，最多 128 字节；应用代码里的常量，不放值 |
| `spans[].kind` | string |  | server：处理请求的那一段；internal（缺省）：应用自己加的步骤<br>可选值：`server`、`internal` |
| `spans[].start` | string(date-time) | 是 | 开始时间；一小时以前或五分钟以后的拒收 |
| `spans[].durationUs` | integer(int64) | 是 | 耗时，微秒；超过一小时按一小时<br>最小 0 |
| `spans[].status` | integer |  | 状态码（HTTP 状态一类） |
| `spans[].error` | boolean |  | 这一步失败了；状态码 500 以上也算 |
| `spans[].attrs` | map[string, string] |  | 属性：只收登记过的名字（见接口说明），最多 32 个，值最多 256 字节，线上企业的值脱敏 |

**响应**

| 状态 | 说明 |
|---|---|
| 202 | 已收下 |
| 401 | 见 [Unauthorized](#resp-unauthorized) |
| 403 | 见 [Forbidden](#resp-forbidden) |
| 413 | `payload_too_large`：正文太大。见 [PayloadTooLarge](#resp-payloadtoolarge) |
| 422 | 字段或参数不被接受（正文读不出来、有未知字段、缺必填、取值越界）。见 [Unprocessable](#resp-unprocessable) |
| 429 | 见 [RateLimited](#resp-ratelimited) |
| 500 | 见 [Internal](#resp-internal) |
| 501 | `capability_off`：这个部署（或这个宿主）不提供这项能力，正文的 `capability` 是它的范围。见 [CapabilityOff](#resp-capabilityoff) |
| 503 | `unavailable`：暂时不可用（这个部署不提供的能力是 501 `capability_off`）。见 [Unavailable](#resp-unavailable) |

**202 响应正文**（`application/json`）

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `accepted` | integer | 是 | 缓冲下来的段数 |
| `dropped` | integer | 是 | 超过每秒限额丢弃的段数 |
| `refused` | integer | 是 | 标记对不上、id 不合格式或时间不合理而拒收的段数 |
| `truncated` | integer | 是 | 丢了属性或截断过的段数 |

## analytics

本应用自定义的分析数据点：按清单登记的数据集写入，按企业脱敏后留 31 天，开发者组织按字段名查询（k3 analytics query）

### `POST /analytics`

写本应用的分析数据点

需要范围 `app.analytics:write`。English: writes data points into the datasets the app's manifest declares (`analytics: [{name, blobs, doubles}]`, ADR-0155 §1), for its developer organisation to query by field name (POST /api/dev/apps/{id}/analytics/query, `k3 analytics query`).

一个点属于一个数据集：最多 1 个索引 `index`（≤ 96 字节）、按登记顺序的字符串 `blobs`（最多 20 个，每个 ≤ 256 字节，超出截断）和数值 `doubles`（最多 20 个，有限数）。平台补上企业（只给不透明的 `tenant` 引用）、版本和 test/live。开发者组织自己的测试企业保留字符串原文（只去掉密钥一类）；其他企业的 `index` 和每个字符串都按日志的规则脱敏，原文不落库；数值原样保存。没登记的数据集、比登记多的值、超长的索引丢弃并计数；每个应用每秒最多保存 200 点，超出的丢弃并计数（开发者查询时看到 `dropped`）。保留 31 天，只追加不改。一次 1–250 点，回答 202。

- 认证：`bearer`
- 范围：`app.analytics:write`
- operationId：`writeAnalytics`

**参数**

| 名称 | 位置 | 类型 | 必填 | 说明 |
|---|---|---|---|---|
| `Idempotency-Key` | header | string |  | 每个 POST 都收。SDK 未传入时会生成 uuid v4。24 小时内同键同正文重放第一次的回答<br>最短 1，最长 255 |

**请求正文（必填）**（`application/json`）

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `version` | string |  | 应用版本（托管进程的 K3_VERSION）：数据集按这个版本的清单认 |
| `points` | AnalyticsPoint[] | 是 | 最多 250 项 |
| `points[].dataset` | string | 是 | 清单 analytics 里登记的数据集名 |
| `points[].index` | string |  | 索引（可选），线上企业的脱敏后保存<br>最长 96 |
| `points[].blobs` | string[] |  | 字符串值，按数据集登记的 blobs 顺序；每个最多 256 字节，线上企业的脱敏后保存<br>最多 20 项 |
| `points[].doubles` | number[] |  | 数值，按数据集登记的 doubles 顺序<br>最多 20 项 |
| `points[].at` | string(date-time) |  | 发生时间；缺省、超前 5 分钟以上或早于一天用平台时间 |

**响应**

| 状态 | 说明 |
|---|---|
| 202 | 已收下 |
| 401 | 见 [Unauthorized](#resp-unauthorized) |
| 403 | 见 [Forbidden](#resp-forbidden) |
| 413 | `payload_too_large`：正文太大。见 [PayloadTooLarge](#resp-payloadtoolarge) |
| 422 | 字段或参数不被接受（正文读不出来、有未知字段、缺必填、取值越界）。见 [Unprocessable](#resp-unprocessable) |
| 429 | 见 [RateLimited](#resp-ratelimited) |
| 500 | 见 [Internal](#resp-internal) |
| 501 | `capability_off`：这个部署（或这个宿主）不提供这项能力，正文的 `capability` 是它的范围。见 [CapabilityOff](#resp-capabilityoff) |
| 503 | `unavailable`：暂时不可用（这个部署不提供的能力是 501 `capability_off`）。见 [Unavailable](#resp-unavailable) |

**202 响应正文**（`application/json`）

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `accepted` | integer | 是 | 保存的点数 |
| `dropped` | integer | 是 | 丢弃的点数 |
| `truncated` | integer | 是 | 字符串截断过的点数 |
| `reasons` | map[string, integer] |  | 按原因的丢弃数：rate_limited（超过每秒 200 点）、unknown_dataset（版本没登记的数据集）、invalid（比登记多的值、超长的索引、不是有限数） |

## blobs

企业文件存储里本应用自己的文件夹：存取、元数据、列出、复制、删除，以及直传用的预签名地址

### `GET /blobs/objects/{key...}`

读取本应用文件夹里一个对象的字节

需要范围 `app.blobs:read`。English: streams the bytes of the object `key` of the app's folder.

回答的正文是对象的原始字节，`Content-Type`、`Content-Length`、`Last-Modified` 取自对象，并带 `X-Content-Type-Options: nosniff`。对象不存在是 404 `not_found`。
SDK：Go `App.Blobs.Get` 回答 `RawResponse`（调用方读完 `Body` 后关闭），TS `app.blobs.get` 回答 fetch 的 `Response`。只要元数据用 `blobs.head`。

- 认证：`bearer`
- 范围：`app.blobs:read`
- operationId：`getBlob`

**参数**

| 名称 | 位置 | 类型 | 必填 | 说明 |
|---|---|---|---|---|
| `key` | path | string | 是 | 对象的键，以 apps/{本应用 id}/ 开头，可以含 /（路径里的 / 不要转义） |

**响应**

| 状态 | 说明 |
|---|---|
| 200 | 对象的字节 |
| 401 | 见 [Unauthorized](#resp-unauthorized) |
| 403 | 见 [Forbidden](#resp-forbidden) |
| 404 | `not_found`：对应的记录不存在（客户中心没有这个客户，访客、服务或所指的东西不存在）。见 [NotFound](#resp-notfound) |
| 422 | 字段或参数不被接受（正文读不出来、有未知字段、缺必填、取值越界）。见 [Unprocessable](#resp-unprocessable) |
| 429 | 见 [RateLimited](#resp-ratelimited) |
| 500 | 见 [Internal](#resp-internal) |
| 501 | `capability_off`：这个部署（或这个宿主）不提供这项能力，正文的 `capability` 是它的范围。见 [CapabilityOff](#resp-capabilityoff) |
| 503 | `unavailable`：暂时不可用（这个部署不提供的能力是 501 `capability_off`）。见 [Unavailable](#resp-unavailable) |

**200 响应正文**（`application/octet-stream`）

类型 `string(binary)`

### `PUT /blobs/objects/{key...}`

把一段字节存成本应用文件夹里的一个对象

需要范围 `app.blobs:write`。English: stores the request body, raw bytes, as the object `key` in the app's folder of the tenant's file store (the platform's, or the tenant's own bucket).

路径里的 `{key...}` 吃掉余下的整个路径，键必须以 `apps/{本应用 id}/` 开头，且其后至少还有一个字符；不能有空的、`.` 或 `..` 段，不能有反斜杠和控制字符，最长 1024 字节。正文就是对象的字节（不是 JSON），`Content-Type` 请求头是对象的类型（没有时按 `application/octet-stream`），必须带 `Content-Length`（没有是 411）。经平台存的单个对象最大 512 MiB，更大的用 `POST /blobs/presign` 拿上传地址直传。企业的存储额度对两条路都有效。成功回答 204，没有正文。
SDK：Go `App.Blobs.Put`（`io.Reader`、长度、类型），TS `app.blobs.put`（`Blob`、`ArrayBuffer`、`Uint8Array` 或流）。

- 认证：`bearer`
- 范围：`app.blobs:write`
- operationId：`putBlob`

**参数**

| 名称 | 位置 | 类型 | 必填 | 说明 |
|---|---|---|---|---|
| `key` | path | string | 是 | 对象的键，以 apps/{本应用 id}/ 开头，可以含 /（路径里的 / 不要转义） |

**请求正文（必填）**（`application/octet-stream`）

类型 `string(binary)`

对象的字节

**响应**

| 状态 | 说明 |
|---|---|
| 204 | 已存下。没有正文 |
| 401 | 见 [Unauthorized](#resp-unauthorized) |
| 403 | 见 [Forbidden](#resp-forbidden) |
| 411 | 没带 `Content-Length`。见 [LengthRequired](#resp-lengthrequired) |
| 413 | `payload_too_large`：正文太大。见 [PayloadTooLarge](#resp-payloadtoolarge) |
| 422 | 字段或参数不被接受（正文读不出来、有未知字段、缺必填、取值越界）。见 [Unprocessable](#resp-unprocessable) |
| 429 | 见 [RateLimited](#resp-ratelimited) |
| 500 | 见 [Internal](#resp-internal) |
| 501 | `capability_off`：这个部署（或这个宿主）不提供这项能力，正文的 `capability` 是它的范围。见 [CapabilityOff](#resp-capabilityoff) |
| 503 | `unavailable`：暂时不可用（这个部署不提供的能力是 501 `capability_off`）。见 [Unavailable](#resp-unavailable) |

### `DELETE /blobs/objects/{key...}`

删除本应用文件夹里的一个对象

需要范围 `app.blobs:write`。English: deletes the object `key`; deleting what is not there is done.

成功回答 204，没有正文；对象本来就不存在也是 204。

- 认证：`bearer`
- 范围：`app.blobs:write`
- operationId：`deleteBlob`

**参数**

| 名称 | 位置 | 类型 | 必填 | 说明 |
|---|---|---|---|---|
| `key` | path | string | 是 | 对象的键，以 apps/{本应用 id}/ 开头，可以含 /（路径里的 / 不要转义） |

**响应**

| 状态 | 说明 |
|---|---|
| 204 | 已删除（或本来就不在）。没有正文 |
| 401 | 见 [Unauthorized](#resp-unauthorized) |
| 403 | 见 [Forbidden](#resp-forbidden) |
| 422 | 字段或参数不被接受（正文读不出来、有未知字段、缺必填、取值越界）。见 [Unprocessable](#resp-unprocessable) |
| 429 | 见 [RateLimited](#resp-ratelimited) |
| 500 | 见 [Internal](#resp-internal) |
| 501 | `capability_off`：这个部署（或这个宿主）不提供这项能力，正文的 `capability` 是它的范围。见 [CapabilityOff](#resp-capabilityoff) |
| 503 | `unavailable`：暂时不可用（这个部署不提供的能力是 501 `capability_off`）。见 [Unavailable](#resp-unavailable) |

### `GET /blobs/meta/{key...}`

读取一个对象的元数据

需要范围 `app.blobs:read`。English: the size, type and last write time of the object `key`, without its bytes.

对象不存在是 404 `not_found`。

- 认证：`bearer`
- 范围：`app.blobs:read`
- operationId：`headBlob`

**参数**

| 名称 | 位置 | 类型 | 必填 | 说明 |
|---|---|---|---|---|
| `key` | path | string | 是 | 对象的键，以 apps/{本应用 id}/ 开头，可以含 /（路径里的 / 不要转义） |

**响应**

| 状态 | 说明 |
|---|---|
| 200 | 对象的元数据 |
| 401 | 见 [Unauthorized](#resp-unauthorized) |
| 403 | 见 [Forbidden](#resp-forbidden) |
| 404 | `not_found`：对应的记录不存在（客户中心没有这个客户，访客、服务或所指的东西不存在）。见 [NotFound](#resp-notfound) |
| 422 | 字段或参数不被接受（正文读不出来、有未知字段、缺必填、取值越界）。见 [Unprocessable](#resp-unprocessable) |
| 429 | 见 [RateLimited](#resp-ratelimited) |
| 500 | 见 [Internal](#resp-internal) |
| 501 | `capability_off`：这个部署（或这个宿主）不提供这项能力，正文的 `capability` 是它的范围。见 [CapabilityOff](#resp-capabilityoff) |
| 503 | `unavailable`：暂时不可用（这个部署不提供的能力是 501 `capability_off`）。见 [Unavailable](#resp-unavailable) |

**200 响应正文**（`application/json`）

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `key` | string | 是 | 对象的键，以 apps/{本应用 id}/ 开头 |
| `size` | integer | 是 | 字节数 |
| `contentType` | string |  | 对象的类型。可能为空 |
| `modTime` | string(date-time) | 是 | 最后写入的时间 |

示例：

```json
{
  "key": "apps/crm/exports/2026-10.csv",
  "size": 20480,
  "contentType": "text/csv",
  "modTime": "2026-10-31T08:00:00Z"
}
```

### `GET /blobs`

列出本应用文件夹里某个前缀下的对象

需要范围 `app.blobs:read`。English: lists the objects below a prefix of the app's folder.

`prefix` 省略时列出整个文件夹 `apps/{本应用 id}/`；给了就必须在本应用的文件夹里，可以以 `/` 结尾。返回 `{items}`。

- 认证：`bearer`
- 范围：`app.blobs:read`
- operationId：`listBlobs`

**参数**

| 名称 | 位置 | 类型 | 必填 | 说明 |
|---|---|---|---|---|
| `prefix` | query | string |  | 前缀，以 apps/{本应用 id}/ 开头。省略时是整个文件夹 |

**响应**

| 状态 | 说明 |
|---|---|
| 200 | 对象 |
| 401 | 见 [Unauthorized](#resp-unauthorized) |
| 403 | 见 [Forbidden](#resp-forbidden) |
| 422 | 字段或参数不被接受（正文读不出来、有未知字段、缺必填、取值越界）。见 [Unprocessable](#resp-unprocessable) |
| 429 | 见 [RateLimited](#resp-ratelimited) |
| 500 | 见 [Internal](#resp-internal) |
| 501 | `capability_off`：这个部署（或这个宿主）不提供这项能力，正文的 `capability` 是它的范围。见 [CapabilityOff](#resp-capabilityoff) |
| 503 | `unavailable`：暂时不可用（这个部署不提供的能力是 501 `capability_off`）。见 [Unavailable](#resp-unavailable) |

**200 响应正文**（`application/json`）

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `items` | BlobInfo[] | 是 | 对象 |
| `items[].key` | string | 是 | 对象的键，以 apps/{本应用 id}/ 开头 |
| `items[].size` | integer | 是 | 字节数 |
| `items[].contentType` | string |  | 对象的类型。可能为空 |
| `items[].modTime` | string(date-time) | 是 | 最后写入的时间 |

示例：

```json
{
  "items": [
    {
      "key": "apps/crm/exports/2026-10.csv",
      "size": 20480,
      "contentType": "text/csv",
      "modTime": "2026-10-31T08:00:00Z"
    }
  ]
}
```

### `POST /blobs/copy`

在本应用的文件夹里复制一个对象

需要范围 `app.blobs:write`。English: copies the object `src` to `dst`, both in the app's folder.

`src` 不存在是 404 `not_found`。成功回答 204，没有正文。

- 认证：`bearer`
- 范围：`app.blobs:write`
- operationId：`copyBlob`

**参数**

| 名称 | 位置 | 类型 | 必填 | 说明 |
|---|---|---|---|---|
| `Idempotency-Key` | header | string |  | 每个 POST 都收。SDK 未传入时会生成 uuid v4。24 小时内同键同正文重放第一次的回答<br>最短 1，最长 255 |

**请求正文（必填）**（`application/json`）

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `src` | string | 是 | 来源对象的键 |
| `dst` | string | 是 | 目标对象的键。已有同名对象会被覆盖 |

示例：

```json
{
  "src": "apps/crm/exports/2026-10.csv",
  "dst": "apps/crm/archive/2026-10.csv"
}
```

**响应**

| 状态 | 说明 |
|---|---|
| 204 | 已复制。没有正文 |
| 401 | 见 [Unauthorized](#resp-unauthorized) |
| 403 | 见 [Forbidden](#resp-forbidden) |
| 404 | `not_found`：对应的记录不存在（客户中心没有这个客户，访客、服务或所指的东西不存在）。见 [NotFound](#resp-notfound) |
| 413 | `payload_too_large`：正文太大。见 [PayloadTooLarge](#resp-payloadtoolarge) |
| 422 | 字段或参数不被接受（正文读不出来、有未知字段、缺必填、取值越界）。见 [Unprocessable](#resp-unprocessable) |
| 429 | 见 [RateLimited](#resp-ratelimited) |
| 500 | 见 [Internal](#resp-internal) |
| 501 | `capability_off`：这个部署（或这个宿主）不提供这项能力，正文的 `capability` 是它的范围。见 [CapabilityOff](#resp-capabilityoff) |
| 503 | `unavailable`：暂时不可用（这个部署不提供的能力是 501 `capability_off`）。见 [Unavailable](#resp-unavailable) |

### `POST /blobs/delete-prefix`

删除本应用文件夹里某个前缀下的全部对象

需要范围 `app.blobs:write`。English: deletes every object below `prefix` of the app's folder.

`prefix` 可以就是整个文件夹 `apps/{本应用 id}/`（删光本应用的文件）。成功回答 204，没有正文。

- 认证：`bearer`
- 范围：`app.blobs:write`
- operationId：`deleteBlobPrefix`

**参数**

| 名称 | 位置 | 类型 | 必填 | 说明 |
|---|---|---|---|---|
| `Idempotency-Key` | header | string |  | 每个 POST 都收。SDK 未传入时会生成 uuid v4。24 小时内同键同正文重放第一次的回答<br>最短 1，最长 255 |

**请求正文（必填）**（`application/json`）

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `prefix` | string | 是 | 前缀，以 apps/{本应用 id}/ 开头；可以就是整个文件夹 |

示例：

```json
{
  "prefix": "apps/crm/tmp/"
}
```

**响应**

| 状态 | 说明 |
|---|---|
| 204 | 已删除。没有正文 |
| 401 | 见 [Unauthorized](#resp-unauthorized) |
| 403 | 见 [Forbidden](#resp-forbidden) |
| 413 | `payload_too_large`：正文太大。见 [PayloadTooLarge](#resp-payloadtoolarge) |
| 422 | 字段或参数不被接受（正文读不出来、有未知字段、缺必填、取值越界）。见 [Unprocessable](#resp-unprocessable) |
| 429 | 见 [RateLimited](#resp-ratelimited) |
| 500 | 见 [Internal](#resp-internal) |
| 501 | `capability_off`：这个部署（或这个宿主）不提供这项能力，正文的 `capability` 是它的范围。见 [CapabilityOff](#resp-capabilityoff) |
| 503 | `unavailable`：暂时不可用（这个部署不提供的能力是 501 `capability_off`）。见 [Unavailable](#resp-unavailable) |

### `POST /blobs/presign`

为一个对象拿一个不带令牌的上传或下载地址

需要范围 `app.blobs:read`；`method` 为 `PUT`（上传）时还需要 `app.blobs:write`，缺了是 403。English: a URL to PUT (upload) or GET (download) one object of the app's folder, usable without a token until `expiresAt`.

字节直接走到文件存储，不经平台，所以没有 512 MiB 的限制；企业的存储额度仍然有效。`ttlSeconds` 省略时取最大值：上传 900 秒、下载 300 秒，超过是 422。上传必须给 `size`（确切字节数，≤ 512 MiB）：大小签进地址，长度不符的上传被存储拒绝；放不进企业剩余额度的在签发前就是 413。下载地址要求对象已存在，不存在是 404 `not_found`。

- 认证：`bearer`
- 范围：`app.blobs:read`
- operationId：`presignBlob`

**参数**

| 名称 | 位置 | 类型 | 必填 | 说明 |
|---|---|---|---|---|
| `Idempotency-Key` | header | string |  | 每个 POST 都收。SDK 未传入时会生成 uuid v4。24 小时内同键同正文重放第一次的回答<br>最短 1，最长 255 |

**请求正文（必填）**（`application/json`）

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `key` | string | 是 | 对象的键 |
| `method` | string | 是 | PUT 上传（还需要 app.blobs:write），GET 下载<br>可选值：`PUT`、`GET` |
| `ttlSeconds` | integer |  | 地址的有效秒数。省略（或 0）取最大值：上传（PUT）900、下载（GET）300<br>最小 0，最大 900 |
| `size` | integer(int64) |  | 上传（PUT）必填：上传的确切字节数，最多 512 MiB。大小签进地址，存储只收这么多字节；签发前按企业剩余的存储额度检查，放不下是 413。下载不带<br>最小 0，最大 536870912 |

示例：

```json
{
  "key": "apps/crm/imports/2026-10.csv",
  "method": "PUT",
  "ttlSeconds": 900,
  "size": 48213
}
```

**响应**

| 状态 | 说明 |
|---|---|
| 200 | 地址 |
| 401 | 见 [Unauthorized](#resp-unauthorized) |
| 403 | 见 [Forbidden](#resp-forbidden) |
| 404 | `not_found`：对应的记录不存在（客户中心没有这个客户，访客、服务或所指的东西不存在）。见 [NotFound](#resp-notfound) |
| 413 | `payload_too_large`：正文太大。见 [PayloadTooLarge](#resp-payloadtoolarge) |
| 422 | 字段或参数不被接受（正文读不出来、有未知字段、缺必填、取值越界）。见 [Unprocessable](#resp-unprocessable) |
| 429 | 见 [RateLimited](#resp-ratelimited) |
| 500 | 见 [Internal](#resp-internal) |
| 501 | `capability_off`：这个部署（或这个宿主）不提供这项能力，正文的 `capability` 是它的范围。见 [CapabilityOff](#resp-capabilityoff) |
| 503 | `unavailable`：暂时不可用（这个部署不提供的能力是 501 `capability_off`）。见 [Unavailable](#resp-unavailable) |

**200 响应正文**（`application/json`）

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `url` | string | 是 | 直接对文件存储用的地址 |
| `method` | string | 是 | 用这个方法去请求它：PUT 或 GET |
| `expiresAt` | string(date-time) | 是 | 地址失效的时间 |

示例：

```json
{
  "url": "https://files.example.com/apps/crm/exports/2026-10.csv?X-Signature=abc123",
  "method": "GET",
  "expiresAt": "2026-10-31T08:10:00Z"
}
```

## notify

以本应用的名义给企业成员发站内通知

### `POST /notify`

以本应用的名义给企业成员发一条站内通知

需要范围 `app.notify:send`。English: tells people of the tenant something in the platform's message centre, as the app (its name, a link into it); best effort.

`users` 是企业成员的用户 id（空白项忽略，去掉后至少 1 个，最多 500 个），`title` 必填，`body` 是正文，`link` 只能是应用内的路由（以 `/` 开头）、`k3://` 引用，或本应用清单 `egress` 里声明的域名下的 `https://` 地址，其余 422。`class` 说明通知的性质，供推送到平台以外的渠道使用：`approval.task`、`approval.result`、`mention`，省略由渠道决定。只发给本企业的在职成员：其余的人数计入 `rejected`，不说明原因。同一个调用方（页面会话的人或应用本身）十分钟内最多通知 1000 人次，超过是 429 并带 `Retry-After`。回答 202，`accepted` 是收到通知的人数。

- 认证：`bearer`
- 范围：`app.notify:send`
- operationId：`sendNotice`

**参数**

| 名称 | 位置 | 类型 | 必填 | 说明 |
|---|---|---|---|---|
| `Idempotency-Key` | header | string |  | 每个 POST 都收。SDK 未传入时会生成 uuid v4。24 小时内同键同正文重放第一次的回答<br>最短 1，最长 255 |

**请求正文（必填）**（`application/json`）

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `users` | string[] | 是 | 企业成员的用户 id，最多 500 个 |
| `title` | string | 是 | 标题。必填 |
| `body` | string |  | 正文 |
| `link` | string |  | 点开后去哪：应用内的路由、https:// 或 k3:// 地址 |
| `class` | string |  | 通知的性质，供推送到平台以外的渠道用。省略由渠道决定<br>可选值：`approval.task`、`approval.result`、`mention` |

示例：

```json
{
  "users": [
    "u_1001",
    "u_1002"
  ],
  "title": "有 3 张工单等你处理",
  "body": "客户“李明”的报修已分配给你",
  "link": "/tickets?status=open",
  "class": "approval.task"
}
```

**响应**

| 状态 | 说明 |
|---|---|
| 202 | 已交给消息中心 |
| 401 | 见 [Unauthorized](#resp-unauthorized) |
| 403 | 见 [Forbidden](#resp-forbidden) |
| 413 | `payload_too_large`：正文太大。见 [PayloadTooLarge](#resp-payloadtoolarge) |
| 422 | 字段或参数不被接受（正文读不出来、有未知字段、缺必填、取值越界）。见 [Unprocessable](#resp-unprocessable) |
| 429 | 见 [RateLimited](#resp-ratelimited) |
| 500 | 见 [Internal](#resp-internal) |
| 501 | `capability_off`：这个部署（或这个宿主）不提供这项能力，正文的 `capability` 是它的范围。见 [CapabilityOff](#resp-capabilityoff) |
| 503 | `unavailable`：暂时不可用（这个部署不提供的能力是 501 `capability_off`）。见 [Unavailable](#resp-unavailable) |

**202 响应正文**（`application/json`）

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `accepted` | integer | 是 | 收到通知的人数 |
| `rejected` | integer | 是 | 没有发出的人数（不是本企业的在职成员） |

示例：

```json
{
  "accepted": 2,
  "rejected": 0
}
```

## drafts

草稿：为成员起草变更由本人确认，以及当前人员审阅、修改、放弃、应用涉及本应用的草稿项，和记录的来源

### `GET /drafts`

列出当前人员涉及本应用的草稿

需要范围 `app.drafts:read`。English: the person's drafts that have an item of this app.

只收页面会话：这里的“人”是令牌里的人，应用自己的令牌是 403 `forbidden`。返回 `{drafts}`，最多 50 条；每条里本应用的项是完整的，其他应用的项只有数量。

- 认证：`bearer`
- 范围：`app.drafts:read`
- operationId：`listDrafts`

**参数**

| 名称 | 位置 | 类型 | 必填 | 说明 |
|---|---|---|---|---|
| `status` | query | string |  | 草稿状态：省略是 open 与 partial，all 是全部 |
| `target` | query | string |  | 只要作用于这个 k3:// 引用的草稿 |
| `session` | query | string |  | 只要这次对话里起草的草稿 |

**响应**

| 状态 | 说明 |
|---|---|
| 200 | 草稿 |
| 400 | 草稿服务的拒绝，状态由它定（400、403、404、409、422 等）。见 [DraftRefused](#resp-draftrefused) |
| 401 | 见 [Unauthorized](#resp-unauthorized) |
| 403 | 见 [Forbidden](#resp-forbidden) |
| 429 | 见 [RateLimited](#resp-ratelimited) |
| 500 | 见 [Internal](#resp-internal) |
| 501 | `capability_off`：这个部署（或这个宿主）不提供这项能力，正文的 `capability` 是它的范围。见 [CapabilityOff](#resp-capabilityoff) |
| 503 | `unavailable`：暂时不可用（这个部署不提供的能力是 501 `capability_off`）。见 [Unavailable](#resp-unavailable) |

**200 响应正文**（`application/json`）

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `drafts` | DraftView[] | 是 |  |
| `drafts[].id` | string | 是 |  |
| `drafts[].tenant` | string |  |  |
| `drafts[].owner` | string | 是 | 草稿属于的人；只有他们能应用 |
| `drafts[].sessionId` | string |  |  |
| `drafts[].origin` | string |  | 来源。普通草稿为空 |
| `drafts[].summary` | string |  |  |
| `drafts[].status` | string | 是 | 可选值：`open`、`applied`、`discarded`、`partial` |
| `drafts[].items` | DraftItem[] | 是 |  |
| `drafts[].items[].id` | string | 是 |  |
| `drafts[].items[].app` | string | 是 |  |
| `drafts[].items[].op` | string | 是 |  |
| `drafts[].items[].target` | string |  | 要改或删的记录的 k3:// 引用 |
| `drafts[].items[].revision` | integer | 是 | 每次改动加一；应用时必须与本人看到的一致 |
| `drafts[].items[].state` | string |  | 状态。省略是 pending<br>可选值：`pending`、`applied`、`abandoned` |
| `drafts[].items[].group` | string |  |  |
| `drafts[].items[].reason` | string |  |  |
| `drafts[].items[].fields` | map[string, DraftField] | 是 | 按字段名的值与提议 |
| `drafts[].items[].dependsOn` | string[] |  |  |
| `drafts[].items[].baseVersion` | string |  | 起草时应用报告的版本；之后预演报告了别的就是过期 |
| `drafts[].items[].dryRun` | DraftExecuteResult |  | 一次预演或执行的结果 |
| `drafts[].items[].dryRun.applied` | boolean |  | 已生效 |
| `drafts[].items[].dryRun.ref` | string |  | 受影响记录的 k3:// 引用 |
| `drafts[].items[].dryRun.summary` | string |  | 一句话说明这个操作做了什么 |
| `drafts[].items[].dryRun.mark` | string |  | 检查结论：ready、warning、choose、invalid |
| `drafts[].items[].dryRun.issues` | object[] |  | 发现的问题 |
| `drafts[].items[].result` | DraftExecuteResult |  | 一次预演或执行的结果 |
| `drafts[].items[].result.applied` | boolean |  | 已生效 |
| `drafts[].items[].result.ref` | string |  | 受影响记录的 k3:// 引用 |
| `drafts[].items[].result.summary` | string |  | 一句话说明这个操作做了什么 |
| `drafts[].items[].result.mark` | string |  | 检查结论：ready、warning、choose、invalid |
| `drafts[].items[].result.issues` | object[] |  | 发现的问题 |
| `drafts[].items[].mark` | string |  | 现算的检查结论 |
| `drafts[].items[].applicable` | boolean |  | 现算的：能否应用 |
| `drafts[].items[].turns` | string[] |  | 提议过这一项的轮次，旧的在前 |
| `drafts[].createdAt` | string(date-time) | 是 |  |
| `drafts[].updatedAt` | string(date-time) | 是 |  |
| `drafts[].others` | DraftOtherApp[] |  | 其他应用的项的数量 |
| `drafts[].others[].app` | string | 是 |  |
| `drafts[].others[].pending` | integer | 是 |  |
| `drafts[].others[].applied` | integer | 是 |  |
| `drafts[].ops` | map[string, object] |  | 返回的项用到的操作的声明，键是“应用.操作”：标题、输入模式、风险和本人要做的事 |
| `drafts[].merged` | DraftItemRef[] |  | 并入了已有待办项的请求项，以及它们现在在哪 |
| `drafts[].merged[].draftId` | string | 是 |  |
| `drafts[].merged[].itemId` | string | 是 |  |
| `drafts[].merged[].key` | string |  | 请求里给它的 key |

### `POST /drafts`

为一位成员起草一批变更，由本人确认后才生效

需要范围 `app.drafts:write`。English: proposes changes with the app's own ops for a person to confirm, as the agent does.

正文是草稿服务自己的创建正文（`sessionId`、`turn`、`summary`、`items`）外加 `for`，即草稿属于的人。草稿是这个人的，由本应用代写（委托，作者是 `app:{应用 id}`）：本人在工作台或本应用的页面里审阅并应用；没有他们预先签下的策略，什么都不会自动生效。每个 `items[].app` 只能是本应用（省略时补上），否则 422。应用自己的令牌必须给 `for`（企业的在职成员；不是的一律 403 `forbidden`，不说明这个人是否存在）；页面会话的 `for` 省略或等于本人，给别人是 422。本应用十分钟内给同一个人最多起草 30 份，超过是 429 并带 `Retry-After`。应用凭证不因此获得这个人的任何权限：草稿只给本人审阅，委托方不能应用。草稿有待办项时，平台会通知本人“有 N 项变更等你确认”。返回 201 和草稿视图。

- 认证：`bearer`
- 范围：`app.drafts:write`
- operationId：`createDraft`

**参数**

| 名称 | 位置 | 类型 | 必填 | 说明 |
|---|---|---|---|---|
| `Idempotency-Key` | header | string |  | 每个 POST 都收。SDK 未传入时会生成 uuid v4。24 小时内同键同正文重放第一次的回答<br>最短 1，最长 255 |

**请求正文（必填）**（`application/json`）

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `for` | string |  | 草稿属于的人（企业成员的用户 id）。应用自己的令牌必须给；页面会话省略或等于本人 |
| `sessionId` | string |  | 起草所在的对话 |
| `turn` | string |  | 提议所在的那一轮，随每个提议的值保存 |
| `summary` | string |  | 一句话：草稿做什么、为什么 |
| `items` | DraftItemInput[] | 是 | 提议的变更 |
| `items[].key` | string |  | 这项在本次请求里的名字，供其他项依赖 |
| `items[].app` | string |  | 操作所属的应用。省略时是本应用，写了就必须是本应用 |
| `items[].op` | string | 是 | 操作名，本应用在清单里声明的 op |
| `items[].target` | string |  | 要改或删的记录的 k3:// 引用；新建时省略 |
| `items[].group` | string |  | 起草者给这项的意图标签 |
| `items[].reason` | string |  | 为什么这样改 |
| `items[].dependsOn` | string[] |  | 本次请求里其他项的 key |
| `items[].proposed` | object | 是 | 提议的字段值，按操作的输入模式写 |
| `items[].candidates` | map[string, DraftCandidate[]] |  | 按字段给出的候选值，让本人挑 |

示例：

```json
{
  "for": "u_1001",
  "summary": "把李明的会员有效期延长到 2027-03-31",
  "items": [
    {
      "key": "extend",
      "op": "member.update",
      "target": "k3://members/member/m_42",
      "reason": "客户投诉后补偿",
      "proposed": {
        "validUntil": "2027-03-31"
      }
    }
  ]
}
```

**响应**

| 状态 | 说明 |
|---|---|
| 201 | 草稿 |
| 400 | 草稿服务的拒绝，状态由它定（400、403、404、409、422 等）。见 [DraftRefused](#resp-draftrefused) |
| 401 | 见 [Unauthorized](#resp-unauthorized) |
| 403 | 见 [Forbidden](#resp-forbidden) |
| 413 | `payload_too_large`：正文太大。见 [PayloadTooLarge](#resp-payloadtoolarge) |
| 422 | 字段或参数不被接受（正文读不出来、有未知字段、缺必填、取值越界）。见 [Unprocessable](#resp-unprocessable) |
| 429 | 见 [RateLimited](#resp-ratelimited) |
| 500 | 见 [Internal](#resp-internal) |
| 501 | `capability_off`：这个部署（或这个宿主）不提供这项能力，正文的 `capability` 是它的范围。见 [CapabilityOff](#resp-capabilityoff) |
| 503 | `unavailable`：暂时不可用（这个部署不提供的能力是 501 `capability_off`）。见 [Unavailable](#resp-unavailable) |

**201 响应正文**（`application/json`）

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `id` | string | 是 |  |
| `tenant` | string |  |  |
| `owner` | string | 是 | 草稿属于的人；只有他们能应用 |
| `sessionId` | string |  |  |
| `origin` | string |  | 来源。普通草稿为空 |
| `summary` | string |  |  |
| `status` | string | 是 | 可选值：`open`、`applied`、`discarded`、`partial` |
| `items` | DraftItem[] | 是 |  |
| `items[].id` | string | 是 |  |
| `items[].app` | string | 是 |  |
| `items[].op` | string | 是 |  |
| `items[].target` | string |  | 要改或删的记录的 k3:// 引用 |
| `items[].revision` | integer | 是 | 每次改动加一；应用时必须与本人看到的一致 |
| `items[].state` | string |  | 状态。省略是 pending<br>可选值：`pending`、`applied`、`abandoned` |
| `items[].group` | string |  |  |
| `items[].reason` | string |  |  |
| `items[].fields` | map[string, DraftField] | 是 | 按字段名的值与提议 |
| `items[].dependsOn` | string[] |  |  |
| `items[].baseVersion` | string |  | 起草时应用报告的版本；之后预演报告了别的就是过期 |
| `items[].dryRun` | DraftExecuteResult |  | 一次预演或执行的结果 |
| `items[].dryRun.applied` | boolean |  | 已生效 |
| `items[].dryRun.ref` | string |  | 受影响记录的 k3:// 引用 |
| `items[].dryRun.summary` | string |  | 一句话说明这个操作做了什么 |
| `items[].dryRun.mark` | string |  | 检查结论：ready、warning、choose、invalid |
| `items[].dryRun.issues` | object[] |  | 发现的问题 |
| `items[].result` | DraftExecuteResult |  | 一次预演或执行的结果 |
| `items[].result.applied` | boolean |  | 已生效 |
| `items[].result.ref` | string |  | 受影响记录的 k3:// 引用 |
| `items[].result.summary` | string |  | 一句话说明这个操作做了什么 |
| `items[].result.mark` | string |  | 检查结论：ready、warning、choose、invalid |
| `items[].result.issues` | object[] |  | 发现的问题 |
| `items[].mark` | string |  | 现算的检查结论 |
| `items[].applicable` | boolean |  | 现算的：能否应用 |
| `items[].turns` | string[] |  | 提议过这一项的轮次，旧的在前 |
| `createdAt` | string(date-time) | 是 |  |
| `updatedAt` | string(date-time) | 是 |  |
| `others` | DraftOtherApp[] |  | 其他应用的项的数量 |
| `others[].app` | string | 是 |  |
| `others[].pending` | integer | 是 |  |
| `others[].applied` | integer | 是 |  |
| `ops` | map[string, object] |  | 返回的项用到的操作的声明，键是“应用.操作”：标题、输入模式、风险和本人要做的事 |
| `merged` | DraftItemRef[] |  | 并入了已有待办项的请求项，以及它们现在在哪 |
| `merged[].draftId` | string | 是 |  |
| `merged[].itemId` | string | 是 |  |
| `merged[].key` | string |  | 请求里给它的 key |

示例：

```json
{
  "id": "d_01HZX",
  "owner": "u_1001",
  "summary": "把李明的会员有效期延长到 2027-03-31",
  "status": "open",
  "items": [
    {
      "id": "i_1",
      "app": "crm",
      "op": "member.update",
      "target": "k3://members/member/m_42",
      "revision": 1,
      "state": "pending",
      "fields": {},
      "mark": "ready",
      "applicable": true
    }
  ],
  "createdAt": "2026-10-31T08:00:00Z",
  "updatedAt": "2026-10-31T08:00:00Z"
}
```

### `GET /drafts/{id}`

读取一份草稿：本应用的项是完整的

需要范围 `app.drafts:read`。English: one draft with this app's items in full (the review page).

只收页面会话：这里的“人”是令牌里的人，应用自己的令牌是 403 `forbidden`。`revalidate=1` 先把检查重跑一遍再回答。

- 认证：`bearer`
- 范围：`app.drafts:read`
- operationId：`getDraft`

**参数**

| 名称 | 位置 | 类型 | 必填 | 说明 |
|---|---|---|---|---|
| `id` | path | string | 是 | 草稿 id |
| `revalidate` | query | string |  | 传 1 时先重跑检查<br>可选值：`1` |

**响应**

| 状态 | 说明 |
|---|---|
| 200 | 草稿 |
| 400 | 草稿服务的拒绝，状态由它定（400、403、404、409、422 等）。见 [DraftRefused](#resp-draftrefused) |
| 401 | 见 [Unauthorized](#resp-unauthorized) |
| 403 | 见 [Forbidden](#resp-forbidden) |
| 429 | 见 [RateLimited](#resp-ratelimited) |
| 500 | 见 [Internal](#resp-internal) |
| 501 | `capability_off`：这个部署（或这个宿主）不提供这项能力，正文的 `capability` 是它的范围。见 [CapabilityOff](#resp-capabilityoff) |
| 503 | `unavailable`：暂时不可用（这个部署不提供的能力是 501 `capability_off`）。见 [Unavailable](#resp-unavailable) |

**200 响应正文**（`application/json`）

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `id` | string | 是 |  |
| `tenant` | string |  |  |
| `owner` | string | 是 | 草稿属于的人；只有他们能应用 |
| `sessionId` | string |  |  |
| `origin` | string |  | 来源。普通草稿为空 |
| `summary` | string |  |  |
| `status` | string | 是 | 可选值：`open`、`applied`、`discarded`、`partial` |
| `items` | DraftItem[] | 是 |  |
| `items[].id` | string | 是 |  |
| `items[].app` | string | 是 |  |
| `items[].op` | string | 是 |  |
| `items[].target` | string |  | 要改或删的记录的 k3:// 引用 |
| `items[].revision` | integer | 是 | 每次改动加一；应用时必须与本人看到的一致 |
| `items[].state` | string |  | 状态。省略是 pending<br>可选值：`pending`、`applied`、`abandoned` |
| `items[].group` | string |  |  |
| `items[].reason` | string |  |  |
| `items[].fields` | map[string, DraftField] | 是 | 按字段名的值与提议 |
| `items[].dependsOn` | string[] |  |  |
| `items[].baseVersion` | string |  | 起草时应用报告的版本；之后预演报告了别的就是过期 |
| `items[].dryRun` | DraftExecuteResult |  | 一次预演或执行的结果 |
| `items[].dryRun.applied` | boolean |  | 已生效 |
| `items[].dryRun.ref` | string |  | 受影响记录的 k3:// 引用 |
| `items[].dryRun.summary` | string |  | 一句话说明这个操作做了什么 |
| `items[].dryRun.mark` | string |  | 检查结论：ready、warning、choose、invalid |
| `items[].dryRun.issues` | object[] |  | 发现的问题 |
| `items[].result` | DraftExecuteResult |  | 一次预演或执行的结果 |
| `items[].result.applied` | boolean |  | 已生效 |
| `items[].result.ref` | string |  | 受影响记录的 k3:// 引用 |
| `items[].result.summary` | string |  | 一句话说明这个操作做了什么 |
| `items[].result.mark` | string |  | 检查结论：ready、warning、choose、invalid |
| `items[].result.issues` | object[] |  | 发现的问题 |
| `items[].mark` | string |  | 现算的检查结论 |
| `items[].applicable` | boolean |  | 现算的：能否应用 |
| `items[].turns` | string[] |  | 提议过这一项的轮次，旧的在前 |
| `createdAt` | string(date-time) | 是 |  |
| `updatedAt` | string(date-time) | 是 |  |
| `others` | DraftOtherApp[] |  | 其他应用的项的数量 |
| `others[].app` | string | 是 |  |
| `others[].pending` | integer | 是 |  |
| `others[].applied` | integer | 是 |  |
| `ops` | map[string, object] |  | 返回的项用到的操作的声明，键是“应用.操作”：标题、输入模式、风险和本人要做的事 |
| `merged` | DraftItemRef[] |  | 并入了已有待办项的请求项，以及它们现在在哪 |
| `merged[].draftId` | string | 是 |  |
| `merged[].itemId` | string | 是 |  |
| `merged[].key` | string |  | 请求里给它的 key |

示例：

```json
{
  "id": "d_01HZX",
  "owner": "u_1001",
  "summary": "把李明的会员有效期延长到 2027-03-31",
  "status": "open",
  "items": [
    {
      "id": "i_1",
      "app": "crm",
      "op": "member.update",
      "target": "k3://members/member/m_42",
      "revision": 1,
      "state": "pending",
      "fields": {},
      "mark": "ready",
      "applicable": true
    }
  ],
  "createdAt": "2026-10-31T08:00:00Z",
  "updatedAt": "2026-10-31T08:00:00Z"
}
```

### `PATCH /drafts/{id}/items/{item}`

记下本人对一个草稿项的值和选择

需要范围 `app.drafts:review`。English: the person's values and choices for one item.

只收页面会话：这里的“人”是令牌里的人，应用自己的令牌是 403 `forbidden`。`revision` 是本人看到的版本；`fields` 是他们填的值；`pick` 按字段说选哪个值（`base` 保持原样，某一轮则取那一轮的提议，空串取最新）。返回 `{item}`。

- 认证：`bearer`
- 范围：`app.drafts:review`
- operationId：`reviewDraftItem`

**参数**

| 名称 | 位置 | 类型 | 必填 | 说明 |
|---|---|---|---|---|
| `id` | path | string | 是 | 草稿 id |
| `item` | path | string | 是 | 草稿项 id |

**请求正文（必填）**（`application/json`）

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `revision` | integer | 是 | 本人看到的版本 |
| `fields` | object |  | 本人填的值，按字段名 |
| `pick` | map[string, string] |  | 按字段选哪个值：base 保持原样，某一轮则取那一轮的提议，空串取最新 |

示例：

```json
{
  "revision": 1,
  "fields": {
    "validUntil": "2027-06-30"
  }
}
```

**响应**

| 状态 | 说明 |
|---|---|
| 200 | 更新后的草稿项 |
| 400 | 草稿服务的拒绝，状态由它定（400、403、404、409、422 等）。见 [DraftRefused](#resp-draftrefused) |
| 401 | 见 [Unauthorized](#resp-unauthorized) |
| 403 | 见 [Forbidden](#resp-forbidden) |
| 413 | `payload_too_large`：正文太大。见 [PayloadTooLarge](#resp-payloadtoolarge) |
| 429 | 见 [RateLimited](#resp-ratelimited) |
| 500 | 见 [Internal](#resp-internal) |
| 501 | `capability_off`：这个部署（或这个宿主）不提供这项能力，正文的 `capability` 是它的范围。见 [CapabilityOff](#resp-capabilityoff) |
| 503 | `unavailable`：暂时不可用（这个部署不提供的能力是 501 `capability_off`）。见 [Unavailable](#resp-unavailable) |

**200 响应正文**（`application/json`）

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `item` | DraftItem | 是 | 草稿里的一项：一个应用的一个操作 |
| `item.id` | string | 是 |  |
| `item.app` | string | 是 |  |
| `item.op` | string | 是 |  |
| `item.target` | string |  | 要改或删的记录的 k3:// 引用 |
| `item.revision` | integer | 是 | 每次改动加一；应用时必须与本人看到的一致 |
| `item.state` | string |  | 状态。省略是 pending<br>可选值：`pending`、`applied`、`abandoned` |
| `item.group` | string |  |  |
| `item.reason` | string |  |  |
| `item.fields` | map[string, DraftField] | 是 | 按字段名的值与提议 |
| `item.dependsOn` | string[] |  |  |
| `item.baseVersion` | string |  | 起草时应用报告的版本；之后预演报告了别的就是过期 |
| `item.dryRun` | DraftExecuteResult |  | 一次预演或执行的结果 |
| `item.dryRun.applied` | boolean |  | 已生效 |
| `item.dryRun.ref` | string |  | 受影响记录的 k3:// 引用 |
| `item.dryRun.summary` | string |  | 一句话说明这个操作做了什么 |
| `item.dryRun.mark` | string |  | 检查结论：ready、warning、choose、invalid |
| `item.dryRun.issues` | object[] |  | 发现的问题 |
| `item.result` | DraftExecuteResult |  | 一次预演或执行的结果 |
| `item.result.applied` | boolean |  | 已生效 |
| `item.result.ref` | string |  | 受影响记录的 k3:// 引用 |
| `item.result.summary` | string |  | 一句话说明这个操作做了什么 |
| `item.result.mark` | string |  | 检查结论：ready、warning、choose、invalid |
| `item.result.issues` | object[] |  | 发现的问题 |
| `item.mark` | string |  | 现算的检查结论 |
| `item.applicable` | boolean |  | 现算的：能否应用 |
| `item.turns` | string[] |  | 提议过这一项的轮次，旧的在前 |

### `POST /drafts/{id}/items/{item}/abandon`

放弃一个草稿项

需要范围 `app.drafts:review`。English: drops one item.

只收页面会话：这里的“人”是令牌里的人，应用自己的令牌是 403 `forbidden`。返回 `{ok: true}`。

- 认证：`bearer`
- 范围：`app.drafts:review`
- operationId：`abandonDraftItem`

**参数**

| 名称 | 位置 | 类型 | 必填 | 说明 |
|---|---|---|---|---|
| `id` | path | string | 是 | 草稿 id |
| `item` | path | string | 是 | 草稿项 id |
| `Idempotency-Key` | header | string |  | 每个 POST 都收。SDK 未传入时会生成 uuid v4。24 小时内同键同正文重放第一次的回答<br>最短 1，最长 255 |

**响应**

| 状态 | 说明 |
|---|---|
| 200 | 已放弃 |
| 400 | 草稿服务的拒绝，状态由它定（400、403、404、409、422 等）。见 [DraftRefused](#resp-draftrefused) |
| 401 | 见 [Unauthorized](#resp-unauthorized) |
| 403 | 见 [Forbidden](#resp-forbidden) |
| 429 | 见 [RateLimited](#resp-ratelimited) |
| 500 | 见 [Internal](#resp-internal) |
| 501 | `capability_off`：这个部署（或这个宿主）不提供这项能力，正文的 `capability` 是它的范围。见 [CapabilityOff](#resp-capabilityoff) |
| 503 | `unavailable`：暂时不可用（这个部署不提供的能力是 501 `capability_off`）。见 [Unavailable](#resp-unavailable) |

**200 响应正文**（`application/json`）

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `ok` | boolean | 是 |  |

示例：

```json
{
  "ok": true
}
```

### `POST /drafts/{id}/items/{item}/rebase`

以记录现在的样子为基础继续这个草稿项

需要范围 `app.drafts:review`。English: goes on from the record as it is now.

只收页面会话：这里的“人”是令牌里的人，应用自己的令牌是 403 `forbidden`。返回 `{item}`。

- 认证：`bearer`
- 范围：`app.drafts:review`
- operationId：`rebaseDraftItem`

**参数**

| 名称 | 位置 | 类型 | 必填 | 说明 |
|---|---|---|---|---|
| `id` | path | string | 是 | 草稿 id |
| `item` | path | string | 是 | 草稿项 id |
| `Idempotency-Key` | header | string |  | 每个 POST 都收。SDK 未传入时会生成 uuid v4。24 小时内同键同正文重放第一次的回答<br>最短 1，最长 255 |

**响应**

| 状态 | 说明 |
|---|---|
| 200 | 重新定基后的草稿项 |
| 400 | 草稿服务的拒绝，状态由它定（400、403、404、409、422 等）。见 [DraftRefused](#resp-draftrefused) |
| 401 | 见 [Unauthorized](#resp-unauthorized) |
| 403 | 见 [Forbidden](#resp-forbidden) |
| 429 | 见 [RateLimited](#resp-ratelimited) |
| 500 | 见 [Internal](#resp-internal) |
| 501 | `capability_off`：这个部署（或这个宿主）不提供这项能力，正文的 `capability` 是它的范围。见 [CapabilityOff](#resp-capabilityoff) |
| 503 | `unavailable`：暂时不可用（这个部署不提供的能力是 501 `capability_off`）。见 [Unavailable](#resp-unavailable) |

**200 响应正文**（`application/json`）

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `item` | DraftItem | 是 | 草稿里的一项：一个应用的一个操作 |
| `item.id` | string | 是 |  |
| `item.app` | string | 是 |  |
| `item.op` | string | 是 |  |
| `item.target` | string |  | 要改或删的记录的 k3:// 引用 |
| `item.revision` | integer | 是 | 每次改动加一；应用时必须与本人看到的一致 |
| `item.state` | string |  | 状态。省略是 pending<br>可选值：`pending`、`applied`、`abandoned` |
| `item.group` | string |  |  |
| `item.reason` | string |  |  |
| `item.fields` | map[string, DraftField] | 是 | 按字段名的值与提议 |
| `item.dependsOn` | string[] |  |  |
| `item.baseVersion` | string |  | 起草时应用报告的版本；之后预演报告了别的就是过期 |
| `item.dryRun` | DraftExecuteResult |  | 一次预演或执行的结果 |
| `item.dryRun.applied` | boolean |  | 已生效 |
| `item.dryRun.ref` | string |  | 受影响记录的 k3:// 引用 |
| `item.dryRun.summary` | string |  | 一句话说明这个操作做了什么 |
| `item.dryRun.mark` | string |  | 检查结论：ready、warning、choose、invalid |
| `item.dryRun.issues` | object[] |  | 发现的问题 |
| `item.result` | DraftExecuteResult |  | 一次预演或执行的结果 |
| `item.result.applied` | boolean |  | 已生效 |
| `item.result.ref` | string |  | 受影响记录的 k3:// 引用 |
| `item.result.summary` | string |  | 一句话说明这个操作做了什么 |
| `item.result.mark` | string |  | 检查结论：ready、warning、choose、invalid |
| `item.result.issues` | object[] |  | 发现的问题 |
| `item.mark` | string |  | 现算的检查结论 |
| `item.applicable` | boolean |  | 现算的：能否应用 |
| `item.turns` | string[] |  | 提议过这一项的轮次，旧的在前 |

### `POST /drafts/{id}/apply`

应用本人审阅过的草稿项

需要范围 `app.drafts:review`。English: applies this app's items the person reviewed, with the pending items they depend on.

只收页面会话：这里的“人”是令牌里的人，应用自己的令牌是 403 `forbidden`。`items` 里每项是 `{id, revision, confirmed?}`，`revision` 必须是本人看到的版本。返回草稿的新状态和每一项的结果（`results` 按项 id 索引）。

- 认证：`bearer`
- 范围：`app.drafts:review`
- operationId：`applyDraft`

**参数**

| 名称 | 位置 | 类型 | 必填 | 说明 |
|---|---|---|---|---|
| `id` | path | string | 是 | 草稿 id |
| `Idempotency-Key` | header | string |  | 每个 POST 都收。SDK 未传入时会生成 uuid v4。24 小时内同键同正文重放第一次的回答<br>最短 1，最长 255 |

**请求正文（必填）**（`application/json`）

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `items` | DraftApplyItem[] | 是 |  |
| `items[].id` | string | 是 |  |
| `items[].revision` | integer | 是 |  |
| `items[].confirmed` | boolean |  | 本人确认了操作 intervention.confirm 里写的事 |

示例：

```json
{
  "items": [
    {
      "id": "i_1",
      "revision": 1,
      "confirmed": true
    }
  ]
}
```

**响应**

| 状态 | 说明 |
|---|---|
| 200 | 应用的结果 |
| 400 | 草稿服务的拒绝，状态由它定（400、403、404、409、422 等）。见 [DraftRefused](#resp-draftrefused) |
| 401 | 见 [Unauthorized](#resp-unauthorized) |
| 403 | 见 [Forbidden](#resp-forbidden) |
| 413 | `payload_too_large`：正文太大。见 [PayloadTooLarge](#resp-payloadtoolarge) |
| 429 | 见 [RateLimited](#resp-ratelimited) |
| 500 | 见 [Internal](#resp-internal) |
| 501 | `capability_off`：这个部署（或这个宿主）不提供这项能力，正文的 `capability` 是它的范围。见 [CapabilityOff](#resp-capabilityoff) |
| 503 | `unavailable`：暂时不可用（这个部署不提供的能力是 501 `capability_off`）。见 [Unavailable](#resp-unavailable) |

**200 响应正文**（`application/json`）

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `status` | string | 是 | 可选值：`open`、`applied`、`discarded`、`partial` |
| `results` | map[string, DraftExecuteResult] | 是 | 按项 id |

示例：

```json
{
  "status": "applied",
  "results": {
    "i_1": {
      "applied": true,
      "ref": "k3://members/member/m_42",
      "summary": "有效期延至 2027-03-31"
    }
  }
}
```

### `GET /provenance`

查一条记录是谁写的

需要范围 `app.drafts:read`。English: who wrote a record of this app: a draft the person applied, or a policy they pre-signed.

只收页面会话：这里的“人”是令牌里的人，应用自己的令牌是 403 `forbidden`。`ref` 必填，是记录的 k3:// 引用。返回 `{provenance}`。

- 认证：`bearer`
- 范围：`app.drafts:read`
- operationId：`getProvenance`

**参数**

| 名称 | 位置 | 类型 | 必填 | 说明 |
|---|---|---|---|---|
| `ref` | query | string | 是 | 记录的 k3:// 引用 |

**响应**

| 状态 | 说明 |
|---|---|
| 200 | 来源 |
| 400 | 草稿服务的拒绝，状态由它定（400、403、404、409、422 等）。见 [DraftRefused](#resp-draftrefused) |
| 401 | 见 [Unauthorized](#resp-unauthorized) |
| 403 | 见 [Forbidden](#resp-forbidden) |
| 429 | 见 [RateLimited](#resp-ratelimited) |
| 500 | 见 [Internal](#resp-internal) |
| 501 | `capability_off`：这个部署（或这个宿主）不提供这项能力，正文的 `capability` 是它的范围。见 [CapabilityOff](#resp-capabilityoff) |
| 503 | `unavailable`：暂时不可用（这个部署不提供的能力是 501 `capability_off`）。见 [Unavailable](#resp-unavailable) |

**200 响应正文**（`application/json`）

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `provenance` | DraftProvenance[] | 是 |  |
| `provenance[].ref` | string | 是 |  |
| `provenance[].at` | string(date-time) | 是 |  |
| `provenance[].app` | string | 是 |  |
| `provenance[].op` | string | 是 |  |
| `provenance[].via` | string | 是 | 本人应用了一份草稿，还是预先签了策略<br>可选值：`draft`、`policy` |
| `provenance[].draftId` | string | 是 |  |
| `provenance[].itemId` | string | 是 |  |
| `provenance[].policyId` | string |  |  |
| `provenance[].subject` | string | 是 | 本人的用户 id |
| `provenance[].name` | string |  |  |

示例：

```json
{
  "provenance": [
    {
      "ref": "k3://members/member/m_42",
      "at": "2026-10-31T08:05:00Z",
      "app": "crm",
      "op": "member.update",
      "via": "draft",
      "draftId": "d_01HZX",
      "itemId": "i_1",
      "subject": "u_1001",
      "name": "王芳"
    }
  ]
}
```

## outreach

触达：向客户排短信或公众号模板消息，读渠道、模板和消息记录（是否发出由平台在发送时判断）

### `POST /outreach/send`

向一位客户排一条触达消息

需要范围 `app.outreach:send`。English: queues a message to a customer by a template of the tenant's; the platform decides at send time whether it may go.

应用不发送，也看不到手机号：同意、退订、免打扰、频次和企业上限都由平台在发送时判断。`key` 是这条消息的幂等键，必填；同键再来不会加新消息，回 200 和当初那条的 `id`（`existing` 为 true）；新排上的回 202。`purpose` 是 `service`（服务通知）或 `marketing`（营销）。

- 认证：`bearer`
- 范围：`app.outreach:send`
- operationId：`sendOutreach`

**参数**

| 名称 | 位置 | 类型 | 必填 | 说明 |
|---|---|---|---|---|
| `Idempotency-Key` | header | string |  | 每个 POST 都收。SDK 未传入时会生成 uuid v4。24 小时内同键同正文重放第一次的回答<br>最短 1，最长 255 |

**请求正文（必填）**（`application/json`）

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `key` | string | 是 | 这条消息的幂等键。同键再来不会加新消息 |
| `customer` | string | 是 | 客户 id |
| `channel` | string | 是 | 渠道种类，例如 sms |
| `template` | string | 是 | 模板 id |
| `vars` | map[string, string] |  | 模板里的变量，按名字 |
| `purpose` | string | 是 | 服务通知，或营销<br>可选值：`service`、`marketing` |
| `triggerRef` | string |  | 是什么引起的，k3:// 引用，写进日志，不含个人信息 |

示例：

```json
{
  "key": "o:k3://order/order/o_1|c_88|sms",
  "customer": "c_88",
  "channel": "sms",
  "template": "tpl_order_ok",
  "vars": {
    "order": "A1024"
  },
  "purpose": "service",
  "triggerRef": "k3://order/order/o_1"
}
```

**响应**

| 状态 | 说明 |
|---|---|
| 202 | 已排上（同键再来是 200，existing 为 true） |
| 401 | 见 [Unauthorized](#resp-unauthorized) |
| 403 | 见 [Forbidden](#resp-forbidden) |
| 404 | `not_found`：对应的记录不存在（客户中心没有这个客户，访客、服务或所指的东西不存在）。见 [NotFound](#resp-notfound) |
| 413 | `payload_too_large`：正文太大。见 [PayloadTooLarge](#resp-payloadtoolarge) |
| 422 | 字段或参数不被接受（正文读不出来、有未知字段、缺必填、取值越界）。见 [Unprocessable](#resp-unprocessable) |
| 429 | 见 [RateLimited](#resp-ratelimited) |
| 500 | 见 [Internal](#resp-internal) |
| 501 | `capability_off`：这个部署（或这个宿主）不提供这项能力，正文的 `capability` 是它的范围。见 [CapabilityOff](#resp-capabilityoff) |
| 503 | `unavailable`：暂时不可用（这个部署不提供的能力是 501 `capability_off`）。见 [Unavailable](#resp-unavailable) |

**202 响应正文**（`application/json`）

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `id` | string | 是 | 消息 id |
| `existing` | boolean | 是 | 这个 key 以前排过：没有加新消息，id 是当初那条 |

示例：

```json
{
  "id": "msg_01HZY",
  "existing": false
}
```

### `GET /outreach/channels`

列出触达渠道及其是否就绪

需要范围 `app.outreach:read`。English: the tenant's outreach channels and whether each can send.

返回 `{items}`。渠道的密钥不会出现，只有 `secret.set` 和指纹。

- 认证：`bearer`
- 范围：`app.outreach:read`
- operationId：`listOutreachChannels`

**响应**

| 状态 | 说明 |
|---|---|
| 200 | 渠道 |
| 401 | 见 [Unauthorized](#resp-unauthorized) |
| 403 | 见 [Forbidden](#resp-forbidden) |
| 422 | 字段或参数不被接受（正文读不出来、有未知字段、缺必填、取值越界）。见 [Unprocessable](#resp-unprocessable) |
| 429 | 见 [RateLimited](#resp-ratelimited) |
| 500 | 见 [Internal](#resp-internal) |
| 501 | `capability_off`：这个部署（或这个宿主）不提供这项能力，正文的 `capability` 是它的范围。见 [CapabilityOff](#resp-capabilityoff) |
| 503 | `unavailable`：暂时不可用（这个部署不提供的能力是 501 `capability_off`）。见 [Unavailable](#resp-unavailable) |

**200 响应正文**（`application/json`）

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `items` | OutreachChannel[] | 是 |  |
| `items[].kind` | string | 是 | 渠道种类，例如 sms |
| `items[].name` | string | 是 |  |
| `items[].registered` | boolean | 是 | 平台这个进程能经它发送 |
| `items[].configured` | boolean | 是 |  |
| `items[].enabled` | boolean | 是 |  |
| `items[].ready` | boolean | 是 | 已配置、已打开（公众号还要已绑定） |
| `items[].provider` | string |  |  |
| `items[].signature` | string |  |  |
| `items[].keyId` | string |  |  |
| `items[].region` | string |  |  |
| `items[].appId` | string |  |  |
| `items[].secret` | OutreachSecretInfo | 是 |  |
| `items[].secret.set` | boolean | 是 | 已设置密钥 |
| `items[].secret.fingerprint` | string |  | 密钥的指纹 |
| `items[].callbackUrl` | string |  |  |
| `items[].replyUrl` | string |  |  |
| `items[].status` | string |  |  |
| `items[].note` | string |  |  |
| `items[].account` | string |  |  |
| `items[].lastTest` | object |  | 上一次测试发送的结果 |
| `items[].updatedBy` | string |  |  |
| `items[].updatedAt` | string(date-time) |  |  |
| `items[].capabilities` | object | 是 | 渠道的能力：needsTemplate、receipts、unsubReply、idempotent 等 |

示例：

```json
{
  "items": [
    {
      "kind": "sms",
      "name": "短信",
      "registered": true,
      "configured": true,
      "enabled": true,
      "ready": true,
      "provider": "aliyun",
      "signature": "示例企业",
      "secret": {
        "set": true,
        "fingerprint": "a1b2c3"
      },
      "capabilities": {
        "needsTemplate": true,
        "receipts": true
      }
    }
  ]
}
```

### `GET /outreach/templates`

列出消息模板

需要范围 `app.outreach:read`。English: the tenant's message templates.

`channel` 只要这个渠道的模板；`archived=1` 连归档的也要。返回 `{items}`。

- 认证：`bearer`
- 范围：`app.outreach:read`
- operationId：`listOutreachTemplates`

**参数**

| 名称 | 位置 | 类型 | 必填 | 说明 |
|---|---|---|---|---|
| `channel` | query | string |  | 渠道种类，例如 sms |
| `archived` | query | string |  | 传 1 时包含归档的模板 |

**响应**

| 状态 | 说明 |
|---|---|
| 200 | 模板 |
| 401 | 见 [Unauthorized](#resp-unauthorized) |
| 403 | 见 [Forbidden](#resp-forbidden) |
| 422 | 字段或参数不被接受（正文读不出来、有未知字段、缺必填、取值越界）。见 [Unprocessable](#resp-unprocessable) |
| 429 | 见 [RateLimited](#resp-ratelimited) |
| 500 | 见 [Internal](#resp-internal) |
| 501 | `capability_off`：这个部署（或这个宿主）不提供这项能力，正文的 `capability` 是它的范围。见 [CapabilityOff](#resp-capabilityoff) |
| 503 | `unavailable`：暂时不可用（这个部署不提供的能力是 501 `capability_off`）。见 [Unavailable](#resp-unavailable) |

**200 响应正文**（`application/json`）

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `items` | OutreachTemplate[] | 是 |  |
| `items[].id` | string | 是 |  |
| `items[].channel` | string | 是 |  |
| `items[].name` | string | 是 |  |
| `items[].purpose` | string | 是 | 可选值：`service`、`marketing` |
| `items[].body` | string | 是 | 正文，变量写成 {变量名} |
| `items[].fields` | OutreachTemplateField[] |  |  |
| `items[].fields[].name` | string | 是 |  |
| `items[].fields[].value` | string | 是 |  |
| `items[].paramNames` | string[] |  |  |
| `items[].channelTemplateId` | string |  | 渠道方（短信商、公众号）的模板编号 |
| `items[].url` | string |  |  |
| `items[].approvalState` | string | 是 | 审核状态，例如 unsubmitted、approved |
| `items[].approvalNote` | string |  |  |
| `items[].version` | integer | 是 |  |
| `items[].active` | boolean | 是 |  |
| `items[].variables` | string[] |  | 正文里用到的变量 |
| `items[].warnings` | string[] |  |  |
| `items[].used` | integer |  | 用它发过的消息数 |
| `items[].createdBy` | string |  |  |
| `items[].updatedBy` | string |  |  |
| `items[].createdAt` | string(date-time) |  |  |
| `items[].updatedAt` | string(date-time) |  |  |

### `GET /outreach/templates/{id}`

读取一个消息模板

需要范围 `app.outreach:read`。English: one message template.

不存在是 404 `not_found`。

- 认证：`bearer`
- 范围：`app.outreach:read`
- operationId：`getOutreachTemplate`

**参数**

| 名称 | 位置 | 类型 | 必填 | 说明 |
|---|---|---|---|---|
| `id` | path | string | 是 | 模板 id |

**响应**

| 状态 | 说明 |
|---|---|
| 200 | 模板 |
| 401 | 见 [Unauthorized](#resp-unauthorized) |
| 403 | 见 [Forbidden](#resp-forbidden) |
| 404 | `not_found`：对应的记录不存在（客户中心没有这个客户，访客、服务或所指的东西不存在）。见 [NotFound](#resp-notfound) |
| 422 | 字段或参数不被接受（正文读不出来、有未知字段、缺必填、取值越界）。见 [Unprocessable](#resp-unprocessable) |
| 429 | 见 [RateLimited](#resp-ratelimited) |
| 500 | 见 [Internal](#resp-internal) |
| 501 | `capability_off`：这个部署（或这个宿主）不提供这项能力，正文的 `capability` 是它的范围。见 [CapabilityOff](#resp-capabilityoff) |
| 503 | `unavailable`：暂时不可用（这个部署不提供的能力是 501 `capability_off`）。见 [Unavailable](#resp-unavailable) |

**200 响应正文**（`application/json`）

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `id` | string | 是 |  |
| `channel` | string | 是 |  |
| `name` | string | 是 |  |
| `purpose` | string | 是 | 可选值：`service`、`marketing` |
| `body` | string | 是 | 正文，变量写成 {变量名} |
| `fields` | OutreachTemplateField[] |  |  |
| `fields[].name` | string | 是 |  |
| `fields[].value` | string | 是 |  |
| `paramNames` | string[] |  |  |
| `channelTemplateId` | string |  | 渠道方（短信商、公众号）的模板编号 |
| `url` | string |  |  |
| `approvalState` | string | 是 | 审核状态，例如 unsubmitted、approved |
| `approvalNote` | string |  |  |
| `version` | integer | 是 |  |
| `active` | boolean | 是 |  |
| `variables` | string[] |  | 正文里用到的变量 |
| `warnings` | string[] |  |  |
| `used` | integer |  | 用它发过的消息数 |
| `createdBy` | string |  |  |
| `updatedBy` | string |  |  |
| `createdAt` | string(date-time) |  |  |
| `updatedAt` | string(date-time) |  |  |

示例：

```json
{
  "id": "tpl_order_ok",
  "channel": "sms",
  "name": "订单确认",
  "purpose": "service",
  "body": "您的订单{order}已确认",
  "channelTemplateId": "SMS_NOTICE",
  "approvalState": "approved",
  "version": 1,
  "active": true,
  "variables": [
    "order"
  ],
  "used": 128,
  "createdAt": "2026-09-01T08:00:00Z",
  "updatedAt": "2026-09-01T08:00:00Z"
}
```

### `GET /outreach/messages/{id}`

读取一条触达消息的状态

需要范围 `app.outreach:read`。English: one message of the outreach log, with its state and, when it was not sent, the reason.

不存在是 404 `not_found`。

- 认证：`bearer`
- 范围：`app.outreach:read`
- operationId：`getOutreachMessage`

**参数**

| 名称 | 位置 | 类型 | 必填 | 说明 |
|---|---|---|---|---|
| `id` | path | string | 是 | 消息 id（发送时回答的 id） |

**响应**

| 状态 | 说明 |
|---|---|
| 200 | 消息 |
| 401 | 见 [Unauthorized](#resp-unauthorized) |
| 403 | 见 [Forbidden](#resp-forbidden) |
| 404 | `not_found`：对应的记录不存在（客户中心没有这个客户，访客、服务或所指的东西不存在）。见 [NotFound](#resp-notfound) |
| 422 | 字段或参数不被接受（正文读不出来、有未知字段、缺必填、取值越界）。见 [Unprocessable](#resp-unprocessable) |
| 429 | 见 [RateLimited](#resp-ratelimited) |
| 500 | 见 [Internal](#resp-internal) |
| 501 | `capability_off`：这个部署（或这个宿主）不提供这项能力，正文的 `capability` 是它的范围。见 [CapabilityOff](#resp-capabilityoff) |
| 503 | `unavailable`：暂时不可用（这个部署不提供的能力是 501 `capability_off`）。见 [Unavailable](#resp-unavailable) |

**200 响应正文**（`application/json`）

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `id` | string | 是 |  |
| `customer` | string | 是 |  |
| `customerName` | string |  |  |
| `channel` | string | 是 |  |
| `template` | string | 是 |  |
| `templateName` | string |  |  |
| `templateVersion` | integer |  |  |
| `purpose` | string |  |  |
| `triggerApp` | string |  |  |
| `triggerRef` | string |  |  |
| `vars` | map[string, string] |  |  |
| `state` | string | 是 | 状态，例如 queued、sent、delivered、failed、skipped |
| `reason` | string |  | 没发出时的原因码 |
| `reasonText` | string |  | 原因的话 |
| `detail` | string |  |  |
| `attempts` | integer | 是 |  |
| `providerRef` | string |  |  |
| `cost` | integer |  |  |
| `deliverAfter` | string(date-time) |  |  |
| `sentAt` | string(date-time) |  |  |
| `deliveredAt` | string(date-time) |  |  |
| `createdBy` | string |  |  |
| `createdAt` | string(date-time) | 是 |  |
| `updatedAt` | string(date-time) | 是 |  |

示例：

```json
{
  "id": "msg_01HZY",
  "customer": "c_88",
  "channel": "sms",
  "template": "tpl_order_ok",
  "templateName": "订单确认",
  "templateVersion": 1,
  "purpose": "service",
  "vars": {
    "order": "A1024"
  },
  "state": "sent",
  "attempts": 1,
  "cost": 1,
  "sentAt": "2026-10-31T08:00:05Z",
  "createdAt": "2026-10-31T08:00:00Z",
  "updatedAt": "2026-10-31T08:00:05Z"
}
```

### `GET /outreach/customers/{id}/messages`

本企业一位客户在各渠道收到过的消息（opv2 J2 ⑤）

需要范围 `app.outreach:reach`。English: the messages a customer of the tenant was sent on every channel, newest first, trimmed.

只限本企业的客户：别的 id 是 404 `not_found`；合并过的客户按主客户读（返回的 `customer` 是主客户）。应用自己的调用（企业令牌）可读本企业任何客户；页面会话里的人只能读他在客户应用里「查看客户」（customer.read）数据范围内的客户，范围外的与不存在一样返回 404。每条只有渠道、用途、状态、原因与时间；模板和触发来源只在本应用自己发的消息上给（`mine`）。没有消息内容、变量和接收号码。

- 认证：`bearer`
- 范围：`app.outreach:reach`
- operationId：`listCustomerOutreach`

**参数**

| 名称 | 位置 | 类型 | 必填 | 说明 |
|---|---|---|---|---|
| `id` | path | string | 是 | 客户 id |
| `since` | query | string(date-time) |  | 只列这个时间及以后建的消息（RFC 3339） |
| `limit` | query | integer |  | 最多几条<br>默认 `50`<br>最小 1，最大 200 |

**响应**

| 状态 | 说明 |
|---|---|
| 200 | 消息，新的在前 |
| 401 | 见 [Unauthorized](#resp-unauthorized) |
| 403 | 见 [Forbidden](#resp-forbidden) |
| 404 | `not_found`：对应的记录不存在（客户中心没有这个客户，访客、服务或所指的东西不存在）。见 [NotFound](#resp-notfound) |
| 422 | 字段或参数不被接受（正文读不出来、有未知字段、缺必填、取值越界）。见 [Unprocessable](#resp-unprocessable) |
| 429 | 见 [RateLimited](#resp-ratelimited) |
| 500 | 见 [Internal](#resp-internal) |
| 501 | `capability_off`：这个部署（或这个宿主）不提供这项能力，正文的 `capability` 是它的范围。见 [CapabilityOff](#resp-capabilityoff) |
| 503 | `unavailable`：暂时不可用（这个部署不提供的能力是 501 `capability_off`）。见 [Unavailable](#resp-unavailable) |

**200 响应正文**（`application/json`）

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `customer` | string | 是 | 主客户 id |
| `items` | SentMessage[] | 是 |  |
| `items[].id` | string | 是 | 消息 id |
| `items[].channel` | string | 是 | 渠道种类 |
| `items[].purpose` | string | 是 | service 或 marketing |
| `items[].state` | string | 是 | queued、sent、delivered、skipped、failed 或 cancelled |
| `items[].reason` | string |  | 没发出或失败的原因代码 |
| `items[].mine` | boolean | 是 | 本应用发的 |
| `items[].template` | string |  | 模板 id（只在 mine 时） |
| `items[].triggerRef` | string |  | 触发来源（只在 mine 时） |
| `items[].createdAt` | string(date-time) | 是 |  |
| `items[].sentAt` | string(date-time) |  |  |
| `items[].deliveredAt` | string(date-time) |  |  |

### `POST /outreach/reachable`

发送前问：这位客户现在能不能经这个渠道、为这个用途被触达（opv2 J2 ⑤）

需要范围 `app.outreach:reach`。English: whether the customer can be reached on the channel for the purpose now; nothing is reserved.

按发送时同样的顺序检查，只答能或不能和第一个不能的原因：渠道没开（channel_off）；撤回了同意（withdrawn）；营销消息没有同意（no_consent）；没有手机号或 openid（no_identity）；30 天内被服务商拦截过（blocked）；营销的每日、每周频控（freq_day、freq_week）；企业每日营销上限（tenant_cap）。能触达但在免打扰时段时，`later` 是会发出的时间。只读，不占频次；真正发送时平台会再查一遍。只限本企业的客户（别的 id 404）；页面会话里的人只限他「查看客户」数据范围内的客户，范围外 404。

- 认证：`bearer`
- 范围：`app.outreach:reach`
- operationId：`checkReachable`

**参数**

| 名称 | 位置 | 类型 | 必填 | 说明 |
|---|---|---|---|---|
| `Idempotency-Key` | header | string |  | 每个 POST 都收。SDK 未传入时会生成 uuid v4。24 小时内同键同正文重放第一次的回答<br>最短 1，最长 255 |

**请求正文（必填）**（`application/json`）

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `customer` | string | 是 | 客户 id |
| `channel` | string | 是 | 渠道种类，例如 sms |
| `purpose` | string | 是 | service 或 marketing |

**响应**

| 状态 | 说明 |
|---|---|
| 200 | 能或不能 |
| 401 | 见 [Unauthorized](#resp-unauthorized) |
| 403 | 见 [Forbidden](#resp-forbidden) |
| 404 | `not_found`：对应的记录不存在（客户中心没有这个客户，访客、服务或所指的东西不存在）。见 [NotFound](#resp-notfound) |
| 422 | 字段或参数不被接受（正文读不出来、有未知字段、缺必填、取值越界）。见 [Unprocessable](#resp-unprocessable) |
| 429 | 见 [RateLimited](#resp-ratelimited) |
| 500 | 见 [Internal](#resp-internal) |
| 501 | `capability_off`：这个部署（或这个宿主）不提供这项能力，正文的 `capability` 是它的范围。见 [CapabilityOff](#resp-capabilityoff) |
| 503 | `unavailable`：暂时不可用（这个部署不提供的能力是 501 `capability_off`）。见 [Unavailable](#resp-unavailable) |

**200 响应正文**（`application/json`）

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `reachable` | boolean | 是 | 现在能不能触达 |
| `reason` | string |  | 第一个不能的原因代码 |
| `later` | string(date-time) |  | 免打扰时段里营销消息会发出的时间 |

## options

选项列表：企业为本应用的文本字段维护的可选值

### `GET /options/{list}`

读取企业为本应用某个字段维护的选项列表

需要范围 `app.options:read`。English: the list of choices the tenant keeps for a text field of the app.

企业从没保存过时，回答的是 `default` 给的默认值。返回 `{items}`。

- 认证：`bearer`
- 范围：`app.options:read`
- operationId：`getOptions`

**参数**

| 名称 | 位置 | 类型 | 必填 | 说明 |
|---|---|---|---|---|
| `list` | path | string | 是 | 列表的键：小写字母开头，只含小写字母、数字和 -，最长 64 |
| `default` | query | string |  | 企业从没保存过这个列表时的默认选项。可以重复；SDK 的参数一次只带一个 |

**响应**

| 状态 | 说明 |
|---|---|
| 200 | 选项 |
| 401 | 见 [Unauthorized](#resp-unauthorized) |
| 403 | 见 [Forbidden](#resp-forbidden) |
| 422 | 字段或参数不被接受（正文读不出来、有未知字段、缺必填、取值越界）。见 [Unprocessable](#resp-unprocessable) |
| 429 | 见 [RateLimited](#resp-ratelimited) |
| 500 | 见 [Internal](#resp-internal) |
| 501 | `capability_off`：这个部署（或这个宿主）不提供这项能力，正文的 `capability` 是它的范围。见 [CapabilityOff](#resp-capabilityoff) |
| 503 | `unavailable`：暂时不可用（这个部署不提供的能力是 501 `capability_off`）。见 [Unavailable](#resp-unavailable) |

**200 响应正文**（`application/json`）

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `items` | string[] | 是 | 选项 |

示例：

```json
{
  "items": [
    "门店",
    "线上",
    "转介绍"
  ]
}
```

### `PUT /options/{list}`

整体替换或追加一个选项列表

需要范围 `app.options:write`。English: replaces the whole list (`items`), or adds one option (`add`).

`items` 与 `add` 至少给一个。列表的声明（`title`、`maxLen`、`defaults`）每次随调用带上。带 `K3-Dry-Run: 1` 时只检查，不保存。返回改动前（`old`）和改动后（`now`）的列表。

- 认证：`bearer`
- 范围：`app.options:write`
- operationId：`putOptions`

**参数**

| 名称 | 位置 | 类型 | 必填 | 说明 |
|---|---|---|---|---|
| `list` | path | string | 是 | 列表的键：小写字母开头，只含小写字母、数字和 -，最长 64 |
| `K3-Dry-Run` | header | string |  | 传 1 时只预演：回答和真做一样，什么也不留下<br>可选值：`1` |

**请求正文（必填）**（`application/json`）

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `title` | string |  | 列表的名字。省略时用键 |
| `maxLen` | integer |  | 每个选项最多多少个字符 |
| `defaults` | string[] |  | 企业从没保存过时的默认选项 |
| `items` | string[] |  | 整个列表，替换原来的 |
| `add` | string |  | 追加一个选项 |

示例：

```json
{
  "title": "客户来源",
  "maxLen": 20,
  "add": "展会"
}
```

**响应**

| 状态 | 说明 |
|---|---|
| 200 | 改动前后的列表 |
| 401 | 见 [Unauthorized](#resp-unauthorized) |
| 403 | 见 [Forbidden](#resp-forbidden) |
| 413 | `payload_too_large`：正文太大。见 [PayloadTooLarge](#resp-payloadtoolarge) |
| 422 | 字段或参数不被接受（正文读不出来、有未知字段、缺必填、取值越界）。见 [Unprocessable](#resp-unprocessable) |
| 429 | 见 [RateLimited](#resp-ratelimited) |
| 500 | 见 [Internal](#resp-internal) |
| 501 | `capability_off`：这个部署（或这个宿主）不提供这项能力，正文的 `capability` 是它的范围。见 [CapabilityOff](#resp-capabilityoff) |
| 503 | `unavailable`：暂时不可用（这个部署不提供的能力是 501 `capability_off`）。见 [Unavailable](#resp-unavailable) |

**200 响应正文**（`application/json`）

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `old` | string[] | 是 | 改动前 |
| `now` | string[] | 是 | 改动后 |

示例：

```json
{
  "old": [
    "门店",
    "线上"
  ],
  "now": [
    "门店",
    "线上",
    "展会"
  ]
}
```

## models

企业的服务模型：起草文字、读图片、转写语音（计入企业的用量与预算）

### `GET /models`

问企业有没有开放服务模型

需要范围 `app.models:use`。English: whether a service model is on offer, whether speech is transcribed, and the model's name.

返回 `model`（有无模型）、`transcribe`（能否转写语音）、`serviceModel`（模型名，平台不说时省略）。

- 认证：`bearer`
- 范围：`app.models:use`
- operationId：`getModelStatus`

**响应**

| 状态 | 说明 |
|---|---|
| 200 | 模型状态 |
| 401 | 见 [Unauthorized](#resp-unauthorized) |
| 403 | 见 [Forbidden](#resp-forbidden) |
| 429 | 见 [RateLimited](#resp-ratelimited) |
| 500 | 见 [Internal](#resp-internal) |
| 501 | `capability_off`：这个部署（或这个宿主）不提供这项能力，正文的 `capability` 是它的范围。见 [CapabilityOff](#resp-capabilityoff) |
| 503 | `unavailable`：暂时不可用（这个部署不提供的能力是 501 `capability_off`）。见 [Unavailable](#resp-unavailable) |

**200 响应正文**（`application/json`）

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `model` | boolean | 是 | 有服务模型可用 |
| `transcribe` | boolean | 是 | 能转写语音 |
| `serviceModel` | string |  | 服务模型的名字。平台不说时省略 |

示例：

```json
{
  "model": true,
  "transcribe": true,
  "serviceModel": "service-default"
}
```

### `POST /models/complete`

用企业的服务模型起草文字

需要范围 `app.models:use`。English: drafts text with the model the tenant's administrator chose for service scenes.

调用算调用者的：页面会话的人被屏蔽、企业的预算和限流都照常生效，用量按 `scene` 计。`scene` 是小写字母开头的字母、数字和下划线（最长 64），`system` 加 `user` 最多 256 KiB。图片走 `/models/read`。企业没有开放模型时是 503，正文里 code 为 model_off（不在 ErrorCode 里，按状态和这个字段判断）；预算或限流的拒绝保留模型路由器的状态和原话。

- 认证：`bearer`
- 范围：`app.models:use`
- operationId：`completeModel`

**参数**

| 名称 | 位置 | 类型 | 必填 | 说明 |
|---|---|---|---|---|
| `Idempotency-Key` | header | string |  | 每个 POST 都收。SDK 未传入时会生成 uuid v4。24 小时内同键同正文重放第一次的回答<br>最短 1，最长 255 |

**请求正文（必填）**（`application/json`）

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `scene` | string | 是 | 这次用途的名字，用量报表按它分开<br>格式 `^[a-z][a-z0-9_]{0,63}$` |
| `system` | string |  | 系统提示 |
| `user` | string | 是 | 用户的话。不能为空 |
| `images` | ModelImage[] |  | 图片。只有 /models/read 收 |
| `images[].mime` | string | 是 | 图片类型<br>可选值：`image/png`、`image/jpeg`、`image/webp` |
| `images[].data` | string(byte) | 是 | 图片字节的 base64 |

示例：

```json
{
  "scene": "reply_draft",
  "system": "你是客服，用简短、礼貌的口吻回复。",
  "user": "客户问：订单什么时候发货？"
}
```

**响应**

| 状态 | 说明 |
|---|---|
| 200 | 模型的回答 |
| 401 | 见 [Unauthorized](#resp-unauthorized) |
| 403 | 见 [Forbidden](#resp-forbidden) |
| 413 | `payload_too_large`：正文太大。见 [PayloadTooLarge](#resp-payloadtoolarge) |
| 422 | 字段或参数不被接受（正文读不出来、有未知字段、缺必填、取值越界）。见 [Unprocessable](#resp-unprocessable) |
| 429 | 见 [RateLimited](#resp-ratelimited) |
| 500 | 见 [Internal](#resp-internal) |
| 501 | `capability_off`：这个部署（或这个宿主）不提供这项能力，正文的 `capability` 是它的范围。见 [CapabilityOff](#resp-capabilityoff) |
| 503 | `unavailable`：暂时不可用（这个部署不提供的能力是 501 `capability_off`）。见 [Unavailable](#resp-unavailable) |

**200 响应正文**（`application/json`）

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `text` | string | 是 |  |

示例：

```json
{
  "text": "您好，订单会在 24 小时内发出，发货后会短信通知您。"
}
```

### `POST /models/read`

让企业的服务模型读文字加图片

需要范围 `app.models:use`。English: reads a document with pictures with the service model.

`images` 每张是 `{mime, data}`：`mime` 为 image/png、image/jpeg 或 image/webp，`data` 是 base64。整个请求最大 24 MiB。企业没有开放模型时是 503，正文里 code 为 model_off（不在 ErrorCode 里，按状态和这个字段判断）；预算或限流的拒绝保留模型路由器的状态和原话。

- 认证：`bearer`
- 范围：`app.models:use`
- operationId：`readWithModel`

**参数**

| 名称 | 位置 | 类型 | 必填 | 说明 |
|---|---|---|---|---|
| `Idempotency-Key` | header | string |  | 每个 POST 都收。SDK 未传入时会生成 uuid v4。24 小时内同键同正文重放第一次的回答<br>最短 1，最长 255 |

**请求正文（必填）**（`application/json`）

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `scene` | string | 是 | 这次用途的名字，用量报表按它分开<br>格式 `^[a-z][a-z0-9_]{0,63}$` |
| `system` | string |  | 系统提示 |
| `user` | string | 是 | 用户的话。不能为空 |
| `images` | ModelImage[] |  | 图片。只有 /models/read 收 |
| `images[].mime` | string | 是 | 图片类型<br>可选值：`image/png`、`image/jpeg`、`image/webp` |
| `images[].data` | string(byte) | 是 | 图片字节的 base64 |

示例：

```json
{
  "scene": "reply_draft",
  "system": "你是客服，用简短、礼貌的口吻回复。",
  "user": "客户问：订单什么时候发货？"
}
```

**响应**

| 状态 | 说明 |
|---|---|
| 200 | 模型的回答 |
| 401 | 见 [Unauthorized](#resp-unauthorized) |
| 403 | 见 [Forbidden](#resp-forbidden) |
| 413 | `payload_too_large`：正文太大。见 [PayloadTooLarge](#resp-payloadtoolarge) |
| 422 | 字段或参数不被接受（正文读不出来、有未知字段、缺必填、取值越界）。见 [Unprocessable](#resp-unprocessable) |
| 429 | 见 [RateLimited](#resp-ratelimited) |
| 500 | 见 [Internal](#resp-internal) |
| 501 | `capability_off`：这个部署（或这个宿主）不提供这项能力，正文的 `capability` 是它的范围。见 [CapabilityOff](#resp-capabilityoff) |
| 503 | `unavailable`：暂时不可用（这个部署不提供的能力是 501 `capability_off`）。见 [Unavailable](#resp-unavailable) |

**200 响应正文**（`application/json`）

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `text` | string | 是 |  |

示例：

```json
{
  "text": "您好，订单会在 24 小时内发出，发货后会短信通知您。"
}
```

### `POST /models/transcribe`

把一段录音转成文字

需要范围 `app.models:use`。English: turns the recording in the body into text.

正文是录音的原始字节，`Content-Type` 必须是 `audio/…`，最大 25 MB，不能为空。回答 `{text}`。企业没有开放模型时是 503，正文里 code 为 model_off（不在 ErrorCode 里，按状态和这个字段判断）；预算或限流的拒绝保留模型路由器的状态和原话。
SDK：Go `App.Models.Transcribe`，TS `app.models.transcribe`；录音的 `Content-Type` 由调用方给出。

- 认证：`bearer`
- 范围：`app.models:use`
- operationId：`transcribeAudio`

**参数**

| 名称 | 位置 | 类型 | 必填 | 说明 |
|---|---|---|---|---|
| `Idempotency-Key` | header | string |  | 每个 POST 都收。SDK 未传入时会生成 uuid v4。24 小时内同键同正文重放第一次的回答<br>最短 1，最长 255 |

**请求正文（必填）**（`audio/*`）

类型 `string(binary)`

录音的字节

**响应**

| 状态 | 说明 |
|---|---|
| 200 | 转写的文字 |
| 401 | 见 [Unauthorized](#resp-unauthorized) |
| 403 | 见 [Forbidden](#resp-forbidden) |
| 413 | `payload_too_large`：正文太大。见 [PayloadTooLarge](#resp-payloadtoolarge) |
| 422 | 字段或参数不被接受（正文读不出来、有未知字段、缺必填、取值越界）。见 [Unprocessable](#resp-unprocessable) |
| 429 | 见 [RateLimited](#resp-ratelimited) |
| 500 | 见 [Internal](#resp-internal) |
| 501 | `capability_off`：这个部署（或这个宿主）不提供这项能力，正文的 `capability` 是它的范围。见 [CapabilityOff](#resp-capabilityoff) |
| 503 | `unavailable`：暂时不可用（这个部署不提供的能力是 501 `capability_off`）。见 [Unavailable](#resp-unavailable) |

**200 响应正文**（`application/json`）

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `text` | string | 是 |  |

示例：

```json
{
  "text": "您好，订单会在 24 小时内发出，发货后会短信通知您。"
}
```

## intent

意图解析：把当前人员的一句话（可带图片）解析成本应用某个操作的输入建议，由本人确认后再提交

### `POST /intent/parse`

把一句话解析成本应用某个操作的输入建议

需要范围 `app.intent:parse`。English: turns one sentence of the page session's person (and up to 3 photos) into a proposal for the input of one of this app's ops, the same answer an app in the platform's process gets at `/api/intent/parse` on its own origin. Nothing is submitted: the person confirms the proposal with the op itself.

只收页面会话：应用自己的令牌是 403，正文里 code 为 person_required（解析按人进行：他能用的操作、他看得到的记录）。`app` 可省略，省略时就是本应用；写了别的应用是 422。`op` 必须是本应用清单里的操作：当前人员看不到本应用是 404（code 为 app_not_available），操作不能解析是 422（code 为 op_not_parsable）。规则能读出的（日期、时间、金额、手机号、数量、页面所在的记录）不调模型、不计费；其余的经企业的服务模型（场景 intent）读出，按用量计点，`usage` 说明这次花了多少。`text` 最多 2000 个字符；`attachments` 最多 3 张图片（png、jpeg、webp、gif，每张最大 5 MiB，base64 或 data: URL），整个请求约 21 MiB；`context.refs` 最多 20 个 `k3://` 引用。每人每分钟最多 30 次，超过是 429。Idempotency-Key 收下但不保存回答：解析不写任何东西，重试就是再解析一次（再计一次用量）。企业没有开放模型时只用规则读（`by` 为 rules）。平台没有接上解析服务时是 501（code 为 capability_off），接上了但没有配置解析器时是 503（code 为 intent_off）；模型这次读不出来是 502，code 为 intent_failed（页面改让人填表）。这几个 code 不在 ErrorCode 里，按状态和这个字段判断。

- 认证：`bearer`
- 范围：`app.intent:parse`
- operationId：`parseIntent`

**参数**

| 名称 | 位置 | 类型 | 必填 | 说明 |
|---|---|---|---|---|
| `Idempotency-Key` | header | string |  | 每个 POST 都收。SDK 未传入时会生成 uuid v4。24 小时内同键同正文重放第一次的回答<br>最短 1，最长 255 |

**请求正文（必填）**（`application/json`）

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `app` | string |  | 应用 id。省略就是本应用，写别的应用是 422 |
| `op` | string | 是 | 本应用清单里的操作名 |
| `text` | string |  | 这句话。没有图片时不能为空<br>最长 2000 |
| `attachments` | IntentAttachment[] |  | 图片（例如发票照片）<br>最多 3 项 |
| `attachments[].name` | string |  | 文件名 |
| `attachments[].mime` | string |  | image/png、image/jpeg、image/webp 或 image/gif；data 是 data: URL 时可省略 |
| `attachments[].data` | string | 是 | 图片字节的 base64，或 data: URL。最大 5 MiB |
| `context` | IntentContext |  | 页面知道的：页面所在的记录、当前人员的时间与时区 |
| `context.refs` | string[] |  | 页面所在记录的 k3:// 引用<br>最多 20 项 |
| `context.now` | string(date-time) |  | 当前时间（RFC 3339），省略用平台的时钟 |
| `context.tz` | string |  | IANA 时区名，省略是 Asia/Shanghai |

示例：

```json
{
  "op": "visit.create",
  "text": "张三明天下午三点复诊，做肩颈康复",
  "context": {
    "tz": "Asia/Shanghai"
  }
}
```

**响应**

| 状态 | 说明 |
|---|---|
| 200 | 输入建议 |
| 401 | 见 [Unauthorized](#resp-unauthorized) |
| 403 | 见 [Forbidden](#resp-forbidden) |
| 404 | `not_found`：对应的记录不存在（客户中心没有这个客户，访客、服务或所指的东西不存在）。见 [NotFound](#resp-notfound) |
| 413 | `payload_too_large`：正文太大。见 [PayloadTooLarge](#resp-payloadtoolarge) |
| 422 | 字段或参数不被接受（正文读不出来、有未知字段、缺必填、取值越界）。见 [Unprocessable](#resp-unprocessable) |
| 429 | 见 [RateLimited](#resp-ratelimited) |
| 500 | 见 [Internal](#resp-internal) |
| 501 | `capability_off`：这个部署（或这个宿主）不提供这项能力，正文的 `capability` 是它的范围。见 [CapabilityOff](#resp-capabilityoff) |
| 503 | `unavailable`：暂时不可用（这个部署不提供的能力是 501 `capability_off`）。见 [Unavailable](#resp-unavailable) |

**200 响应正文**（`application/json`）

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `input` | object | 是 | 操作的输入（按操作的输入结构校验过） |
| `fields` | map[string, IntentField] | 是 | 每个值从哪来，按字段名 |
| `missing` | string[] | 是 | 还没有值的必填项，确认卡片会问 |
| `rejected` | IntentRejection[] |  | 读出来但不符合操作的值（名字对不上记录、选项里没有） |
| `rejected[].field` | string | 是 |  |
| `rejected[].reason` | string | 是 |  |
| `by` | string | 是 | rules 是只用规则（不计费），model 是调了模型<br>可选值：`rules`、`model` |
| `usage` | IntentUsage | 是 | 这次解析花了多少：计费的 micros，和点数（1 点 = 10,000 micros，两位小数；花了就至少 0.01） |
| `usage.points` | number | 是 |  |
| `usage.micros` | integer(int64) | 是 |  |

示例：

```json
{
  "input": {
    "customer": "c_8f2",
    "startsAt": "2026-11-03T15:00:00+08:00"
  },
  "fields": {
    "customer": {
      "source": "stated",
      "ref": "k3://customer/customer/c_8f2",
      "label": "张三"
    },
    "startsAt": {
      "source": "stated"
    }
  },
  "missing": [],
  "by": "model",
  "usage": {
    "points": 0.02,
    "micros": 180
  }
}
```

## pay

收款：经企业自己的支付宝或微信收款账户向客户收款、关闭未付的单、退款（只看得到本应用的收款单）

### `POST /pay/orders`

经企业的收款账户向客户发起一笔收款

需要范围 `app.pay:write`。English: asks the tenant's customer to pay through the tenant's own Alipay or WeChat Pay account.

本应用是这张收款单的来源应用，只看得到自己的收款单；款项到账后，平台把结果送到本应用声明的 `event:payment` 服务。`key` 是这笔收款的幂等键：同一个业务动作只问一次，同键再来回答第一张单；没写时用请求头 `Idempotency-Key`，都没有是 422。这个键属于企业，别的应用用过的键是 409 `conflict`。`amountCents` 是整数分，`ttlSeconds` 是多久不付就失效。返回 201、收款单和客户的付款页 `link`；JSAPI 交易另有 `payParams`（交给微信的已签名参数，JSON 文本）。

- 认证：`bearer`
- 范围：`app.pay:write`
- operationId：`createPayOrder`

**参数**

| 名称 | 位置 | 类型 | 必填 | 说明 |
|---|---|---|---|---|
| `Idempotency-Key` | header | string |  | 每个 POST 都收。SDK 未传入时会生成 uuid v4。24 小时内同键同正文重放第一次的回答<br>最短 1，最长 255 |

**请求正文（必填）**（`application/json`）

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `channel` | string | 是 | 收款渠道，例如 alipay、wechat |
| `product` | string |  | 渠道里的产品。省略是渠道的第一个 |
| `amountCents` | integer | 是 | 金额，整数分 |
| `subject` | string | 是 | 付款页上的标题 |
| `sourceRef` | string |  | 这笔收款对应的记录，k3:// 引用 |
| `key` | string |  | 幂等键；省略时用请求头 Idempotency-Key |
| `ttlSeconds` | integer |  | 多少秒不付就失效。省略用平台默认 |
| `returnUrl` | string |  | 付完后浏览器回到哪里（支付宝页面支付、微信 H5） |
| `openId` | string |  | JSAPI 交易的付款人 openid |
| `clientIp` | string |  | H5 交易付款人的 IP |

示例：

```json
{
  "channel": "wechat",
  "amountCents": 9900,
  "subject": "会员年费",
  "sourceRef": "k3://members/member/m_42",
  "key": "renew-m_42-2026",
  "ttlSeconds": 1800
}
```

**响应**

| 状态 | 说明 |
|---|---|
| 201 | 收款单 |
| 401 | 见 [Unauthorized](#resp-unauthorized) |
| 403 | 见 [Forbidden](#resp-forbidden) |
| 404 | `not_found`：对应的记录不存在（客户中心没有这个客户，访客、服务或所指的东西不存在）。见 [NotFound](#resp-notfound) |
| 409 | 见 [Conflict](#resp-conflict) |
| 413 | `payload_too_large`：正文太大。见 [PayloadTooLarge](#resp-payloadtoolarge) |
| 422 | 字段或参数不被接受（正文读不出来、有未知字段、缺必填、取值越界）。见 [Unprocessable](#resp-unprocessable) |
| 429 | 见 [RateLimited](#resp-ratelimited) |
| 500 | 见 [Internal](#resp-internal) |
| 501 | `capability_off`：这个部署（或这个宿主）不提供这项能力，正文的 `capability` 是它的范围。见 [CapabilityOff](#resp-capabilityoff) |
| 503 | `unavailable`：暂时不可用（这个部署不提供的能力是 501 `capability_off`）。见 [Unavailable](#resp-unavailable) |

**201 响应正文**（`application/json`）

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `tenant` | string |  |  |
| `id` | string | 是 |  |
| `channel` | string | 是 |  |
| `product` | string |  |  |
| `outTradeNo` | string |  | 给渠道的商户单号 |
| `amountCents` | integer | 是 |  |
| `currency` | string |  |  |
| `subject` | string |  |  |
| `status` | string | 是 | 状态，例如 pending、paid、closed、refunded |
| `sourceApp` | string |  | 来源应用，即本应用 |
| `sourceRef` | string |  |  |
| `payToken` | string |  |  |
| `codeUrl` | string |  | 扫码支付的链接 |
| `payUrl` | string |  | 渠道的付款链接 |
| `appId` | string |  |  |
| `channelTradeNo` | string |  |  |
| `expiresAt` | string(date-time) | 是 |  |
| `paidAt` | string(date-time) |  |  |
| `closedAt` | string(date-time) |  |  |
| `refundedCents` | integer | 是 | 已退多少 |
| `createdBy` | string |  |  |
| `createdAt` | string(date-time) | 是 |  |
| `settledAt` | string(date-time) |  |  |
| `settleAttempts` | integer |  |  |
| `lastError` | string |  |  |
| `link` | string |  | 客户的付款页 |
| `payParams` | string |  | JSAPI 交易交给微信的已签名参数（JSON 文本） |

示例：

```json
{
  "id": "po_01HZZ",
  "channel": "wechat",
  "product": "native",
  "outTradeNo": "po_01HZZ",
  "amountCents": 9900,
  "currency": "CNY",
  "subject": "会员年费",
  "status": "pending",
  "sourceApp": "crm",
  "sourceRef": "k3://members/member/m_42",
  "expiresAt": "2026-10-31T09:00:00Z",
  "refundedCents": 0,
  "createdAt": "2026-10-31T08:30:00Z",
  "settleAttempts": 0,
  "link": "https://pay.example.com/p/abc123"
}
```

### `GET /pay/orders/{id}`

读取本应用的一张收款单

需要范围 `app.pay:read`。English: one order of the app.

不存在或不是本应用的是 404 `not_found`。

- 认证：`bearer`
- 范围：`app.pay:read`
- operationId：`getPayOrder`

**参数**

| 名称 | 位置 | 类型 | 必填 | 说明 |
|---|---|---|---|---|
| `id` | path | string | 是 | 收款单 id |

**响应**

| 状态 | 说明 |
|---|---|
| 200 | 收款单 |
| 401 | 见 [Unauthorized](#resp-unauthorized) |
| 403 | 见 [Forbidden](#resp-forbidden) |
| 404 | `not_found`：对应的记录不存在（客户中心没有这个客户，访客、服务或所指的东西不存在）。见 [NotFound](#resp-notfound) |
| 422 | 字段或参数不被接受（正文读不出来、有未知字段、缺必填、取值越界）。见 [Unprocessable](#resp-unprocessable) |
| 429 | 见 [RateLimited](#resp-ratelimited) |
| 500 | 见 [Internal](#resp-internal) |
| 501 | `capability_off`：这个部署（或这个宿主）不提供这项能力，正文的 `capability` 是它的范围。见 [CapabilityOff](#resp-capabilityoff) |
| 503 | `unavailable`：暂时不可用（这个部署不提供的能力是 501 `capability_off`）。见 [Unavailable](#resp-unavailable) |

**200 响应正文**（`application/json`）

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `tenant` | string |  |  |
| `id` | string | 是 |  |
| `channel` | string | 是 |  |
| `product` | string |  |  |
| `outTradeNo` | string |  | 给渠道的商户单号 |
| `amountCents` | integer | 是 |  |
| `currency` | string |  |  |
| `subject` | string |  |  |
| `status` | string | 是 | 状态，例如 pending、paid、closed、refunded |
| `sourceApp` | string |  | 来源应用，即本应用 |
| `sourceRef` | string |  |  |
| `payToken` | string |  |  |
| `codeUrl` | string |  | 扫码支付的链接 |
| `payUrl` | string |  | 渠道的付款链接 |
| `appId` | string |  |  |
| `channelTradeNo` | string |  |  |
| `expiresAt` | string(date-time) | 是 |  |
| `paidAt` | string(date-time) |  |  |
| `closedAt` | string(date-time) |  |  |
| `refundedCents` | integer | 是 | 已退多少 |
| `createdBy` | string |  |  |
| `createdAt` | string(date-time) | 是 |  |
| `settledAt` | string(date-time) |  |  |
| `settleAttempts` | integer |  |  |
| `lastError` | string |  |  |
| `link` | string |  | 客户的付款页 |
| `payParams` | string |  | JSAPI 交易交给微信的已签名参数（JSON 文本） |

示例：

```json
{
  "id": "po_01HZZ",
  "channel": "wechat",
  "product": "native",
  "outTradeNo": "po_01HZZ",
  "amountCents": 9900,
  "currency": "CNY",
  "subject": "会员年费",
  "status": "pending",
  "sourceApp": "crm",
  "sourceRef": "k3://members/member/m_42",
  "expiresAt": "2026-10-31T09:00:00Z",
  "refundedCents": 0,
  "createdAt": "2026-10-31T08:30:00Z",
  "settleAttempts": 0,
  "link": "https://pay.example.com/p/abc123"
}
```

### `POST /pay/orders/{id}/cancel`

关闭一张还没付的收款单

需要范围 `app.pay:write`。English: closes an unpaid order.

已付的单不能关（用退款），那是 409 `conflict`。返回关闭后的收款单。

- 认证：`bearer`
- 范围：`app.pay:write`
- operationId：`cancelPayOrder`

**参数**

| 名称 | 位置 | 类型 | 必填 | 说明 |
|---|---|---|---|---|
| `id` | path | string | 是 | 收款单 id |
| `Idempotency-Key` | header | string |  | 每个 POST 都收。SDK 未传入时会生成 uuid v4。24 小时内同键同正文重放第一次的回答<br>最短 1，最长 255 |

**响应**

| 状态 | 说明 |
|---|---|
| 200 | 关闭后的收款单 |
| 401 | 见 [Unauthorized](#resp-unauthorized) |
| 403 | 见 [Forbidden](#resp-forbidden) |
| 404 | `not_found`：对应的记录不存在（客户中心没有这个客户，访客、服务或所指的东西不存在）。见 [NotFound](#resp-notfound) |
| 409 | 见 [Conflict](#resp-conflict) |
| 422 | 字段或参数不被接受（正文读不出来、有未知字段、缺必填、取值越界）。见 [Unprocessable](#resp-unprocessable) |
| 429 | 见 [RateLimited](#resp-ratelimited) |
| 500 | 见 [Internal](#resp-internal) |
| 501 | `capability_off`：这个部署（或这个宿主）不提供这项能力，正文的 `capability` 是它的范围。见 [CapabilityOff](#resp-capabilityoff) |
| 503 | `unavailable`：暂时不可用（这个部署不提供的能力是 501 `capability_off`）。见 [Unavailable](#resp-unavailable) |

**200 响应正文**（`application/json`）

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `tenant` | string |  |  |
| `id` | string | 是 |  |
| `channel` | string | 是 |  |
| `product` | string |  |  |
| `outTradeNo` | string |  | 给渠道的商户单号 |
| `amountCents` | integer | 是 |  |
| `currency` | string |  |  |
| `subject` | string |  |  |
| `status` | string | 是 | 状态，例如 pending、paid、closed、refunded |
| `sourceApp` | string |  | 来源应用，即本应用 |
| `sourceRef` | string |  |  |
| `payToken` | string |  |  |
| `codeUrl` | string |  | 扫码支付的链接 |
| `payUrl` | string |  | 渠道的付款链接 |
| `appId` | string |  |  |
| `channelTradeNo` | string |  |  |
| `expiresAt` | string(date-time) | 是 |  |
| `paidAt` | string(date-time) |  |  |
| `closedAt` | string(date-time) |  |  |
| `refundedCents` | integer | 是 | 已退多少 |
| `createdBy` | string |  |  |
| `createdAt` | string(date-time) | 是 |  |
| `settledAt` | string(date-time) |  |  |
| `settleAttempts` | integer |  |  |
| `lastError` | string |  |  |
| `link` | string |  | 客户的付款页 |
| `payParams` | string |  | JSAPI 交易交给微信的已签名参数（JSON 文本） |

示例：

```json
{
  "id": "po_01HZZ",
  "channel": "wechat",
  "product": "native",
  "outTradeNo": "po_01HZZ",
  "amountCents": 9900,
  "currency": "CNY",
  "subject": "会员年费",
  "status": "pending",
  "sourceApp": "crm",
  "sourceRef": "k3://members/member/m_42",
  "expiresAt": "2026-10-31T09:00:00Z",
  "refundedCents": 0,
  "createdAt": "2026-10-31T08:30:00Z",
  "settleAttempts": 0,
  "link": "https://pay.example.com/p/abc123"
}
```

### `GET /pay/orders/{id}/refunds`

列出一张收款单的退款

需要范围 `app.pay:read`。English: the refunds of an order.

返回 `{items}`。

- 认证：`bearer`
- 范围：`app.pay:read`
- operationId：`listPayRefunds`

**参数**

| 名称 | 位置 | 类型 | 必填 | 说明 |
|---|---|---|---|---|
| `id` | path | string | 是 | 收款单 id |

**响应**

| 状态 | 说明 |
|---|---|
| 200 | 退款 |
| 401 | 见 [Unauthorized](#resp-unauthorized) |
| 403 | 见 [Forbidden](#resp-forbidden) |
| 404 | `not_found`：对应的记录不存在（客户中心没有这个客户，访客、服务或所指的东西不存在）。见 [NotFound](#resp-notfound) |
| 422 | 字段或参数不被接受（正文读不出来、有未知字段、缺必填、取值越界）。见 [Unprocessable](#resp-unprocessable) |
| 429 | 见 [RateLimited](#resp-ratelimited) |
| 500 | 见 [Internal](#resp-internal) |
| 501 | `capability_off`：这个部署（或这个宿主）不提供这项能力，正文的 `capability` 是它的范围。见 [CapabilityOff](#resp-capabilityoff) |
| 503 | `unavailable`：暂时不可用（这个部署不提供的能力是 501 `capability_off`）。见 [Unavailable](#resp-unavailable) |

**200 响应正文**（`application/json`）

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `items` | PayRefund[] | 是 |  |
| `items[].tenant` | string |  |  |
| `items[].id` | string | 是 |  |
| `items[].orderId` | string | 是 |  |
| `items[].outRefundNo` | string |  |  |
| `items[].amountCents` | integer | 是 |  |
| `items[].reason` | string |  |  |
| `items[].status` | string | 是 |  |
| `items[].channelRefundId` | string |  |  |
| `items[].createdBy` | string |  |  |
| `items[].createdAt` | string(date-time) | 是 |  |
| `items[].settledAt` | string(date-time) |  |  |
| `items[].settleAttempts` | integer |  |  |
| `items[].lastError` | string |  |  |

### `POST /pay/orders/{id}/refunds`

给一张已付的收款单退款

需要范围 `app.pay:write`。English: refunds a paid order, in part or what is left.

`amountCents` 为 0 或省略是退剩下的全部。`key` 是这笔退款的幂等键，没写时用请求头 `Idempotency-Key`，都没有是 422。返回 201 和退款。

- 认证：`bearer`
- 范围：`app.pay:write`
- operationId：`refundPayOrder`

**参数**

| 名称 | 位置 | 类型 | 必填 | 说明 |
|---|---|---|---|---|
| `id` | path | string | 是 | 收款单 id |
| `Idempotency-Key` | header | string |  | 每个 POST 都收。SDK 未传入时会生成 uuid v4。24 小时内同键同正文重放第一次的回答<br>最短 1，最长 255 |

**请求正文（必填）**（`application/json`）

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `amountCents` | integer |  | 退多少，整数分 |
| `reason` | string |  | 退款原因 |
| `key` | string |  | 幂等键；省略时用请求头 Idempotency-Key |

示例：

```json
{
  "amountCents": 3000,
  "reason": "部分退款",
  "key": "refund-o_1-1"
}
```

**响应**

| 状态 | 说明 |
|---|---|
| 201 | 退款 |
| 401 | 见 [Unauthorized](#resp-unauthorized) |
| 403 | 见 [Forbidden](#resp-forbidden) |
| 404 | `not_found`：对应的记录不存在（客户中心没有这个客户，访客、服务或所指的东西不存在）。见 [NotFound](#resp-notfound) |
| 409 | 见 [Conflict](#resp-conflict) |
| 413 | `payload_too_large`：正文太大。见 [PayloadTooLarge](#resp-payloadtoolarge) |
| 422 | 字段或参数不被接受（正文读不出来、有未知字段、缺必填、取值越界）。见 [Unprocessable](#resp-unprocessable) |
| 429 | 见 [RateLimited](#resp-ratelimited) |
| 500 | 见 [Internal](#resp-internal) |
| 501 | `capability_off`：这个部署（或这个宿主）不提供这项能力，正文的 `capability` 是它的范围。见 [CapabilityOff](#resp-capabilityoff) |
| 503 | `unavailable`：暂时不可用（这个部署不提供的能力是 501 `capability_off`）。见 [Unavailable](#resp-unavailable) |

**201 响应正文**（`application/json`）

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `tenant` | string |  |  |
| `id` | string | 是 |  |
| `orderId` | string | 是 |  |
| `outRefundNo` | string |  |  |
| `amountCents` | integer | 是 |  |
| `reason` | string |  |  |
| `status` | string | 是 |  |
| `channelRefundId` | string |  |  |
| `createdBy` | string |  |  |
| `createdAt` | string(date-time) | 是 |  |
| `settledAt` | string(date-time) |  |  |
| `settleAttempts` | integer |  |  |
| `lastError` | string |  |  |

示例：

```json
{
  "id": "rf_01HZZ",
  "orderId": "po_01HZZ",
  "amountCents": 3000,
  "reason": "部分退款",
  "status": "processing",
  "createdAt": "2026-10-31T10:00:00Z"
}
```

## approvals

审批：为本应用的对象提交、重新提交、撤回审批，读进度，处理当前人员的待办

### `GET /approvals`

读取一个对象最近的审批

需要范围 `app.approvals:read`。English: the latest approval of an object of the app, for its requester or someone asked to decide, or for the app itself.

审批是人的：除 `GET /approvals/participates`、`GET /approvals` 与 `POST /approvals/{id}/withdraw` 外只收页面会话，应用自己的令牌是 403 `forbidden`；应用自己的令牌可以读取本应用任何对象的审批（不含个人的待办）、撤回本应用的任何审批（谁可以由应用判断）。`subject` 是对象的引用。别人的审批、或没有审批，是 404 `not_found`。

- 认证：`bearer`
- 范围：`app.approvals:read`
- operationId：`getApproval`

**参数**

| 名称 | 位置 | 类型 | 必填 | 说明 |
|---|---|---|---|---|
| `subject` | query | string | 是 | 对象的引用 |

**响应**

| 状态 | 说明 |
|---|---|
| 200 | 审批 |
| 401 | 见 [Unauthorized](#resp-unauthorized) |
| 403 | 见 [Forbidden](#resp-forbidden) |
| 404 | `not_found`：对应的记录不存在（客户中心没有这个客户，访客、服务或所指的东西不存在）。见 [NotFound](#resp-notfound) |
| 422 | 字段或参数不被接受（正文读不出来、有未知字段、缺必填、取值越界）。见 [Unprocessable](#resp-unprocessable) |
| 429 | 见 [RateLimited](#resp-ratelimited) |
| 500 | 见 [Internal](#resp-internal) |
| 501 | `capability_off`：这个部署（或这个宿主）不提供这项能力，正文的 `capability` 是它的范围。见 [CapabilityOff](#resp-capabilityoff) |
| 503 | `unavailable`：暂时不可用（这个部署不提供的能力是 501 `capability_off`）。见 [Unavailable](#resp-unavailable) |

**200 响应正文**（`application/json`）

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `id` | string | 是 |  |
| `kind` | string | 是 |  |
| `subject` | string | 是 |  |
| `number` | string | 是 |  |
| `title` | string | 是 |  |
| `status` | string | 是 | running、returned、approved、rejected、withdrawn、terminated、error |
| `requester` | string | 是 |  |
| `lines` | ApprovalLine[] | 是 |  |
| `lines[].label` | string | 是 |  |
| `lines[].text` | string | 是 |  |
| `waiting` | string[] | 是 | 在等谁，例如“直属上级：鲍勃” |
| `mine` | ApprovalTask[] | 是 | 调用者自己待办的任务 |
| `mine[].id` | string | 是 |  |
| `mine[].node` | string | 是 |  |
| `mine[].name` | string | 是 |  |
| `mine[].version` | integer | 是 | 处理时要带回的版本 |
| `mine[].actions` | string[] | 是 | 能做的动作 |
| `mine[].needComment` | string[] |  | 必须写意见的动作 |
| `steps` | ApprovalStep[] | 是 |  |
| `steps[].kind` | string | 是 |  |
| `steps[].node` | string |  |  |
| `steps[].actor` | string |  |  |
| `steps[].name` | string |  |  |
| `steps[].comment` | string |  |  |
| `steps[].at` | string(date-time) | 是 |  |
| `canWithdraw` | boolean | 是 |  |
| `link` | string | 是 | 审批应用里这条审批的页面 |
| `settle` | string |  | 把结果告诉第三方应用的进展：pending、done、failed；没有要告诉的则省略 |
| `settleNote` | string |  |  |

示例：

```json
{
  "id": "ap_01J",
  "kind": "hr.leave",
  "subject": "k3://hr/leave/l_17",
  "number": "QJ-2026-0042",
  "title": "王芳请假 2 天",
  "status": "running",
  "requester": "u_1001",
  "lines": [
    {
      "label": "日期",
      "text": "11-03 至 11-04"
    }
  ],
  "waiting": [
    "直属上级：鲍勃"
  ],
  "mine": [],
  "steps": [],
  "canWithdraw": true,
  "link": "/instances/ap_01J"
}
```

### `POST /approvals`

为本应用的一个对象提交审批

需要范围 `app.approvals:write`。English: asks for the approval of one object of the app, as the person using it.

审批是人的：除 `GET /approvals/participates`、`GET /approvals` 与 `POST /approvals/{id}/withdraw` 外只收页面会话，应用自己的令牌是 403 `forbidden`；应用自己的令牌可以读取本应用任何对象的审批（不含个人的待办）、撤回本应用的任何审批（谁可以由应用判断）。`kind` 是本应用在清单 `approvals` 里声明的审批种类，`subject` 是对象的引用。事实和摘要不由人的令牌给：平台向应用发签名的 PREPARE 调用，由应用的回答决定；结果以签名的 DECIDED 事件送回应用。同一对象已有没结束的审批是 409 `conflict`；应用拒绝（对象不存在、状态不对）是 409 或 422，话是给人看的。返回 201 和审批的引用。

- 认证：`bearer`
- 范围：`app.approvals:write`
- operationId：`submitApproval`

**参数**

| 名称 | 位置 | 类型 | 必填 | 说明 |
|---|---|---|---|---|
| `Idempotency-Key` | header | string |  | 每个 POST 都收。SDK 未传入时会生成 uuid v4。24 小时内同键同正文重放第一次的回答<br>最短 1，最长 255 |

**请求正文（必填）**（`application/json`）

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `kind` | string | 是 | 本应用在清单 approvals 里声明的审批种类 |
| `subject` | string | 是 | 对象的引用，在这个种类声明的前缀之下 |

示例：

```json
{
  "kind": "hr.leave",
  "subject": "k3://hr/leave/l_17"
}
```

**响应**

| 状态 | 说明 |
|---|---|
| 201 | 审批的引用 |
| 401 | 见 [Unauthorized](#resp-unauthorized) |
| 403 | 见 [Forbidden](#resp-forbidden) |
| 404 | `not_found`：对应的记录不存在（客户中心没有这个客户，访客、服务或所指的东西不存在）。见 [NotFound](#resp-notfound) |
| 409 | 见 [Conflict](#resp-conflict) |
| 413 | `payload_too_large`：正文太大。见 [PayloadTooLarge](#resp-payloadtoolarge) |
| 422 | 字段或参数不被接受（正文读不出来、有未知字段、缺必填、取值越界）。见 [Unprocessable](#resp-unprocessable) |
| 429 | 见 [RateLimited](#resp-ratelimited) |
| 500 | 见 [Internal](#resp-internal) |
| 501 | `capability_off`：这个部署（或这个宿主）不提供这项能力，正文的 `capability` 是它的范围。见 [CapabilityOff](#resp-capabilityoff) |
| 503 | `unavailable`：暂时不可用（这个部署不提供的能力是 501 `capability_off`）。见 [Unavailable](#resp-unavailable) |

**201 响应正文**（`application/json`）

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `id` | string | 是 |  |
| `number` | string | 是 | 审批单号 |
| `status` | string | 是 | running、returned、approved、rejected、withdrawn、terminated、error |

示例：

```json
{
  "id": "ap_01J",
  "number": "QJ-2026-0042",
  "status": "running"
}
```

### `POST /approvals/{id}/resubmit`

重新提交一个被退回的审批

需要范围 `app.approvals:write`。English: submits a returned approval again.

审批是人的：除 `GET /approvals/participates`、`GET /approvals` 与 `POST /approvals/{id}/withdraw` 外只收页面会话，应用自己的令牌是 403 `forbidden`；应用自己的令牌可以读取本应用任何对象的审批（不含个人的待办）、撤回本应用的任何审批（谁可以由应用判断）。正文同提交。返回审批的引用。

- 认证：`bearer`
- 范围：`app.approvals:write`
- operationId：`resubmitApproval`

**参数**

| 名称 | 位置 | 类型 | 必填 | 说明 |
|---|---|---|---|---|
| `id` | path | string | 是 | 审批 id |
| `Idempotency-Key` | header | string |  | 每个 POST 都收。SDK 未传入时会生成 uuid v4。24 小时内同键同正文重放第一次的回答<br>最短 1，最长 255 |

**请求正文（必填）**（`application/json`）

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `kind` | string | 是 | 本应用在清单 approvals 里声明的审批种类 |
| `subject` | string | 是 | 对象的引用，在这个种类声明的前缀之下 |

示例：

```json
{
  "kind": "hr.leave",
  "subject": "k3://hr/leave/l_17"
}
```

**响应**

| 状态 | 说明 |
|---|---|
| 200 | 审批的引用 |
| 401 | 见 [Unauthorized](#resp-unauthorized) |
| 403 | 见 [Forbidden](#resp-forbidden) |
| 404 | `not_found`：对应的记录不存在（客户中心没有这个客户，访客、服务或所指的东西不存在）。见 [NotFound](#resp-notfound) |
| 409 | 见 [Conflict](#resp-conflict) |
| 413 | `payload_too_large`：正文太大。见 [PayloadTooLarge](#resp-payloadtoolarge) |
| 422 | 字段或参数不被接受（正文读不出来、有未知字段、缺必填、取值越界）。见 [Unprocessable](#resp-unprocessable) |
| 429 | 见 [RateLimited](#resp-ratelimited) |
| 500 | 见 [Internal](#resp-internal) |
| 501 | `capability_off`：这个部署（或这个宿主）不提供这项能力，正文的 `capability` 是它的范围。见 [CapabilityOff](#resp-capabilityoff) |
| 503 | `unavailable`：暂时不可用（这个部署不提供的能力是 501 `capability_off`）。见 [Unavailable](#resp-unavailable) |

**200 响应正文**（`application/json`）

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `id` | string | 是 |  |
| `number` | string | 是 | 审批单号 |
| `status` | string | 是 | running、returned、approved、rejected、withdrawn、terminated、error |

示例：

```json
{
  "id": "ap_01J",
  "number": "QJ-2026-0042",
  "status": "running"
}
```

### `POST /approvals/{id}/withdraw`

撤回自己提交的审批

需要范围 `app.approvals:write`。English: withdraws the person's own approval, or (the app's own token) any approval of the app's objects.

审批是人的：除 `GET /approvals/participates`、`GET /approvals` 与 `POST /approvals/{id}/withdraw` 外只收页面会话，应用自己的令牌是 403 `forbidden`；应用自己的令牌可以读取本应用任何对象的审批（不含个人的待办）、撤回本应用的任何审批（谁可以由应用判断）。`reason` 是撤回的原因，可省略。返回审批的引用。

- 认证：`bearer`
- 范围：`app.approvals:write`
- operationId：`withdrawApproval`

**参数**

| 名称 | 位置 | 类型 | 必填 | 说明 |
|---|---|---|---|---|
| `id` | path | string | 是 | 审批 id |
| `Idempotency-Key` | header | string |  | 每个 POST 都收。SDK 未传入时会生成 uuid v4。24 小时内同键同正文重放第一次的回答<br>最短 1，最长 255 |

**请求正文（必填）**（`application/json`）

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `reason` | string |  | 撤回的原因 |

示例：

```json
{
  "reason": "日期填错了"
}
```

**响应**

| 状态 | 说明 |
|---|---|
| 200 | 审批的引用 |
| 401 | 见 [Unauthorized](#resp-unauthorized) |
| 403 | 见 [Forbidden](#resp-forbidden) |
| 404 | `not_found`：对应的记录不存在（客户中心没有这个客户，访客、服务或所指的东西不存在）。见 [NotFound](#resp-notfound) |
| 409 | 见 [Conflict](#resp-conflict) |
| 413 | `payload_too_large`：正文太大。见 [PayloadTooLarge](#resp-payloadtoolarge) |
| 422 | 字段或参数不被接受（正文读不出来、有未知字段、缺必填、取值越界）。见 [Unprocessable](#resp-unprocessable) |
| 429 | 见 [RateLimited](#resp-ratelimited) |
| 500 | 见 [Internal](#resp-internal) |
| 501 | `capability_off`：这个部署（或这个宿主）不提供这项能力，正文的 `capability` 是它的范围。见 [CapabilityOff](#resp-capabilityoff) |
| 503 | `unavailable`：暂时不可用（这个部署不提供的能力是 501 `capability_off`）。见 [Unavailable](#resp-unavailable) |

**200 响应正文**（`application/json`）

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `id` | string | 是 |  |
| `number` | string | 是 | 审批单号 |
| `status` | string | 是 | running、returned、approved、rejected、withdrawn、terminated、error |

示例：

```json
{
  "id": "ap_01J",
  "number": "QJ-2026-0042",
  "status": "running"
}
```

### `GET /approvals/waiting`

列出当前人员本应用的审批待办

需要范围 `app.approvals:read`。English: the tasks waiting for the person in the app's approvals.

审批是人的：除 `GET /approvals/participates`、`GET /approvals` 与 `POST /approvals/{id}/withdraw` 外只收页面会话，应用自己的令牌是 403 `forbidden`；应用自己的令牌可以读取本应用任何对象的审批（不含个人的待办）、撤回本应用的任何审批（谁可以由应用判断）。`kind` 只要这一种审批的待办。最多 100 条。返回 `{items}`。

- 认证：`bearer`
- 范围：`app.approvals:read`
- operationId：`listWaitingApprovals`

**参数**

| 名称 | 位置 | 类型 | 必填 | 说明 |
|---|---|---|---|---|
| `kind` | query | string |  | 审批种类，例如 hr.leave |

**响应**

| 状态 | 说明 |
|---|---|
| 200 | 待办 |
| 401 | 见 [Unauthorized](#resp-unauthorized) |
| 403 | 见 [Forbidden](#resp-forbidden) |
| 422 | 字段或参数不被接受（正文读不出来、有未知字段、缺必填、取值越界）。见 [Unprocessable](#resp-unprocessable) |
| 429 | 见 [RateLimited](#resp-ratelimited) |
| 500 | 见 [Internal](#resp-internal) |
| 501 | `capability_off`：这个部署（或这个宿主）不提供这项能力，正文的 `capability` 是它的范围。见 [CapabilityOff](#resp-capabilityoff) |
| 503 | `unavailable`：暂时不可用（这个部署不提供的能力是 501 `capability_off`）。见 [Unavailable](#resp-unavailable) |

**200 响应正文**（`application/json`）

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `items` | ApprovalWaiting[] | 是 |  |
| `items[].approval` | string | 是 | 审批 id |
| `items[].task` | ApprovalTask | 是 | 一条待办，以及能对它做什么 |
| `items[].task.id` | string | 是 |  |
| `items[].task.node` | string | 是 |  |
| `items[].task.name` | string | 是 |  |
| `items[].task.version` | integer | 是 | 处理时要带回的版本 |
| `items[].task.actions` | string[] | 是 | 能做的动作 |
| `items[].task.needComment` | string[] |  | 必须写意见的动作 |
| `items[].subject` | string | 是 |  |
| `items[].title` | string | 是 |  |
| `items[].lines` | ApprovalLine[] | 是 |  |
| `items[].lines[].label` | string | 是 |  |
| `items[].lines[].text` | string | 是 |  |
| `items[].since` | string(date-time) | 是 |  |

### `POST /approvals/tasks/{task}/actions`

处理一条审批待办

需要范围 `app.approvals:write`。English: decides a task of the person's.

审批是人的：除 `GET /approvals/participates`、`GET /approvals` 与 `POST /approvals/{id}/withdraw` 外只收页面会话，应用自己的令牌是 403 `forbidden`；应用自己的令牌可以读取本应用任何对象的审批（不含个人的待办）、撤回本应用的任何审批（谁可以由应用判断）。`action` 是 approve、reject、return、transfer、presign、aftersign、cosign 之一；`version` 是人看到的待办版本，过期是 409 `conflict`；转交和加签用 `target` 或 `users`。返回处理后的审批。

- 认证：`bearer`
- 范围：`app.approvals:write`
- operationId：`actOnApprovalTask`

**参数**

| 名称 | 位置 | 类型 | 必填 | 说明 |
|---|---|---|---|---|
| `task` | path | string | 是 | 待办 id |
| `Idempotency-Key` | header | string |  | 每个 POST 都收。SDK 未传入时会生成 uuid v4。24 小时内同键同正文重放第一次的回答<br>最短 1，最长 255 |

**请求正文（必填）**（`application/json`）

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `action` | string | 是 | 可选值：`approve`、`reject`、`return`、`transfer`、`presign`、`aftersign`、`cosign` |
| `version` | integer | 是 | 人看到的待办版本 |
| `comment` | string |  |  |
| `target` | string |  | 退回到哪个节点，或转交给谁 |
| `users` | string[] |  | 加签的人 |

示例：

```json
{
  "action": "approve",
  "version": 3,
  "comment": "同意"
}
```

**响应**

| 状态 | 说明 |
|---|---|
| 200 | 处理后的审批 |
| 401 | 见 [Unauthorized](#resp-unauthorized) |
| 403 | 见 [Forbidden](#resp-forbidden) |
| 404 | `not_found`：对应的记录不存在（客户中心没有这个客户，访客、服务或所指的东西不存在）。见 [NotFound](#resp-notfound) |
| 409 | 见 [Conflict](#resp-conflict) |
| 413 | `payload_too_large`：正文太大。见 [PayloadTooLarge](#resp-payloadtoolarge) |
| 422 | 字段或参数不被接受（正文读不出来、有未知字段、缺必填、取值越界）。见 [Unprocessable](#resp-unprocessable) |
| 429 | 见 [RateLimited](#resp-ratelimited) |
| 500 | 见 [Internal](#resp-internal) |
| 501 | `capability_off`：这个部署（或这个宿主）不提供这项能力，正文的 `capability` 是它的范围。见 [CapabilityOff](#resp-capabilityoff) |
| 503 | `unavailable`：暂时不可用（这个部署不提供的能力是 501 `capability_off`）。见 [Unavailable](#resp-unavailable) |

**200 响应正文**（`application/json`）

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `id` | string | 是 |  |
| `kind` | string | 是 |  |
| `subject` | string | 是 |  |
| `number` | string | 是 |  |
| `title` | string | 是 |  |
| `status` | string | 是 | running、returned、approved、rejected、withdrawn、terminated、error |
| `requester` | string | 是 |  |
| `lines` | ApprovalLine[] | 是 |  |
| `lines[].label` | string | 是 |  |
| `lines[].text` | string | 是 |  |
| `waiting` | string[] | 是 | 在等谁，例如“直属上级：鲍勃” |
| `mine` | ApprovalTask[] | 是 | 调用者自己待办的任务 |
| `mine[].id` | string | 是 |  |
| `mine[].node` | string | 是 |  |
| `mine[].name` | string | 是 |  |
| `mine[].version` | integer | 是 | 处理时要带回的版本 |
| `mine[].actions` | string[] | 是 | 能做的动作 |
| `mine[].needComment` | string[] |  | 必须写意见的动作 |
| `steps` | ApprovalStep[] | 是 |  |
| `steps[].kind` | string | 是 |  |
| `steps[].node` | string |  |  |
| `steps[].actor` | string |  |  |
| `steps[].name` | string |  |  |
| `steps[].comment` | string |  |  |
| `steps[].at` | string(date-time) | 是 |  |
| `canWithdraw` | boolean | 是 |  |
| `link` | string | 是 | 审批应用里这条审批的页面 |
| `settle` | string |  | 把结果告诉第三方应用的进展：pending、done、failed；没有要告诉的则省略 |
| `settleNote` | string |  |  |

示例：

```json
{
  "id": "ap_01J",
  "kind": "hr.leave",
  "subject": "k3://hr/leave/l_17",
  "number": "QJ-2026-0042",
  "title": "王芳请假 2 天",
  "status": "running",
  "requester": "u_1001",
  "lines": [
    {
      "label": "日期",
      "text": "11-03 至 11-04"
    }
  ],
  "waiting": [
    "直属上级：鲍勃"
  ],
  "mine": [],
  "steps": [],
  "canWithdraw": true,
  "link": "/instances/ap_01J"
}
```

### `GET /approvals/participates`

问某人是否参与一个对象的审批

需要范围 `app.approvals:read`。English: whether `user` takes part in the approval of `subject` (what lets the app show the object to them).

这一个操作应用自己的令牌也能调。返回 `{participates}`。

- 认证：`bearer`
- 范围：`app.approvals:read`
- operationId：`approvalParticipates`

**参数**

| 名称 | 位置 | 类型 | 必填 | 说明 |
|---|---|---|---|---|
| `subject` | query | string | 是 | 对象的引用 |
| `user` | query | string | 是 | 用户 id |

**响应**

| 状态 | 说明 |
|---|---|
| 200 | 是否参与 |
| 401 | 见 [Unauthorized](#resp-unauthorized) |
| 403 | 见 [Forbidden](#resp-forbidden) |
| 422 | 字段或参数不被接受（正文读不出来、有未知字段、缺必填、取值越界）。见 [Unprocessable](#resp-unprocessable) |
| 429 | 见 [RateLimited](#resp-ratelimited) |
| 500 | 见 [Internal](#resp-internal) |
| 501 | `capability_off`：这个部署（或这个宿主）不提供这项能力，正文的 `capability` 是它的范围。见 [CapabilityOff](#resp-capabilityoff) |
| 503 | `unavailable`：暂时不可用（这个部署不提供的能力是 501 `capability_off`）。见 [Unavailable](#resp-unavailable) |

**200 响应正文**（`application/json`）

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `participates` | boolean | 是 |  |

示例：

```json
{
  "participates": true
}
```

## calendar

日历：把本应用的日程（课节、面试、预订）放到相关人员的日历上

### `POST /calendar/projections`

把本应用的一条日程放到相关人员的日历上，或取消它

需要范围 `app.calendar:write`。English: puts one fact of the app (a session, an interview, a booked room) on the people's calendars as a projection, or cancels it.

每个应用的每个 `ref` 只有一条投影。`item` 是投影；省略或为 null 表示取消。`version` 是这次推送的版本，必须是正数，用来排序：比已应用的更旧的推送回答 `stale`，什么也不改。回答 `result` 是日历条目的 id，或 `stale`。

- 认证：`bearer`
- 范围：`app.calendar:write`
- operationId：`pushCalendarProjection`

**参数**

| 名称 | 位置 | 类型 | 必填 | 说明 |
|---|---|---|---|---|
| `Idempotency-Key` | header | string |  | 每个 POST 都收。SDK 未传入时会生成 uuid v4。24 小时内同键同正文重放第一次的回答<br>最短 1，最长 255 |

**请求正文（必填）**（`application/json`）

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `ref` | string | 是 | 事实的 k3:// 引用 |
| `version` | integer | 是 | 这次推送的版本，正数。比已应用的旧就是 stale<br>最小 1 |
| `item` | CalendarProjection |  | 本应用的一条日程在日历上的样子 |
| `item.ref` | string | 是 | 事实的 k3:// 引用，k3://{应用}/{类型}/{id} |
| `item.route` | string |  | 在本应用里打开它的路由 |
| `item.owner` | string | 是 | 这条日程属于的人（用户 id） |
| `item.attendees` | string[] |  | 其他参与的人 |
| `item.start` | string(date-time) | 是 |  |
| `item.end` | string(date-time) | 是 |  |
| `item.tz` | string |  | 时区 |
| `item.allDay` | boolean |  |  |
| `item.title` | string | 是 | 中文标题 |
| `item.titleEn` | string |  | 英文标题 |
| `item.place` | string |  |  |
| `item.detail` | string |  | 只有这条日程自己的人能读的内容 |
| `item.privacy` | string |  | 隐私等级。省略是 normal<br>可选值：`normal`、`private`、`health` |
| `item.state` | string |  | 应用自己的状态词，显示在条目旁 |

示例：

```json
{
  "ref": "k3://rehab/session/s_7",
  "version": 1,
  "item": {
    "ref": "k3://rehab/session/s_7",
    "route": "/sessions/s_7",
    "owner": "u_2001",
    "attendees": [
      "u_3002"
    ],
    "start": "2026-11-03T02:00:00Z",
    "end": "2026-11-03T03:00:00Z",
    "tz": "Asia/Shanghai",
    "title": "康复课 · 术后康复",
    "titleEn": "Rehab session · Post-op",
    "place": "3 号诊室",
    "privacy": "health",
    "state": "booked"
  }
}
```

**响应**

| 状态 | 说明 |
|---|---|
| 200 | 推送的结果 |
| 401 | 见 [Unauthorized](#resp-unauthorized) |
| 403 | 见 [Forbidden](#resp-forbidden) |
| 413 | `payload_too_large`：正文太大。见 [PayloadTooLarge](#resp-payloadtoolarge) |
| 422 | 字段或参数不被接受（正文读不出来、有未知字段、缺必填、取值越界）。见 [Unprocessable](#resp-unprocessable) |
| 429 | 见 [RateLimited](#resp-ratelimited) |
| 500 | 见 [Internal](#resp-internal) |
| 501 | `capability_off`：这个部署（或这个宿主）不提供这项能力，正文的 `capability` 是它的范围。见 [CapabilityOff](#resp-capabilityoff) |
| 503 | `unavailable`：暂时不可用（这个部署不提供的能力是 501 `capability_off`）。见 [Unavailable](#resp-unavailable) |

**200 响应正文**（`application/json`）

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `result` | string | 是 | 日历条目的 id，或 stale（已应用过更新的推送） |

示例：

```json
{
  "result": "cal_01K"
}
```

## imports

历史导入：登记本应用导入的批次，并记下每条记录的来源

### `POST /imports/{batch}/items`

登记本应用导入的一段历史数据（批次、每一行的结果、记录的来源）

需要范围 `app.imports:write`。English: records one chunk of a history import the app ran in its own process: the batch, what became of each row, and the provenance of each record a row wrote.

应用用自己的命令、在自己的数据库里按业务时刻写入旧系统的历史（ADR-0081），每写完一段就把结果报到这里。平台把批次与行记在跨应用的批次列表里（管理后台的「导入批次」），给每条写成的记录记一条来源（`via` 为 `import`，记录的「来源」与 `GET /provenance` 读到），每段写一条审计。平台不收行的数据：`digest` 是应用对这一行原文的摘要，同一批里同一个外部键再报一次，摘要必须一样。

只收页面会话，且必须是企业管理员（`tenant.admin`）本人：应用自己的令牌是 403 person_required，其他人是 403。`batch` 只能用字母、数字和 `. _ : -`，最多 80 个字符。一次 1 到 500 行，外部键在一次提交里不能重复；`ok` 的行要有 `recordId`，`ref` 必须是本应用的记录（`k3://{本应用 id}/…`），`failed` 的行要有 `error`、不能有记录；不合规是 422，整段不收。已经记为 `ok` 的外部键再报成另一行（摘要不同，或报成 `failed`）是 409 `conflict`，整段不收。正文最多 1 MiB。回答是批次现在的样子。平台没有数据库时是 503。

- 认证：`bearer`
- 范围：`app.imports:write`
- operationId：`reportImportItems`

**参数**

| 名称 | 位置 | 类型 | 必填 | 说明 |
|---|---|---|---|---|
| `Idempotency-Key` | header | string |  | 每个 POST 都收。SDK 未传入时会生成 uuid v4。24 小时内同键同正文重放第一次的回答<br>最短 1，最长 255 |
| `batch` | path | string | 是 | 批号，导入方自定，在本应用内唯一 |

**请求正文（必填）**（`application/json`）

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `label` | string |  | 批次说明，最多 200 字；省略时保留原来的 |
| `items` | ImportReportItem[] | 是 | 最多 500 项 |
| `items[].key` | string | 是 | 旧系统里这一行的键，批内唯一，最多 120 字符 |
| `items[].kind` | string | 是 | 这一行是什么（应用自己的种类） |
| `items[].status` | string | 是 | 写成了，或失败了<br>可选值：`ok`、`failed` |
| `items[].recordId` | string |  | ok：这一行写成的主记录 id |
| `items[].ref` | string |  | ok：主记录的 k3:// 引用（本应用的）；有它就记一条来源 |
| `items[].businessAt` | string(date-time) | 是 | 业务时刻：这件事发生的时间 |
| `items[].digest` | string | 是 | 应用对这一行原文的摘要（同一行再报时必须一样），最多 128 字符 |
| `items[].error` | string |  | failed：原因，导入的人读的，最多 500 字 |

示例：

```json
{
  "label": "旧系统会员 2019–2025",
  "items": [
    {
      "key": "m-1001",
      "kind": "member",
      "status": "ok",
      "recordId": "mem_01K",
      "ref": "k3://membership/member/mem_01K",
      "businessAt": "2019-04-02T08:00:00Z",
      "digest": "9f2c…"
    },
    {
      "key": "m-1002",
      "kind": "member",
      "status": "failed",
      "businessAt": "2019-04-03T08:00:00Z",
      "digest": "1a7e…",
      "error": "手机号不对"
    }
  ]
}
```

**响应**

| 状态 | 说明 |
|---|---|
| 200 | 批次现在的样子 |
| 401 | 见 [Unauthorized](#resp-unauthorized) |
| 403 | 见 [Forbidden](#resp-forbidden) |
| 409 | 见 [Conflict](#resp-conflict) |
| 413 | `payload_too_large`：正文太大。见 [PayloadTooLarge](#resp-payloadtoolarge) |
| 422 | 字段或参数不被接受（正文读不出来、有未知字段、缺必填、取值越界）。见 [Unprocessable](#resp-unprocessable) |
| 429 | 见 [RateLimited](#resp-ratelimited) |
| 500 | 见 [Internal](#resp-internal) |
| 501 | `capability_off`：这个部署（或这个宿主）不提供这项能力，正文的 `capability` 是它的范围。见 [CapabilityOff](#resp-capabilityoff) |
| 503 | `unavailable`：暂时不可用（这个部署不提供的能力是 501 `capability_off`）。见 [Unavailable](#resp-unavailable) |

**200 响应正文**（`application/json`）

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `app` | string | 是 |  |
| `id` | string | 是 |  |
| `label` | string | 是 |  |
| `createdBy` | string | 是 | 建这个批次的人 |
| `createdName` | string | 是 |  |
| `createdAt` | string(date-time) | 是 |  |
| `updatedAt` | string(date-time) | 是 |  |
| `rowsOk` | integer | 是 |  |
| `rowsFailed` | integer | 是 |  |
| `firstAt` | string(date-time) |  | 写成的行里最早的业务时刻 |
| `lastAt` | string(date-time) |  | 写成的行里最晚的业务时刻 |

## peers

其他应用的服务经平台去问：核销台、收银台、资源占用（治疗室与设备）、推广码；平台按已安装并开启的清单选提供者，本应用不指名也碰不到它

### `GET /peers/desk`

其他应用的核销台：有没有，以及当前人员能做什么

需要范围 `app.desk:use`。English: whether an installed app serves the redemption desk, and what the caller may do there.

另一个应用的核销台，经平台去问：平台按这个组织已安装并开启的应用的清单选出提供它的应用，请求的应用不指名也碰不到它。`available` 说明有没有应用提供核销台；`redeem`、`void` 是当前人员能不能核销、作废。应用自己的调用（没有人在背后）这两项永远是 false。

- 认证：`bearer`
- 范围：`app.desk:use`
- operationId：`getPeerDesk`

**响应**

| 状态 | 说明 |
|---|---|
| 200 | 核销台的情况 |
| 401 | 见 [Unauthorized](#resp-unauthorized) |
| 403 | 见 [Forbidden](#resp-forbidden) |
| 429 | 见 [RateLimited](#resp-ratelimited) |
| 500 | 见 [Internal](#resp-internal) |
| 501 | `capability_off`：这个部署（或这个宿主）不提供这项能力，正文的 `capability` 是它的范围。见 [CapabilityOff](#resp-capabilityoff) |
| 503 | `unavailable`：暂时不可用（这个部署不提供的能力是 501 `capability_off`）。见 [Unavailable](#resp-unavailable) |

**200 响应正文**（`application/json`）

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `available` | boolean | 是 | 有应用提供核销台 |
| `redeem` | boolean | 是 | 当前人员能核销；应用自己的调用永远是 false |
| `void` | boolean | 是 | 当前人员能作废；应用自己的调用永远是 false |

### `POST /peers/desk/http`

把本应用页面的一个请求交给其他应用的核销台，以当前人员的身份

需要范围 `app.desk:use`。English: hands a request of the app's page (its `/api/desk/…` path, GET or POST, JSON body) to the redemption desk, as the page session's person.

另一个应用的核销台，经平台去问（平台按已安装并开启的清单选提供者，请求的应用不指名也碰不到它）。`method` 是 GET 或 POST，`path` 必须以 `/api/desk/` 开头，`query` 是查询串，`body` 是 JSON。只有页面会话（页面上的人）可以；应用自己的调用是 403 person_required。没有应用提供核销台是 404 no_provider。回答是提供者的回答原样：`status`、`contentType`、`body`（字节，JSON 里是 base64）；提供者回的错误在 `status` 里，这个接口本身仍是 200。正文最多 1 MiB。

- 认证：`bearer`
- 范围：`app.desk:use`
- operationId：`peerDeskHttp`

**参数**

| 名称 | 位置 | 类型 | 必填 | 说明 |
|---|---|---|---|---|
| `Idempotency-Key` | header | string |  | 每个 POST 都收。SDK 未传入时会生成 uuid v4。24 小时内同键同正文重放第一次的回答<br>最短 1，最长 255 |

**请求正文（必填）**（`application/json`）

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `method` | string | 是 | GET 或 POST<br>可选值：`GET`、`POST` |
| `path` | string | 是 | 提供者自己的路径：核销台以 `/api/desk/` 开头，收银台以 `/api/till/` 开头；不能含 `..`、`?`、`#`、反斜杠 |
| `query` | string |  | 查询串（不含 ?） |
| `body` | any |  | JSON 正文，可省略 |

**响应**

| 状态 | 说明 |
|---|---|
| 200 | 提供者的回答 |
| 401 | 见 [Unauthorized](#resp-unauthorized) |
| 403 | 见 [Forbidden](#resp-forbidden) |
| 404 | `not_found`：对应的记录不存在（客户中心没有这个客户，访客、服务或所指的东西不存在）。见 [NotFound](#resp-notfound) |
| 413 | `payload_too_large`：正文太大。见 [PayloadTooLarge](#resp-payloadtoolarge) |
| 422 | 字段或参数不被接受（正文读不出来、有未知字段、缺必填、取值越界）。见 [Unprocessable](#resp-unprocessable) |
| 429 | 见 [RateLimited](#resp-ratelimited) |
| 500 | 见 [Internal](#resp-internal) |
| 501 | `capability_off`：这个部署（或这个宿主）不提供这项能力，正文的 `capability` 是它的范围。见 [CapabilityOff](#resp-capabilityoff) |
| 503 | `unavailable`：暂时不可用（这个部署不提供的能力是 501 `capability_off`）。见 [Unavailable](#resp-unavailable) |

**200 响应正文**（`application/json`）

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `status` | integer | 是 | 提供者回答的 HTTP 状态 |
| `contentType` | string |  | 提供者回答的 Content-Type |
| `body` | string(byte) |  | 提供者回答的字节（base64） |

### `GET /peers/till`

其他应用的收银台：有没有，以及当前人员能不能收银

需要范围 `app.till:use`。English: whether an installed app serves the till, and whether the caller may sell there.

另一个应用的收银台，经平台去问：平台按这个组织已安装并开启的应用的清单选出提供它的应用，请求的应用不指名也碰不到它。`available` 说明有没有应用提供收银台；`sell` 是当前人员能不能在那里收银。应用自己的调用（没有人在背后）`sell` 永远是 false。

- 认证：`bearer`
- 范围：`app.till:use`
- operationId：`getPeerTill`

**响应**

| 状态 | 说明 |
|---|---|
| 200 | 收银台的情况 |
| 401 | 见 [Unauthorized](#resp-unauthorized) |
| 403 | 见 [Forbidden](#resp-forbidden) |
| 429 | 见 [RateLimited](#resp-ratelimited) |
| 500 | 见 [Internal](#resp-internal) |
| 501 | `capability_off`：这个部署（或这个宿主）不提供这项能力，正文的 `capability` 是它的范围。见 [CapabilityOff](#resp-capabilityoff) |
| 503 | `unavailable`：暂时不可用（这个部署不提供的能力是 501 `capability_off`）。见 [Unavailable](#resp-unavailable) |

**200 响应正文**（`application/json`）

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `available` | boolean | 是 | 有应用提供收银台 |
| `sell` | boolean | 是 | 当前人员能收银；应用自己的调用永远是 false |

### `POST /peers/till/http`

把本应用页面的一个请求交给其他应用的收银台，以当前人员的身份

需要范围 `app.till:use`。English: hands a request of the app's page (its `/api/till/…` path, GET or POST, JSON body) to the till, as the page session's person.

另一个应用的收银台，经平台去问（平台按已安装并开启的清单选提供者，请求的应用不指名也碰不到它）。`method` 是 GET 或 POST，`path` 必须以 `/api/till/` 开头，`query` 是查询串，`body` 是 JSON。只有页面会话（页面上的人）可以；应用自己的调用是 403 person_required。没有应用提供收银台是 404 no_provider。回答是提供者的回答原样：`status`、`contentType`、`body`（字节，JSON 里是 base64）；提供者回的错误在 `status` 里，这个接口本身仍是 200。正文最多 1 MiB。

- 认证：`bearer`
- 范围：`app.till:use`
- operationId：`peerTillHttp`

**参数**

| 名称 | 位置 | 类型 | 必填 | 说明 |
|---|---|---|---|---|
| `Idempotency-Key` | header | string |  | 每个 POST 都收。SDK 未传入时会生成 uuid v4。24 小时内同键同正文重放第一次的回答<br>最短 1，最长 255 |

**请求正文（必填）**（`application/json`）

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `method` | string | 是 | GET 或 POST<br>可选值：`GET`、`POST` |
| `path` | string | 是 | 提供者自己的路径：核销台以 `/api/desk/` 开头，收银台以 `/api/till/` 开头；不能含 `..`、`?`、`#`、反斜杠 |
| `query` | string |  | 查询串（不含 ?） |
| `body` | any |  | JSON 正文，可省略 |

**响应**

| 状态 | 说明 |
|---|---|
| 200 | 提供者的回答 |
| 401 | 见 [Unauthorized](#resp-unauthorized) |
| 403 | 见 [Forbidden](#resp-forbidden) |
| 404 | `not_found`：对应的记录不存在（客户中心没有这个客户，访客、服务或所指的东西不存在）。见 [NotFound](#resp-notfound) |
| 413 | `payload_too_large`：正文太大。见 [PayloadTooLarge](#resp-payloadtoolarge) |
| 422 | 字段或参数不被接受（正文读不出来、有未知字段、缺必填、取值越界）。见 [Unprocessable](#resp-unprocessable) |
| 429 | 见 [RateLimited](#resp-ratelimited) |
| 500 | 见 [Internal](#resp-internal) |
| 501 | `capability_off`：这个部署（或这个宿主）不提供这项能力，正文的 `capability` 是它的范围。见 [CapabilityOff](#resp-capabilityoff) |
| 503 | `unavailable`：暂时不可用（这个部署不提供的能力是 501 `capability_off`）。见 [Unavailable](#resp-unavailable) |

**200 响应正文**（`application/json`）

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `status` | integer | 是 | 提供者回答的 HTTP 状态 |
| `contentType` | string |  | 提供者回答的 Content-Type |
| `body` | string(byte) |  | 提供者回答的字节（base64） |

### `GET /peers/holds`

其他应用有没有提供资源占用（治疗室、设备）

需要范围 `app.holds:read`。English: whether an installed app offers resource holds.

另一个应用的资源占用，经平台去问：平台按这个组织已安装并开启的应用的清单选出提供者，请求的应用不指名也碰不到它。`available` 为 false 时，其余占用接口都是 404 no_provider，调用方可以不占资源直接预约。

- 认证：`bearer`
- 范围：`app.holds:read`
- operationId：`getPeerHolds`

**响应**

| 状态 | 说明 |
|---|---|
| 200 | 有没有 |
| 401 | 见 [Unauthorized](#resp-unauthorized) |
| 403 | 见 [Forbidden](#resp-forbidden) |
| 429 | 见 [RateLimited](#resp-ratelimited) |
| 500 | 见 [Internal](#resp-internal) |
| 501 | `capability_off`：这个部署（或这个宿主）不提供这项能力，正文的 `capability` 是它的范围。见 [CapabilityOff](#resp-capabilityoff) |
| 503 | `unavailable`：暂时不可用（这个部署不提供的能力是 501 `capability_off`）。见 [Unavailable](#resp-unavailable) |

**200 响应正文**（`application/json`）

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `available` | boolean | 是 | 有应用提供 |

### `GET /peers/holds/holdable`

可占用的治疗室和设备

需要范围 `app.holds:read`。English: the items another app may hold, as the caller sees them.

另一个应用的资源占用，经平台去问（提供者由平台按清单选，请求的应用不指名也碰不到它）。没有应用提供资源占用是 404 no_provider。

- 认证：`bearer`
- 范围：`app.holds:read`
- operationId：`listPeerHoldable`

**响应**

| 状态 | 说明 |
|---|---|
| 200 | 可占用的项目 |
| 401 | 见 [Unauthorized](#resp-unauthorized) |
| 403 | 见 [Forbidden](#resp-forbidden) |
| 404 | `not_found`：对应的记录不存在（客户中心没有这个客户，访客、服务或所指的东西不存在）。见 [NotFound](#resp-notfound) |
| 429 | 见 [RateLimited](#resp-ratelimited) |
| 500 | 见 [Internal](#resp-internal) |
| 501 | `capability_off`：这个部署（或这个宿主）不提供这项能力，正文的 `capability` 是它的范围。见 [CapabilityOff](#resp-capabilityoff) |
| 503 | `unavailable`：暂时不可用（这个部署不提供的能力是 501 `capability_off`）。见 [Unavailable](#resp-unavailable) |

**200 响应正文**（`application/json`）

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `items` | PeerHoldableItem[] | 是 |  |
| `items[].id` | string | 是 | 项目 id |
| `items[].name` | string | 是 | 名称 |
| `items[].kind` | string | 是 | 类别的种类词：room、device … |
| `items[].category` | string | 是 | 类别名称 |
| `items[].location` | string |  | 位置 |

### `POST /peers/holds/free`

查一段时间里这些项目空不空

需要范围 `app.holds:read`。English: says, per item, whether it is free in [from, to), not counting the holds of `except`.

另一个应用的资源占用，经平台去问（提供者由平台按清单选，请求的应用不指名也碰不到它）。`except` 是正在改期的来源，它自己的占用不算。没有应用提供资源占用是 404 no_provider。这是读操作，什么也不占。

- 认证：`bearer`
- 范围：`app.holds:read`
- operationId：`checkPeerHoldsFree`

**参数**

| 名称 | 位置 | 类型 | 必填 | 说明 |
|---|---|---|---|---|
| `Idempotency-Key` | header | string |  | 每个 POST 都收。SDK 未传入时会生成 uuid v4。24 小时内同键同正文重放第一次的回答<br>最短 1，最长 255 |

**请求正文（必填）**（`application/json`）

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `items` | string[] | 是 | 要查的项目 id |
| `from` | string(date-time) | 是 | 开始 |
| `to` | string(date-time) | 是 | 结束（不含） |
| `except` | string |  | 正在改期的来源（k3:// 引用），它自己的占用不算 |

**响应**

| 状态 | 说明 |
|---|---|
| 200 | 每个项目的空闲情况 |
| 401 | 见 [Unauthorized](#resp-unauthorized) |
| 403 | 见 [Forbidden](#resp-forbidden) |
| 404 | `not_found`：对应的记录不存在（客户中心没有这个客户，访客、服务或所指的东西不存在）。见 [NotFound](#resp-notfound) |
| 422 | 字段或参数不被接受（正文读不出来、有未知字段、缺必填、取值越界）。见 [Unprocessable](#resp-unprocessable) |
| 429 | 见 [RateLimited](#resp-ratelimited) |
| 500 | 见 [Internal](#resp-internal) |
| 501 | `capability_off`：这个部署（或这个宿主）不提供这项能力，正文的 `capability` 是它的范围。见 [CapabilityOff](#resp-capabilityoff) |
| 503 | `unavailable`：暂时不可用（这个部署不提供的能力是 501 `capability_off`）。见 [Unavailable](#resp-unavailable) |

**200 响应正文**（`application/json`）

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `items` | PeerFreeItem[] | 是 |  |
| `items[].item` | string | 是 | 项目 id |
| `items[].name` | string | 是 | 名称 |
| `items[].free` | boolean | 是 | 这段时间是否空闲 |
| `items[].conflict` | string |  | 被什么占着（可读的标题） |

### `POST /peers/holds/hold`

为本应用的一个对象占用这些项目

需要范围 `app.holds:write`。English: holds every free item for one object of the calling app; a taken item comes back with `conflict` and no hold.

另一个应用的资源占用，经平台去问（提供者由平台按清单选，请求的应用不指名也碰不到它）。`source` 必须是本应用自己的 `k3://` 引用（`k3://{应用}/…`），否则 422；资源应用在收回占用时据此告诉本应用。被占着的项目在回答里带 `conflict`，没有 `holdId`。没有应用提供资源占用是 404 no_provider。

- 认证：`bearer`
- 范围：`app.holds:write`
- operationId：`createPeerHold`

**参数**

| 名称 | 位置 | 类型 | 必填 | 说明 |
|---|---|---|---|---|
| `Idempotency-Key` | header | string |  | 每个 POST 都收。SDK 未传入时会生成 uuid v4。24 小时内同键同正文重放第一次的回答<br>最短 1，最长 255 |

**请求正文（必填）**（`application/json`）

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `source` | string | 是 | 本应用自己的 k3:// 引用（k3://{应用}/…），否则 422 |
| `title` | string |  | 占用的标题（别的人看到占用时读到的） |
| `items` | string[] | 是 | 要占用的项目 id |
| `from` | string(date-time) | 是 | 开始 |
| `to` | string(date-time) | 是 | 结束（不含） |

**响应**

| 状态 | 说明 |
|---|---|
| 200 | 每个项目的占用结果 |
| 401 | 见 [Unauthorized](#resp-unauthorized) |
| 403 | 见 [Forbidden](#resp-forbidden) |
| 404 | `not_found`：对应的记录不存在（客户中心没有这个客户，访客、服务或所指的东西不存在）。见 [NotFound](#resp-notfound) |
| 422 | 字段或参数不被接受（正文读不出来、有未知字段、缺必填、取值越界）。见 [Unprocessable](#resp-unprocessable) |
| 429 | 见 [RateLimited](#resp-ratelimited) |
| 500 | 见 [Internal](#resp-internal) |
| 501 | `capability_off`：这个部署（或这个宿主）不提供这项能力，正文的 `capability` 是它的范围。见 [CapabilityOff](#resp-capabilityoff) |
| 503 | `unavailable`：暂时不可用（这个部署不提供的能力是 501 `capability_off`）。见 [Unavailable](#resp-unavailable) |

**200 响应正文**（`application/json`）

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `items` | PeerHoldResult[] | 是 |  |
| `items[].item` | string | 是 | 项目 id |
| `items[].holdId` | string |  | 占用的 id；被占着的项目没有 |
| `items[].conflict` | string |  | 被什么占着（可读的标题）；有它就没有 holdId |

### `POST /peers/holds/move`

改一个占用的时间

需要范围 `app.holds:write`。English: moves a hold to [from, to).

另一个应用的资源占用，经平台去问（提供者由平台按清单选，请求的应用不指名也碰不到它）。`holdId` 必填。新的时间里项目已被占是 409 hold_conflict；没有应用提供资源占用是 404 no_provider。

- 认证：`bearer`
- 范围：`app.holds:write`
- operationId：`movePeerHold`

**参数**

| 名称 | 位置 | 类型 | 必填 | 说明 |
|---|---|---|---|---|
| `Idempotency-Key` | header | string |  | 每个 POST 都收。SDK 未传入时会生成 uuid v4。24 小时内同键同正文重放第一次的回答<br>最短 1，最长 255 |

**请求正文（必填）**（`application/json`）

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `holdId` | string | 是 | 占用的 id |
| `from` | string(date-time) | 是 | 新的开始 |
| `to` | string(date-time) | 是 | 新的结束（不含） |

**响应**

| 状态 | 说明 |
|---|---|
| 204 | 已改。没有正文 |
| 401 | 见 [Unauthorized](#resp-unauthorized) |
| 403 | 见 [Forbidden](#resp-forbidden) |
| 404 | `not_found`：对应的记录不存在（客户中心没有这个客户，访客、服务或所指的东西不存在）。见 [NotFound](#resp-notfound) |
| 409 | 见 [Conflict](#resp-conflict) |
| 422 | 字段或参数不被接受（正文读不出来、有未知字段、缺必填、取值越界）。见 [Unprocessable](#resp-unprocessable) |
| 429 | 见 [RateLimited](#resp-ratelimited) |
| 500 | 见 [Internal](#resp-internal) |
| 501 | `capability_off`：这个部署（或这个宿主）不提供这项能力，正文的 `capability` 是它的范围。见 [CapabilityOff](#resp-capabilityoff) |
| 503 | `unavailable`：暂时不可用（这个部署不提供的能力是 501 `capability_off`）。见 [Unavailable](#resp-unavailable) |

### `POST /peers/holds/release`

放掉一个占用

需要范围 `app.holds:write`。English: gives a hold back.

另一个应用的资源占用，经平台去问（提供者由平台按清单选，请求的应用不指名也碰不到它）。`holdId` 必填，`reason` 是放掉的原因。没有应用提供资源占用是 404 no_provider。

- 认证：`bearer`
- 范围：`app.holds:write`
- operationId：`releasePeerHold`

**参数**

| 名称 | 位置 | 类型 | 必填 | 说明 |
|---|---|---|---|---|
| `Idempotency-Key` | header | string |  | 每个 POST 都收。SDK 未传入时会生成 uuid v4。24 小时内同键同正文重放第一次的回答<br>最短 1，最长 255 |

**请求正文（必填）**（`application/json`）

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `holdId` | string | 是 | 占用的 id |
| `reason` | string |  | 放掉的原因 |

**响应**

| 状态 | 说明 |
|---|---|
| 204 | 已放掉。没有正文 |
| 401 | 见 [Unauthorized](#resp-unauthorized) |
| 403 | 见 [Forbidden](#resp-forbidden) |
| 404 | `not_found`：对应的记录不存在（客户中心没有这个客户，访客、服务或所指的东西不存在）。见 [NotFound](#resp-notfound) |
| 422 | 字段或参数不被接受（正文读不出来、有未知字段、缺必填、取值越界）。见 [Unprocessable](#resp-unprocessable) |
| 429 | 见 [RateLimited](#resp-ratelimited) |
| 500 | 见 [Internal](#resp-internal) |
| 501 | `capability_off`：这个部署（或这个宿主）不提供这项能力，正文的 `capability` 是它的范围。见 [CapabilityOff](#resp-capabilityoff) |
| 503 | `unavailable`：暂时不可用（这个部署不提供的能力是 501 `capability_off`）。见 [Unavailable](#resp-unavailable) |

### `GET /peers/referral-codes`

其他应用有没有提供推广码

需要范围 `app.referrals:read`。English: whether an installed app offers promotion codes.

另一个应用的推广码，经平台去问：平台按这个组织已安装并开启的应用的清单选出提供者，请求的应用不指名也碰不到它。`available` 为 false 时，其余推广码接口都是 404 no_provider。

- 认证：`bearer`
- 范围：`app.referrals:read`
- operationId：`getPeerReferralCodes`

**响应**

| 状态 | 说明 |
|---|---|
| 200 | 有没有 |
| 401 | 见 [Unauthorized](#resp-unauthorized) |
| 403 | 见 [Forbidden](#resp-forbidden) |
| 429 | 见 [RateLimited](#resp-ratelimited) |
| 500 | 见 [Internal](#resp-internal) |
| 501 | `capability_off`：这个部署（或这个宿主）不提供这项能力，正文的 `capability` 是它的范围。见 [CapabilityOff](#resp-capabilityoff) |
| 503 | `unavailable`：暂时不可用（这个部署不提供的能力是 501 `capability_off`）。见 [Unavailable](#resp-unavailable) |

**200 响应正文**（`application/json`）

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `available` | boolean | 是 | 有应用提供 |

### `POST /peers/referral-codes/code`

一位推广人的推广码、链接和海报

需要范围 `app.referrals:read`。English: the promoter's code, link and posters.

另一个应用的推广码，经平台去问（提供者由平台按清单选，请求的应用不指名也碰不到它）。`promoter` 必须写 `customer` 或 `user` 之一，否则 422；`origin`（`https://host`）是链接指向的企业站点。对方说不是推广人时是 404 `not_found`；没有应用提供推广码是 404 no_provider。这是读操作。

- 认证：`bearer`
- 范围：`app.referrals:read`
- operationId：`getPeerReferralCode`

**参数**

| 名称 | 位置 | 类型 | 必填 | 说明 |
|---|---|---|---|---|
| `Idempotency-Key` | header | string |  | 每个 POST 都收。SDK 未传入时会生成 uuid v4。24 小时内同键同正文重放第一次的回答<br>最短 1，最长 255 |

**请求正文（必填）**（`application/json`）

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `promoter` | PeerPromoter | 是 | 推广人：客户或成员，二者写一个 |
| `promoter.customer` | string |  | 客户 id（cu_…） |
| `promoter.user` | string |  | 成员 id |
| `origin` | string |  | 链接指向的企业站点（https://host） |

**响应**

| 状态 | 说明 |
|---|---|
| 200 | 推广码 |
| 401 | 见 [Unauthorized](#resp-unauthorized) |
| 403 | 见 [Forbidden](#resp-forbidden) |
| 404 | `not_found`：对应的记录不存在（客户中心没有这个客户，访客、服务或所指的东西不存在）。见 [NotFound](#resp-notfound) |
| 422 | 字段或参数不被接受（正文读不出来、有未知字段、缺必填、取值越界）。见 [Unprocessable](#resp-unprocessable) |
| 429 | 见 [RateLimited](#resp-ratelimited) |
| 500 | 见 [Internal](#resp-internal) |
| 501 | `capability_off`：这个部署（或这个宿主）不提供这项能力，正文的 `capability` 是它的范围。见 [CapabilityOff](#resp-capabilityoff) |
| 503 | `unavailable`：暂时不可用（这个部署不提供的能力是 501 `capability_off`）。见 [Unavailable](#resp-unavailable) |

**200 响应正文**（`application/json`）

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `code` | string | 是 | 推广码 |
| `status` | string | 是 | 状态 |
| `link` | string | 是 | 推广链接 |
| `posters` | PeerReferralPoster[] | 是 |  |
| `posters[].id` | string | 是 |  |
| `posters[].name` | string | 是 |  |
| `posters[].headline` | string | 是 |  |
| `posters[].subline` | string | 是 |  |
| `posters[].theme` | string | 是 |  |
| `posters[].cta` | string | 是 |  |
| `posters[].link` | string | 是 |  |

### `POST /peers/referral-codes/promoter`

一个推广码属于哪位推广人

需要范围 `app.referrals:read`。English: who owns a code that is in use.

另一个应用的推广码，经平台去问（提供者由平台按清单选，请求的应用不指名也碰不到它）。`code` 必填。码不存在或现在不带来任何人是 404 `not_found`；没有应用提供推广码是 404 no_provider。这是读操作。

- 认证：`bearer`
- 范围：`app.referrals:read`
- operationId：`getPeerReferralPromoter`

**参数**

| 名称 | 位置 | 类型 | 必填 | 说明 |
|---|---|---|---|---|
| `Idempotency-Key` | header | string |  | 每个 POST 都收。SDK 未传入时会生成 uuid v4。24 小时内同键同正文重放第一次的回答<br>最短 1，最长 255 |

**请求正文（必填）**（`application/json`）

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `code` | string | 是 | 推广码 |

**响应**

| 状态 | 说明 |
|---|---|
| 200 | 推广人 |
| 401 | 见 [Unauthorized](#resp-unauthorized) |
| 403 | 见 [Forbidden](#resp-forbidden) |
| 404 | `not_found`：对应的记录不存在（客户中心没有这个客户，访客、服务或所指的东西不存在）。见 [NotFound](#resp-notfound) |
| 422 | 字段或参数不被接受（正文读不出来、有未知字段、缺必填、取值越界）。见 [Unprocessable](#resp-unprocessable) |
| 429 | 见 [RateLimited](#resp-ratelimited) |
| 500 | 见 [Internal](#resp-internal) |
| 501 | `capability_off`：这个部署（或这个宿主）不提供这项能力，正文的 `capability` 是它的范围。见 [CapabilityOff](#resp-capabilityoff) |
| 503 | `unavailable`：暂时不可用（这个部署不提供的能力是 501 `capability_off`）。见 [Unavailable](#resp-unavailable) |

**200 响应正文**（`application/json`）

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `customer` | string |  | 客户 id（cu_…） |
| `user` | string |  | 成员 id |

## deals

成交事件：本应用的成交、退款与作废交给平台，由平台投递给订阅者

### `POST /deals`

把本应用的一笔成交、退款或作废告诉平台，由平台投递给订阅者

需要范围 `app.deals:write`。English: hands one deal event of the calling app (a sale, a refund, a void) to the platform, which queues it for the deal subscribers exactly as in process.

`source.app` 必须是调用的应用，否则 422。`key` 省略时按 `来源应用:来源引用:种类` 补上，写了就必须等于它；同一个事件（同一个 key）再来一次不会再排队。事件只带内部引用和标签，不带手机号、openid、邮箱、证件号等个人信息（看得出来的会被拒绝，422）。回答的 `queued` 是排给了几个订阅者（0 表示这个组织现在没有订阅者）。事件不是账：钱归经营记账。English: `queued` is how many subscribers the event was queued for.

- 认证：`bearer`
- 范围：`app.deals:write`
- operationId：`emitDeal`

**参数**

| 名称 | 位置 | 类型 | 必填 | 说明 |
|---|---|---|---|---|
| `Idempotency-Key` | header | string |  | 每个 POST 都收。SDK 未传入时会生成 uuid v4。24 小时内同键同正文重放第一次的回答<br>最短 1，最长 255 |

**请求正文（必填）**（`application/json`）

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `key` | string | 是 | 幂等键 |
| `kind` | string | 是 | sale、refund 或 void |
| `source` | DealSource | 是 | 成交从哪个应用来 |
| `source.app` | string | 是 | 应用 id |
| `source.ref` | string | 是 | k3:// 引用，退款指退款本身 |
| `customer` | string |  | 客户 id；匿名为空 |
| `amount_cents` | integer | 是 | 折后实付；退款为退回的金额 |
| `currency` | string |  | 空为租户的币种 |
| `lines` | DealLine[] |  |  |
| `lines[].sku` | string | 是 | 商品 id（对平台不透明） |
| `lines[].name` | string | 是 | 名称 |
| `lines[].qty` | integer | 是 | 数量 |
| `lines[].amount_cents` | integer | 是 | 金额 |
| `attribution` | DealAttribution | 是 | 成交从哪里来（只有内部引用和标签，没有个人信息） |
| `attribution.channel` | string |  | 渠道 |
| `attribution.campaign` | string |  | k3://campaign/&lt;id> |
| `attribution.site` | string |  | 站点 |
| `attribution.referrer` | string |  | 分销者的客户 id |
| `attribution.utm` | map[string, string] |  | UTM |
| `attribution.voucher` | string |  | 用了哪张券 |
| `reverses` | string |  | 退款或作废所冲销的销售的 key |
| `at` | string(date-time) | 是 | 成交发生的业务时间 |

**响应**

| 状态 | 说明 |
|---|---|
| 200 | 事件的键，以及排给了几个订阅者 |
| 401 | 见 [Unauthorized](#resp-unauthorized) |
| 403 | 见 [Forbidden](#resp-forbidden) |
| 422 | 字段或参数不被接受（正文读不出来、有未知字段、缺必填、取值越界）。见 [Unprocessable](#resp-unprocessable) |
| 429 | 见 [RateLimited](#resp-ratelimited) |
| 500 | 见 [Internal](#resp-internal) |
| 501 | `capability_off`：这个部署（或这个宿主）不提供这项能力，正文的 `capability` 是它的范围。见 [CapabilityOff](#resp-capabilityoff) |
| 503 | `unavailable`：暂时不可用（这个部署不提供的能力是 501 `capability_off`）。见 [Unavailable](#resp-unavailable) |

**200 响应正文**（`application/json`）

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `key` | string | 是 | 事件的键 |
| `queued` | integer | 是 | 排给了几个订阅者；0 表示这个组织现在没有订阅者 |

## org

### `GET /org/units`

企业的部门

需要范围 `app.directory:read`。English: the tenant's departments, for an app's own pickers and sharing rules. 没有部门时 `items` 是 `[]`。

- 认证：`bearer`
- 范围：`app.directory:read`
- operationId：`listOrgUnits`

**响应**

| 状态 | 说明 |
|---|---|
| 200 | 企业的部门 |
| 401 | 见 [Unauthorized](#resp-unauthorized) |
| 403 | 见 [Forbidden](#resp-forbidden) |
| 429 | 见 [RateLimited](#resp-ratelimited) |
| 500 | 见 [Internal](#resp-internal) |
| 501 | `capability_off`：这个部署（或这个宿主）不提供这项能力，正文的 `capability` 是它的范围。见 [CapabilityOff](#resp-capabilityoff) |
| 503 | `unavailable`：暂时不可用（这个部署不提供的能力是 501 `capability_off`）。见 [Unavailable](#resp-unavailable) |

**200 响应正文**（`application/json`）

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `items` | OrgUnit[] | 是 |  |
| `items[].id` | string | 是 |  |
| `items[].parentId` | string |  | 上级部门；根部门省略 |
| `items[].name` | string | 是 |  |
| `items[].path` | string | 是 | 从根到它的名字路径 |

### `GET /org/roles`

企业自己的角色

需要范围 `app.directory:read`。English: the tenant's own roles. 没有角色时 `items` 是 `[]`。

- 认证：`bearer`
- 范围：`app.directory:read`
- operationId：`listOrgRoles`

**响应**

| 状态 | 说明 |
|---|---|
| 200 | 企业自己的角色 |
| 401 | 见 [Unauthorized](#resp-unauthorized) |
| 403 | 见 [Forbidden](#resp-forbidden) |
| 429 | 见 [RateLimited](#resp-ratelimited) |
| 500 | 见 [Internal](#resp-internal) |
| 501 | `capability_off`：这个部署（或这个宿主）不提供这项能力，正文的 `capability` 是它的范围。见 [CapabilityOff](#resp-capabilityoff) |
| 503 | `unavailable`：暂时不可用（这个部署不提供的能力是 501 `capability_off`）。见 [Unavailable](#resp-unavailable) |

**200 响应正文**（`application/json`）

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `items` | OrgRole[] | 是 |  |
| `items[].id` | string | 是 |  |
| `items[].name` | string | 是 |  |

### `GET /org/placement`

一个人所在的部门和被指派的角色

需要范围 `app.directory:read`。English: the department chain of a person and the ids of the tenant roles assigned to them.

`units` 先是这个人自己的部门，再是逐级向上的父部门，直到根；然后是他被任命的各部门及其上级。没有部门或角色时是 `[]`。缺 `user` 是 422。

- 认证：`bearer`
- 范围：`app.directory:read`
- operationId：`getOrgPlacement`

**参数**

| 名称 | 位置 | 类型 | 必填 | 说明 |
|---|---|---|---|---|
| `user` | query | string | 是 | 人的 subject（成员的 userId） |

**响应**

| 状态 | 说明 |
|---|---|
| 200 | 部门链和角色 |
| 401 | 见 [Unauthorized](#resp-unauthorized) |
| 403 | 见 [Forbidden](#resp-forbidden) |
| 422 | 字段或参数不被接受（正文读不出来、有未知字段、缺必填、取值越界）。见 [Unprocessable](#resp-unprocessable) |
| 429 | 见 [RateLimited](#resp-ratelimited) |
| 500 | 见 [Internal](#resp-internal) |
| 501 | `capability_off`：这个部署（或这个宿主）不提供这项能力，正文的 `capability` 是它的范围。见 [CapabilityOff](#resp-capabilityoff) |
| 503 | `unavailable`：暂时不可用（这个部署不提供的能力是 501 `capability_off`）。见 [Unavailable](#resp-unavailable) |

**200 响应正文**（`application/json`）

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `units` | string[] | 是 | 部门 id：先是自己的部门，再逐级向上；然后是任命的部门及其上级 |
| `roles` | string[] | 是 | 被指派的角色 id |

### `GET /org/members`

调用者数据范围内的成员及其部门与角色（分页）

需要范围 `app.org:read`。按 `id` 排序分页列出本企业的在职成员（不含机器账号、未激活和已停用的成员）：`id`（账号 id）、`name`（显示名）、`units`（本人所属的部门和被任命的部门，不含上级）、`roles`（企业角色 id）。

人员按调用者的数据范围裁剪：`point` 必填，必须是本应用的权限点（以 `app.{本应用 id}.` 开头），否则 403 `forbidden`。平台按调用者在这个权限点上的数据范围回答：全部 → 本企业的全部成员；部门 → 所属或被任命的部门在范围内的成员；本人 → 调用者自己；调用者没有这个权限是 403 `forbidden`。组织没有启用权限模型（基础模式）时，企业管理员（`tenant.admin`）看到全部成员，其他人只看到自己。应用自己的调用（令牌背后没有人员）没有数据范围，拿不到成员列表：启用权限模型时是 403 `forbidden`，基础模式下是空列表（比 `app.directory:read` 的 `/members` 更严，有意如此）。`next` 是下一页的 cursor，最后一页是空字符串。`limit` 超出 1 到 500 是 422。平台没有接上权限求值、成员目录或组织架构时是 501 `capability_off`。English: the active members of the tenant (no machine accounts, no inactive or invited members), sorted by `id` and paged, cut to the caller's data scope on `point`, which must be one of this app's permissions (`app.{app id}.` prefix; another is 403 `forbidden`): all → every member; departments → members whose own or appointed department is in scope; self → the caller; no access → 403 `forbidden`. Basic mode (no permission model): a tenant administrator sees every member, anyone else only themselves. Each item has only `id`, `name`, `units` (own and appointed departments, no parents) and `roles` (tenant role ids). A `limit` outside 1 to 500 is 422; 501 `capability_off` when access, the directory or the organisation is not wired. The app's own call (no person behind the token) has no data scope and gets no member list: 403 `forbidden` with the permission model on, an empty list in basic mode (stricter than `/members` under `app.directory:read`, on purpose).

隐私规则：谁在哪个部门、担任什么角色是员工的个人数据，所以成员列表有自己的范围 `app.org:read`，与 `app.directory:read` 分开声明、分开审核；只回答令牌所属企业（租户）的数据，只含本企业的成员；按调用者的数据范围裁剪；分页；字段只有 id、显示名、部门和角色，**从不**包含联系方式（邮箱、手机号）或头像。English: privacy rules: who sits in which department with which role is personal data of the staff, so the member list has a scope of its own (`app.org:read`, declared and reviewed apart from `app.directory:read`), answers only the token's tenant and its own members, is cut to the caller's data scope and paged, and carries only ids, display names, departments and roles, never contact details (e-mail, phone) or a picture.

- 认证：`bearer`
- 范围：`app.org:read`
- operationId：`listOrgMembers`

**参数**

| 名称 | 位置 | 类型 | 必填 | 说明 |
|---|---|---|---|---|
| `point` | query | string | 是 | 本应用的权限点（以 `app.{本应用 id}.` 开头）；按调用者在它上面的数据范围裁剪成员 |
| `cursor` | query | string |  | 上一页的 `next`（不透明，原样传回）；省略为第一页 |
| `limit` | query | integer |  | 每页条数，1 到 500，默认 100<br>默认 `100`<br>最小 1，最大 500 |

**响应**

| 状态 | 说明 |
|---|---|
| 200 | 一页成员 |
| 401 | 见 [Unauthorized](#resp-unauthorized) |
| 403 | 见 [Forbidden](#resp-forbidden) |
| 422 | 字段或参数不被接受（正文读不出来、有未知字段、缺必填、取值越界）。见 [Unprocessable](#resp-unprocessable) |
| 429 | 见 [RateLimited](#resp-ratelimited) |
| 500 | 见 [Internal](#resp-internal) |
| 501 | `capability_off`：这个部署（或这个宿主）不提供这项能力，正文的 `capability` 是它的范围。见 [CapabilityOff](#resp-capabilityoff) |
| 503 | `unavailable`：暂时不可用（这个部署不提供的能力是 501 `capability_off`）。见 [Unavailable](#resp-unavailable) |

**200 响应正文**（`application/json`）

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `items` | OrgMember[] | 是 |  |
| `items[].id` | string | 是 | 账号 id |
| `items[].name` | string | 是 | 显示名 |
| `items[].units` | string[] | 是 | 所属的部门和被任命的部门（不含上级） |
| `items[].roles` | string[] | 是 | 企业角色 id |
| `next` | string | 是 | 下一页的 cursor；空字符串是到头了 |

## 事件（Webhooks）

平台向你登记的 https 地址推送的事件。每个事件是一个 `POST` 请求。

### `event:deal`

平台把一笔成交（销售、退款、作废）送给声明了 event:deal 服务的应用

触发器 `event:{kind}`，由平台的服务运行时投递到应用在清单 `services` 里声明的端点，至少一次。正文是 `{input, idempotencyKey, runId, trigger, attempt}`；`idempotencyKey` 也在请求头 `Idempotency-Key`，处理程序按它去重。请求头 `K3-Assertion` 是平台签的短时断言，处理前要验证。回答的状态含义：2xx 完成；423 加 `Retry-After` 表示还没到时候；其他 4xx（408、429 除外）拒绝、不再重试；其余重试。
`input` 是 `{caller, event}`：`caller` 是代表租户的调用者（字段名是 Go 的大写形式 Tenant、User、Name、Roles、Agent、Via），`event` 是成交事件。幂等键是 `deal:` 加事件的 `key`。退款或作废先于它所冲销的销售到达时，回 423 加 `Retry-After`（销售还没到）。

- operationId：`receiveDealEvent`

**参数**

| 名称 | 位置 | 类型 | 必填 | 说明 |
|---|---|---|---|---|
| `K3-Assertion` | header | string | 是 | 平台签发的短时断言（JWT），处理前要验证。同时带 Idempotency-Key（运行的键）、X-K3-Run（运行 id）、X-K3-Attempt（第几次） |
| `Idempotency-Key` | header | string |  | 每个 POST 都收。SDK 未传入时会生成 uuid v4。24 小时内同键同正文重放第一次的回答<br>最短 1，最长 255 |

**请求正文（必填）**（`application/json`）

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `input` | DealDeliveryInput | 是 | event:deal 的输入 |
| `input.caller` | DealCaller | 是 | 代表租户的调用者（Go 结构没有 json 标签，字段名是大写形式） |
| `input.caller.Tenant` | string | 是 | 租户 |
| `input.caller.User` | string |  | 用户 |
| `input.caller.Name` | string |  | 姓名 |
| `input.caller.Roles` | string[] |  | 角色 |
| `input.caller.Agent` | string |  | 平台代理 |
| `input.caller.Via` | string |  | 怎么发起的 |
| `input.event` | DealEvent | 是 | 一笔成交 |
| `input.event.key` | string | 是 | 幂等键 |
| `input.event.kind` | string | 是 | sale、refund 或 void |
| `input.event.source` | DealSource | 是 | 成交从哪个应用来 |
| `input.event.source.app` | string | 是 | 应用 id |
| `input.event.source.ref` | string | 是 | k3:// 引用，退款指退款本身 |
| `input.event.customer` | string |  | 客户 id；匿名为空 |
| `input.event.amount_cents` | integer | 是 | 折后实付；退款为退回的金额 |
| `input.event.currency` | string |  | 空为租户的币种 |
| `input.event.lines` | DealLine[] |  |  |
| `input.event.lines[].sku` | string | 是 | 商品 id（对平台不透明） |
| `input.event.lines[].name` | string | 是 | 名称 |
| `input.event.lines[].qty` | integer | 是 | 数量 |
| `input.event.lines[].amount_cents` | integer | 是 | 金额 |
| `input.event.attribution` | DealAttribution | 是 | 成交从哪里来（只有内部引用和标签，没有个人信息） |
| `input.event.attribution.channel` | string |  | 渠道 |
| `input.event.attribution.campaign` | string |  | k3://campaign/&lt;id> |
| `input.event.attribution.site` | string |  | 站点 |
| `input.event.attribution.referrer` | string |  | 分销者的客户 id |
| `input.event.attribution.utm` | map[string, string] |  | UTM |
| `input.event.attribution.voucher` | string |  | 用了哪张券 |
| `input.event.reverses` | string |  | 退款或作废所冲销的销售的 key |
| `input.event.at` | string(date-time) | 是 | 成交发生的业务时间 |
| `idempotencyKey` | string | 是 | 这次运行的键；同键是同一次运行 |
| `runId` | string | 是 | 运行 id |
| `trigger` | string | 是 | 触发器，例如 "event:deal" |
| `attempt` | integer | 是 | 第几次投递 |

**响应**

| 状态 | 说明 |
|---|---|
| 200 | 已处理。任何 2xx 都表示这次运行完成 |
| 422 | 其他 4xx（408、429 除外）：拒绝，这次运行不再重试，在管理员的运行列表里可见 |
| 423 | 还没到时候：平台过一会儿再送（头 Retry-After 的秒数，5 秒到 1 小时，默认 5 分钟），不用掉一次尝试，也不算服务失败；等满 24 小时仍是 423 则这次运行失败 |
| 500 | 5xx、408、429，或没有回答：按退避重试，最多 5 次 |

### `event:payment`

平台把一笔收款或退款结果送给声明了 event:payment 服务的应用

触发器 `event:{kind}`，由平台的服务运行时投递到应用在清单 `services` 里声明的端点，至少一次。正文是 `{input, idempotencyKey, runId, trigger, attempt}`；`idempotencyKey` 也在请求头 `Idempotency-Key`，处理程序按它去重。请求头 `K3-Assertion` 是平台签的短时断言，处理前要验证。回答的状态含义：2xx 完成；423 加 `Retry-After` 表示还没到时候；其他 4xx（408、429 除外）拒绝、不再重试；其余重试。
`input` 是 `{kind, order, refund}`：`kind` 为 `paid` 或 `refunded`，`refund` 只在退款时有。幂等键是 `paid:` 加订单 id，退款是 `refund:` 加退款 id。

- operationId：`receivePaymentEvent`

**参数**

| 名称 | 位置 | 类型 | 必填 | 说明 |
|---|---|---|---|---|
| `K3-Assertion` | header | string | 是 | 平台签发的短时断言（JWT），处理前要验证。同时带 Idempotency-Key（运行的键）、X-K3-Run（运行 id）、X-K3-Attempt（第几次） |
| `Idempotency-Key` | header | string |  | 每个 POST 都收。SDK 未传入时会生成 uuid v4。24 小时内同键同正文重放第一次的回答<br>最短 1，最长 255 |

**请求正文（必填）**（`application/json`）

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `input` | PaymentDeliveryInput | 是 | event:payment 的输入 |
| `input.kind` | string | 是 | paid 或 refunded<br>可选值：`paid`、`refunded` |
| `input.order` | PaymentOrder | 是 | 一张支付订单 |
| `input.order.tenant` | string | 是 | 租户 |
| `input.order.id` | string | 是 | 订单 id |
| `input.order.channel` | string | 是 | 渠道 |
| `input.order.product` | string | 是 | 产品 |
| `input.order.outTradeNo` | string | 是 | 商户单号 |
| `input.order.amountCents` | integer | 是 | 金额 |
| `input.order.currency` | string | 是 | 币种 |
| `input.order.subject` | string | 是 | 标题 |
| `input.order.status` | string | 是 | 状态 |
| `input.order.sourceApp` | string |  | 来源应用 |
| `input.order.sourceRef` | string |  | 来源引用 |
| `input.order.payToken` | string | 是 | 付款令牌 |
| `input.order.codeUrl` | string |  | 二维码地址 |
| `input.order.payUrl` | string |  | 支付地址 |
| `input.order.appId` | string |  | JSAPI 所用的 appid |
| `input.order.channelTradeNo` | string |  | 渠道单号 |
| `input.order.refundedCents` | integer | 是 | 已退 |
| `input.order.createdBy` | string |  | 创建者 |
| `input.order.settleAttempts` | integer | 是 | 结算尝试次数 |
| `input.order.expiresAt` | string(date-time) | 是 | 过期时间 |
| `input.order.paidAt` | string(date-time) |  | 支付时间 |
| `input.order.closedAt` | string(date-time) |  | 关闭时间 |
| `input.order.createdAt` | string(date-time) | 是 | 创建时间 |
| `input.order.settledAt` | string(date-time) |  | 结算时间 |
| `input.refund` | PaymentRefund |  | 一笔退款 |
| `input.refund.tenant` | string | 是 | 租户 |
| `input.refund.id` | string | 是 | 退款 id |
| `input.refund.orderId` | string | 是 | 订单 id |
| `input.refund.outRefundNo` | string | 是 | 商户退款单号 |
| `input.refund.amountCents` | integer | 是 | 金额 |
| `input.refund.reason` | string |  | 原因 |
| `input.refund.status` | string | 是 | 状态 |
| `input.refund.channelRefundId` | string |  | 渠道退款号 |
| `input.refund.createdBy` | string |  | 创建者 |
| `input.refund.settleAttempts` | integer | 是 | 结算尝试次数 |
| `input.refund.lastError` | string |  | 最近的错误 |
| `input.refund.createdAt` | string(date-time) | 是 | 创建时间 |
| `input.refund.settledAt` | string(date-time) |  | 结算时间 |
| `idempotencyKey` | string | 是 | 这次运行的键；同键是同一次运行 |
| `runId` | string | 是 | 运行 id |
| `trigger` | string | 是 | 触发器，例如 "event:deal" |
| `attempt` | integer | 是 | 第几次投递 |

**响应**

| 状态 | 说明 |
|---|---|
| 200 | 已处理。任何 2xx 都表示这次运行完成 |
| 422 | 其他 4xx（408、429 除外）：拒绝，这次运行不再重试，在管理员的运行列表里可见 |
| 423 | 还没到时候：平台过一会儿再送（头 Retry-After 的秒数，5 秒到 1 小时，默认 5 分钟），不用掉一次尝试，也不算服务失败；等满 24 小时仍是 423 则这次运行失败 |
| 500 | 5xx、408、429，或没有回答：按退避重试，最多 5 次 |

### `event:app.installed`

租户里的应用有变化（安装、升级、开关、卸载）

触发器 `event:{kind}`，由平台的服务运行时投递到应用在清单 `services` 里声明的端点，至少一次。正文是 `{input, idempotencyKey, runId, trigger, attempt}`；`idempotencyKey` 也在请求头 `Idempotency-Key`，处理程序按它去重。请求头 `K3-Assertion` 是平台签的短时断言，处理前要验证。回答的状态含义：2xx 完成；423 加 `Retry-After` 表示还没到时候；其他 4xx（408、429 除外）拒绝、不再重试；其余重试。
`input` 是 `{id, type, tenant, createdAt}`，`type` 为 `app.installed`；幂等键是 `id`。它只说“变了”，要看现状请重新读取。

- operationId：`receiveAppInstalledChange`

**参数**

| 名称 | 位置 | 类型 | 必填 | 说明 |
|---|---|---|---|---|
| `K3-Assertion` | header | string | 是 | 平台签发的短时断言（JWT），处理前要验证。同时带 Idempotency-Key（运行的键）、X-K3-Run（运行 id）、X-K3-Attempt（第几次） |
| `Idempotency-Key` | header | string |  | 每个 POST 都收。SDK 未传入时会生成 uuid v4。24 小时内同键同正文重放第一次的回答<br>最短 1，最长 255 |

**请求正文（必填）**（`application/json`）

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `input` | TenantChangeInput | 是 | 租户变化事件的输入 |
| `input.id` | string | 是 | 事件 id，也是运行的键 |
| `input.type` | string | 是 | app.installed 或 permissions.changed |
| `input.tenant` | string | 是 | 租户 |
| `input.createdAt` | string(date-time) | 是 | 发出时间 |
| `idempotencyKey` | string | 是 | 这次运行的键；同键是同一次运行 |
| `runId` | string | 是 | 运行 id |
| `trigger` | string | 是 | 触发器，例如 "event:deal" |
| `attempt` | integer | 是 | 第几次投递 |

**响应**

| 状态 | 说明 |
|---|---|
| 200 | 已处理。任何 2xx 都表示这次运行完成 |
| 422 | 其他 4xx（408、429 除外）：拒绝，这次运行不再重试，在管理员的运行列表里可见 |
| 423 | 还没到时候：平台过一会儿再送（头 Retry-After 的秒数，5 秒到 1 小时，默认 5 分钟），不用掉一次尝试，也不算服务失败；等满 24 小时仍是 423 则这次运行失败 |
| 500 | 5xx、408、429，或没有回答：按退避重试，最多 5 次 |

### `event:permissions.changed`

租户的部门、角色或分配有变化

触发器 `event:{kind}`，由平台的服务运行时投递到应用在清单 `services` 里声明的端点，至少一次。正文是 `{input, idempotencyKey, runId, trigger, attempt}`；`idempotencyKey` 也在请求头 `Idempotency-Key`，处理程序按它去重。请求头 `K3-Assertion` 是平台签的短时断言，处理前要验证。回答的状态含义：2xx 完成；423 加 `Retry-After` 表示还没到时候；其他 4xx（408、429 除外）拒绝、不再重试；其余重试。
`input` 同 `event:app.installed`，`type` 为 `permissions.changed`；幂等键是 `id`。它只说“变了”，要看现状请重新读取。

- operationId：`receivePermissionsChanged`

**参数**

| 名称 | 位置 | 类型 | 必填 | 说明 |
|---|---|---|---|---|
| `K3-Assertion` | header | string | 是 | 平台签发的短时断言（JWT），处理前要验证。同时带 Idempotency-Key（运行的键）、X-K3-Run（运行 id）、X-K3-Attempt（第几次） |
| `Idempotency-Key` | header | string |  | 每个 POST 都收。SDK 未传入时会生成 uuid v4。24 小时内同键同正文重放第一次的回答<br>最短 1，最长 255 |

**请求正文（必填）**（`application/json`）

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `input` | TenantChangeInput | 是 | 租户变化事件的输入 |
| `input.id` | string | 是 | 事件 id，也是运行的键 |
| `input.type` | string | 是 | app.installed 或 permissions.changed |
| `input.tenant` | string | 是 | 租户 |
| `input.createdAt` | string(date-time) | 是 | 发出时间 |
| `idempotencyKey` | string | 是 | 这次运行的键；同键是同一次运行 |
| `runId` | string | 是 | 运行 id |
| `trigger` | string | 是 | 触发器，例如 "event:deal" |
| `attempt` | integer | 是 | 第几次投递 |

**响应**

| 状态 | 说明 |
|---|---|
| 200 | 已处理。任何 2xx 都表示这次运行完成 |
| 422 | 其他 4xx（408、429 除外）：拒绝，这次运行不再重试，在管理员的运行列表里可见 |
| 423 | 还没到时候：平台过一会儿再送（头 Retry-After 的秒数，5 秒到 1 小时，默认 5 分钟），不用掉一次尝试，也不算服务失败；等满 24 小时仍是 423 则这次运行失败 |
| 500 | 5xx、408、429，或没有回答：按退避重试，最多 5 次 |

### `event:realtime.doc.projected`

本应用的一份实时文档投影完成了

触发器 `event:{kind}`，由平台的服务运行时投递到应用在清单 `services` 里声明的端点（应用不声明就收不到），至少一次。正文是 `{input, idempotencyKey, runId, trigger, attempt}`；`idempotencyKey` 也在请求头 `Idempotency-Key`，处理程序按它去重。请求头 `K3-Assertion` 是平台签的短时断言，处理前要验证。回答的状态含义同其他服务事件。
`input` 是 `{id, type, tenant, data}`，`type` 为 `realtime.doc.projected`，`data` 是 `{doc, kind, seq, actors}`：哪份文档、什么种类、投影在哪个序号、上次投影以来谁写过。要内容请再调用 `GET /realtime/docs/{id}/projection`（读的是现在的投影，可能比事件的序号新；按序号去重，旧的事件不要覆盖新的投影）。

- operationId：`receiveRealtimeDocProjected`

**参数**

| 名称 | 位置 | 类型 | 必填 | 说明 |
|---|---|---|---|---|
| `K3-Assertion` | header | string | 是 | 平台签发的短时断言（JWT），处理前要验证。同时带 Idempotency-Key（运行的键）、X-K3-Run（运行 id）、X-K3-Attempt（第几次） |
| `Idempotency-Key` | header | string |  | 每个 POST 都收。SDK 未传入时会生成 uuid v4。24 小时内同键同正文重放第一次的回答<br>最短 1，最长 255 |

**请求正文（必填）**（`application/json`）

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `input` | RealtimeProjectedInput | 是 | 事件 realtime.doc.projected 的输入 |
| `input.id` | string | 是 | 事件 id，也是运行的键 |
| `input.type` | string | 是 | realtime.doc.projected |
| `input.tenant` | string | 是 |  |
| `input.data` | object | 是 |  |
| `input.data.doc` | string | 是 | 文档 id |
| `input.data.kind` | string | 是 | 文档种类 |
| `input.data.seq` | integer(int64) | 是 | 投影所在的序号 |
| `input.data.actors` | string[] |  | 上次投影以来写过的人（日志里连接的身份，最后一个是最后写的人；服务器自己的编辑是 system） |
| `idempotencyKey` | string | 是 | 这次运行的键；同键是同一次运行 |
| `runId` | string | 是 | 运行 id |
| `trigger` | string | 是 | 触发器，例如 "event:deal" |
| `attempt` | integer | 是 | 第几次投递 |

**响应**

| 状态 | 说明 |
|---|---|
| 200 | 已处理。任何 2xx 都表示这次运行完成 |
| 422 | 其他 4xx（408、429 除外）：拒绝，这次运行不再重试，在管理员的运行列表里可见 |
| 423 | 还没到时候：平台过一会儿再送（头 Retry-After 的秒数，5 秒到 1 小时，默认 5 分钟），不用掉一次尝试，也不算服务失败；等满 24 小时仍是 423 则这次运行失败 |
| 500 | 5xx、408、429，或没有回答：按退避重试，最多 5 次 |

### `event:access.changed`

本应用的一个资源的访问名单变了

触发器 `event:{kind}`，由平台的服务运行时投递到应用在清单 `services` 里声明的端点（应用不声明就收不到），至少一次。正文是 `{input, idempotencyKey, runId, trigger, attempt}`；`idempotencyKey` 也在请求头 `Idempotency-Key`，处理程序按它去重。请求头 `K3-Assertion` 是平台签的短时断言，处理前要验证。回答的状态含义同其他服务事件。
`input` 是 `{id, type, tenant, data}`，`type` 为 `access.changed`，`data` 是 `{type, id}`：哪个资源。登记、删除、分享、撤销、通用访问、开关、转让、批准申请之后都会发。它只说“变了”；维护可见性镜像的应用据此用 `GET /access/{type}/visible` 刷新，最终一致。

- operationId：`receiveAccessChanged`

**参数**

| 名称 | 位置 | 类型 | 必填 | 说明 |
|---|---|---|---|---|
| `K3-Assertion` | header | string | 是 | 平台签发的短时断言（JWT），处理前要验证。同时带 Idempotency-Key（运行的键）、X-K3-Run（运行 id）、X-K3-Attempt（第几次） |
| `Idempotency-Key` | header | string |  | 每个 POST 都收。SDK 未传入时会生成 uuid v4。24 小时内同键同正文重放第一次的回答<br>最短 1，最长 255 |

**请求正文（必填）**（`application/json`）

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `input` | AccessChangedInput | 是 | 事件 access.changed 的输入 |
| `input.id` | string | 是 | 事件 id，也是运行的键 |
| `input.type` | string | 是 | access.changed |
| `input.tenant` | string | 是 |  |
| `input.data` | object | 是 |  |
| `input.data.type` | string | 是 | 资源类型 |
| `input.data.id` | string | 是 | 资源 id |
| `idempotencyKey` | string | 是 | 这次运行的键；同键是同一次运行 |
| `runId` | string | 是 | 运行 id |
| `trigger` | string | 是 | 触发器，例如 "event:deal" |
| `attempt` | integer | 是 | 第几次投递 |

**响应**

| 状态 | 说明 |
|---|---|
| 200 | 已处理。任何 2xx 都表示这次运行完成 |
| 422 | 其他 4xx（408、429 除外）：拒绝，这次运行不再重试，在管理员的运行列表里可见 |
| 423 | 还没到时候：平台过一会儿再送（头 Retry-After 的秒数，5 秒到 1 小时，默认 5 分钟），不用掉一次尝试，也不算服务失败；等满 24 小时仍是 423 则这次运行失败 |
| 500 | 5xx、408、429，或没有回答：按退避重试，最多 5 次 |

### `event:{app}.{what}`

另一个应用发出的事件（它在清单 events.emits 里声明、经 POST /events/emit 发出）

触发器 `event:{app}.{what}`：订阅方在清单 `services[]` 里写 `trigger: ["event:campaign.coupon.claimed"]` 这样的触发器，平台在发布方每次发出这个类型的事件时，对同一租户里装了并开着订阅方的服务启动一次运行，由服务运行时投递到声明的端点，至少一次。正文、断言、回答的含义同其他服务事件（`{input, idempotencyKey, runId, trigger, attempt}`，请求头 `K3-Assertion`、`Idempotency-Key`；2xx 完成，423 稍后再送，其他 4xx 拒绝，5xx / 408 / 429 / 没有回答按退避重试，最多 5 次）。
`input` 是 `{id, type, app, tenant, customer, data, createdAt}`：`app` 是发布方，`id` 也是幂等键（`appevt:{发布方}:{发布方的键}`），`customer` 是客户中心的 id。只有在清单 `events.emits` 里声明了这个类型的应用（类型以它的 id 开头）能发布它；发布方的新版本不再声明时，事件不再送出。这个触发器随订阅方的版本审核。

- operationId：`receiveAppEvent`

**参数**

| 名称 | 位置 | 类型 | 必填 | 说明 |
|---|---|---|---|---|
| `K3-Assertion` | header | string | 是 | 平台签发的短时断言（JWT），处理前要验证。同时带 Idempotency-Key（运行的键）、X-K3-Run（运行 id）、X-K3-Attempt（第几次） |
| `Idempotency-Key` | header | string |  | 每个 POST 都收。SDK 未传入时会生成 uuid v4。24 小时内同键同正文重放第一次的回答<br>最短 1，最长 255 |

**请求正文（必填）**（`application/json`）

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `input` | AppEventInput | 是 | 另一个应用发出的事件，作为订阅服务一次运行的输入 |
| `input.id` | string | 是 | 事件 id，也是运行的幂等键：appevt:{发布方}:{发布方的键} |
| `input.type` | string | 是 | 事件类型，{发布方 id}.{what} |
| `input.app` | string | 是 | 发布方应用 id |
| `input.tenant` | string | 是 | 租户 |
| `input.customer` | string |  | 事件涉及的客户（客户中心的 id），可为空 |
| `input.data` | object |  | 发布方给的事件内容（最多 16 KiB） |
| `input.createdAt` | string(date-time) | 是 | 平台收到事件的时间 |
| `idempotencyKey` | string | 是 | 这次运行的键；同键是同一次运行 |
| `runId` | string | 是 | 运行 id |
| `trigger` | string | 是 | 触发器，例如 "event:deal" |
| `attempt` | integer | 是 | 第几次投递 |

**响应**

| 状态 | 说明 |
|---|---|
| 200 | 已处理。任何 2xx 都表示这次运行完成 |
| 422 | 其他 4xx（408、429 除外）：拒绝，这次运行不再重试，在管理员的运行列表里可见 |
| 423 | 还没到时候：平台过一会儿再送（头 Retry-After 的秒数，5 秒到 1 小时，默认 5 分钟），不用掉一次尝试，也不算服务失败；等满 24 小时仍是 423 则这次运行失败 |
| 500 | 5xx、408、429，或没有回答：按退避重试，最多 5 次 |

### `event:customer.consent.changed`

本企业的一位客户撤回或给出了同意（退订、撤回、员工改动、客户被抹除）

触发器 `event:customer.consent.changed`：只投给清单 `uses` 里有 `app.customers:consent_events` 的应用（客户的选择是个人信息），且要在 `services[]` 里声明这个触发器。由服务运行时投递到声明的端点，至少一次；正文、断言、回答的含义同其他服务事件。
`input` 是 `{id, type, tenant, customer, channel, purpose, state, at}`：`customer` 是客户中心的 id，`channel` 为 sms、wechat_template、wecom 或 any，`purpose` 为 `service` 或 `marketing`，`state` 为 `granted` 或 `withdrawn`。没有手机号、姓名或同意的来源。幂等键是 `id`。
撤回同意时，平台在写入撤回的同一个事务里先取消这位客户在这个渠道、这个用途下排队未发的消息，再把变化写进发件箱；应用收到时，这些消息已经不会发出。客户被抹除时，每一项仍然有效的同意都各发一次撤回。

- operationId：`receiveConsentChanged`

**参数**

| 名称 | 位置 | 类型 | 必填 | 说明 |
|---|---|---|---|---|
| `K3-Assertion` | header | string | 是 | 平台签发的短时断言（JWT），处理前要验证。同时带 Idempotency-Key（运行的键）、X-K3-Run（运行 id）、X-K3-Attempt（第几次） |
| `Idempotency-Key` | header | string |  | 每个 POST 都收。SDK 未传入时会生成 uuid v4。24 小时内同键同正文重放第一次的回答<br>最短 1，最长 255 |

**请求正文（必填）**（`application/json`）

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `input` | ConsentChangeInput | 是 | 一位客户的同意变化，作为订阅服务一次运行的输入（只有客户 id、渠道、用途与状态） |
| `input.id` | string | 是 | 变化的 id，也是运行的幂等键 |
| `input.type` | string | 是 | customer.consent.changed |
| `input.tenant` | string | 是 | 租户 |
| `input.customer` | string | 是 | 客户中心的客户 id |
| `input.channel` | string | 是 | 渠道：sms、wechat_template、wecom，或 any（所有渠道） |
| `input.purpose` | string | 是 | 用途：service 或 marketing |
| `input.state` | string | 是 | 新的状态：granted 或 withdrawn |
| `input.at` | string(date-time) | 是 | 变化写入的时间 |
| `idempotencyKey` | string | 是 | 这次运行的键；同键是同一次运行 |
| `runId` | string | 是 | 运行 id |
| `trigger` | string | 是 | 触发器，例如 "event:deal" |
| `attempt` | integer | 是 | 第几次投递 |

**响应**

| 状态 | 说明 |
|---|---|
| 200 | 已处理。任何 2xx 都表示这次运行完成 |
| 422 | 其他 4xx（408、429 除外）：拒绝，这次运行不再重试，在管理员的运行列表里可见 |
| 423 | 还没到时候：平台过一会儿再送（头 Retry-After 的秒数，5 秒到 1 小时，默认 5 分钟），不用掉一次尝试，也不算服务失败；等满 24 小时仍是 423 则这次运行失败 |
| 500 | 5xx、408、429，或没有回答：按退避重试，最多 5 次 |

### `data:{kind}`

平台的数据事件（租户删除、站点删除、客户合并、本应用被卸载）送给在清单 events.receives 里声明了它的应用

数据事件不经服务运行时：应用在清单 `events.receives` 里写要收的种类（`tenant.deleted`、`site.deleted`、`customer.merged`、`app.uninstalled`），平台在同一个事务里为每个声明了它、且（除 `tenant.deleted` 外）装在这个租户里的应用记一行，再投递到 `POST /k3/events/{kind}`，直到应用回 2xx，至少一次，失败后按退避（30 秒起翻倍，最长 6 小时）一直重试，不放弃。同一个事件可能来不止一次，处理程序第二次发现没什么可做就直接回 204。进程内的应用与独立进程的应用走同一条路、收到同样的调用。
请求头 `K3-Assertion` 是平台签的短时断言（`via` 为 `event`，租户与正文的 `tenant` 相同），处理前要验证；raise-go 的 `appkit.Serve` 已经做了，并只把清单声明了的种类交给 `DataEventHandler`，没声明的回 404。
`subject` 随种类而定：`tenant.deleted` 是租户 id；`site.deleted` 是站点 id；`customer.merged` 是 `<被合并的客户 id>><保留的客户 id>`（`appkit.ParseMergedSubject`），应用把指向前者的记录挪到后者；`app.uninstalled` 是本应用自己的 id，只发给被卸载的这个应用，卸载不删数据（ADR-0028），留不留由应用决定，租户可能再装回来。随租户一起删除的安装只发 `tenant.deleted`。

- operationId：`receiveDataEvent`

**参数**

| 名称 | 位置 | 类型 | 必填 | 说明 |
|---|---|---|---|---|
| `kind` | path | string | 是 | 可选值：`tenant.deleted`、`site.deleted`、`customer.merged`、`app.uninstalled` |
| `K3-Assertion` | header | string | 是 | 平台签发的短时断言（JWT），处理前要验证。同时带 Idempotency-Key（运行的键）、X-K3-Run（运行 id）、X-K3-Attempt（第几次） |

**请求正文（必填）**（`application/json`）

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `tenant` | string | 是 | 租户 id，与断言里的租户相同 |
| `subject` | string | 是 | 事件说的是什么：租户 id、站点 id、&lt;被合并>>&lt;保留> 的客户 id，或本应用的 id（app.uninstalled） |
| `test` | boolean |  | true：开发者在测试企业里用 k3 trigger 发的测试投递，平台什么都没删、没合并、没卸载；真实事件不带这个字段 |

**响应**

| 状态 | 说明 |
|---|---|
| 204 | 已处理。任何 2xx 都表示完成 |
| 404 | 应用没有声明这个种类；平台不会为它记行 |
| 500 | 没处理好：按退避重试，不放弃 |

### `schedule`

平台的时钟按清单里声明的 cron 启动一次服务运行（触发器 schedule）

触发器 `schedule`：服务在清单 `services[]` 里声明 `trigger: ["schedule"]` 和 `schedule: {cron, timezone, missed}`。平台每 30 秒看一次到点的时间，对每个装了并开着本应用的租户各启动一次运行，由服务运行时投递到声明的端点，至少一次；平台多副本时每个时间只启动一次。正文、断言、回答的含义同其他服务事件（`{input, idempotencyKey, runId, trigger, attempt}`，请求头 `K3-Assertion`、`Idempotency-Key`；2xx 完成，423 稍后再送，其他 4xx 拒绝，5xx / 408 / 429 / 没有回答按退避重试，最多 5 次）。
`cron` 是五段（分 时 日 月 星期；数字、`*`、`a-b`、`/n`、逗号列表；星期 0 和 7 都是周日；日和星期都限定时满足其一即可），按 `timezone`（IANA 名称，默认 `Asia/Shanghai`）读。幂等键是 `schedule:` 加这个时间（UTC 的 RFC 3339）。`input` 是 `{scheduledAt, missed}`。
错过的时间（平台重启、故障、服务暂停）：`missed: "skip"`（默认）时晚了超过 5 分钟的时间不再启动，只记数；`missed: "once"` 时只把其中最近的一个补跑一次，`input.missed` 说折进了几个。服务暂停期间到点的时间不启动运行，服务页上看得到原因。新装、升级、卸载最迟 5 分钟后生效。每次投递都记进开发者的请求日志（方向 out），失败的也是一条脱敏错误。

- operationId：`receiveScheduledRun`

**参数**

| 名称 | 位置 | 类型 | 必填 | 说明 |
|---|---|---|---|---|
| `K3-Assertion` | header | string | 是 | 平台签发的短时断言（JWT），处理前要验证。同时带 Idempotency-Key（运行的键）、X-K3-Run（运行 id）、X-K3-Attempt（第几次） |
| `Idempotency-Key` | header | string |  | 每个 POST 都收。SDK 未传入时会生成 uuid v4。24 小时内同键同正文重放第一次的回答<br>最短 1，最长 255 |

**请求正文（必填）**（`application/json`）

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `input` | ScheduleDeliveryInput | 是 | 按清单的 schedule 启动的一次运行的输入 |
| `input.scheduledAt` | string(date-time) | 是 | 这次运行对应的时间（UTC）；幂等键是 schedule: 加它 |
| `input.missed` | integer |  | 之前没有启动的时间有几个（missed: once 时折进本次；skip 时已丢弃） |
| `idempotencyKey` | string | 是 | 这次运行的键；同键是同一次运行 |
| `runId` | string | 是 | 运行 id |
| `trigger` | string | 是 | 触发器，例如 "event:deal" |
| `attempt` | integer | 是 | 第几次投递 |

**响应**

| 状态 | 说明 |
|---|---|
| 200 | 已处理。任何 2xx 都表示这次运行完成 |
| 422 | 其他 4xx（408、429 除外）：拒绝，这次运行不再重试，在管理员的运行列表里可见 |
| 423 | 还没到时候：平台过一会儿再送（头 Retry-After 的秒数，5 秒到 1 小时，默认 5 分钟），不用掉一次尝试，也不算服务失败；等满 24 小时仍是 423 则这次运行失败 |
| 500 | 5xx、408、429，或没有回答：按退避重试，最多 5 次 |

## 通用响应

多个接口共用的响应。

### CapabilityOff

501 `capability_off`：这个部署（或这个宿主）不提供这项能力，正文的 `capability` 是它的范围。不会好转，不要重试：应用照常运行，服务端回自己的调用方 503 `capability_off`，界面隐藏相应的入口（Raise 宿主规范 §5，ADR-0146）。SDK 把它读成 `ErrCapabilityOff`。English：this deployment (or host) does not provide the capability; `capability` names its scope. Do not retry.

**正文**（`application/json`）

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `error` | string |  | 给人看的说明 |
| `code` | string | 是 | 可选值：`capability_off` |
| `capability` | string | 是 | 这项能力的范围，例如 app.customers:read |

### Conflict

409。`conflict`：和当前状态冲突。`idempotency_conflict`：同一 Idempotency-Key 不同正文。`idempotency_in_progress`：第一次还在处理，稍后再读或换新键。

**正文**（`application/json`）

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `error` | ErrorBody | 是 |  |
| `error.code` | string | 是 | 稳定错误码。用它分支，不要解析 message。`invalid` 是这个接口面上的处理程序对 422 回答的码。<br>可选值：`invalid_request`、`invalid_grant`、`consent_required`、`consent_revoked`、`person_gone`、`unauthorized`、`forbidden`、`not_found`、`method_not_allowed`、`conflict`、`payload_too_large`、`validation_failed`、`rate_limited`、`internal`、`not_implemented`、`capability_off`、`unavailable`、`token_required`、`invalid_token`、`invalid_credential`、`invalid_client`、`unsupported_grant_type`、`tenant_required`、`wrong_environment`、`credential_ended`、`address_not_allowed`、`not_installed`、`no_scope`、`scope_required`、`customer_token_required`、`assertion_stale`、`invalid_phone`、`ambiguous_customer`、`too_many_attempts`、`idempotency_conflict`、`idempotency_in_progress`、`condition_failed`、`invalid` |
| `error.message` | string | 是 | 给人看的说明 |
| `error.requestId` | string |  | 与 X-Request-Id 相同 |
| `error.docUrl` | string |  | 这条错误的文档，形如 https://dev.{domain}/docs/errors#scope_required |
| `error.details` | object |  | 没有内容时是 {} |

### DraftRefused

草稿服务的拒绝，状态由它定（400、403、404、409、422 等）。正文是 `{"error": "给人看的话", …}`，外加这次拒绝带的字段（`item`、`missing`、`confirm` 等），不是通用的错误信封。应用自己的令牌打只收页面会话的草稿接口是 403。

**正文**（`application/json`）

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `error` | string | 是 |  |

### Forbidden

403。`scope_required`：缺了操作描述里写的范围；范围必须在应用清单的 `uses` 里声明。`forbidden`：客户令牌不能调用这里；只收应用自己的令牌的操作遇到页面会话；或操作描述里写的归属检查没过（不是本应用的权限点、动作或事件类型，`sourceApp` 不是本应用）。`wrong_environment`：测试密钥打了正式组织，或正式密钥打了测试企业。`not_installed`：应用没装、关掉，或组织停用。`no_scope`：一把范围都没授。`address_not_allowed`：来源 IP 不在凭证允许名单。

**正文**（`application/json`）

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `error` | ErrorBody | 是 |  |
| `error.code` | string | 是 | 稳定错误码。用它分支，不要解析 message。`invalid` 是这个接口面上的处理程序对 422 回答的码。<br>可选值：`invalid_request`、`invalid_grant`、`consent_required`、`consent_revoked`、`person_gone`、`unauthorized`、`forbidden`、`not_found`、`method_not_allowed`、`conflict`、`payload_too_large`、`validation_failed`、`rate_limited`、`internal`、`not_implemented`、`capability_off`、`unavailable`、`token_required`、`invalid_token`、`invalid_credential`、`invalid_client`、`unsupported_grant_type`、`tenant_required`、`wrong_environment`、`credential_ended`、`address_not_allowed`、`not_installed`、`no_scope`、`scope_required`、`customer_token_required`、`assertion_stale`、`invalid_phone`、`ambiguous_customer`、`too_many_attempts`、`idempotency_conflict`、`idempotency_in_progress`、`condition_failed`、`invalid` |
| `error.message` | string | 是 | 给人看的说明 |
| `error.requestId` | string |  | 与 X-Request-Id 相同 |
| `error.docUrl` | string |  | 这条错误的文档，形如 https://dev.{domain}/docs/errors#scope_required |
| `error.details` | object |  | 没有内容时是 {} |

### Internal

500。平台出错，或依赖出了没有名字的错。带上 `X-Request-Id` 报给我们。

**正文**（`application/json`）

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `error` | ErrorBody | 是 |  |
| `error.code` | string | 是 | 稳定错误码。用它分支，不要解析 message。`invalid` 是这个接口面上的处理程序对 422 回答的码。<br>可选值：`invalid_request`、`invalid_grant`、`consent_required`、`consent_revoked`、`person_gone`、`unauthorized`、`forbidden`、`not_found`、`method_not_allowed`、`conflict`、`payload_too_large`、`validation_failed`、`rate_limited`、`internal`、`not_implemented`、`capability_off`、`unavailable`、`token_required`、`invalid_token`、`invalid_credential`、`invalid_client`、`unsupported_grant_type`、`tenant_required`、`wrong_environment`、`credential_ended`、`address_not_allowed`、`not_installed`、`no_scope`、`scope_required`、`customer_token_required`、`assertion_stale`、`invalid_phone`、`ambiguous_customer`、`too_many_attempts`、`idempotency_conflict`、`idempotency_in_progress`、`condition_failed`、`invalid` |
| `error.message` | string | 是 | 给人看的说明 |
| `error.requestId` | string |  | 与 X-Request-Id 相同 |
| `error.docUrl` | string |  | 这条错误的文档，形如 https://dev.{domain}/docs/errors#scope_required |
| `error.details` | object |  | 没有内容时是 {} |

### LengthRequired

411：没带 `Content-Length`。上传对象必须说明长度。正文是 {"error": "…"}。

**正文**（`application/json`）

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `error` | string |  |  |

### NotFound

404 `not_found`：对应的记录不存在（客户中心没有这个客户，访客、服务或所指的东西不存在）。

**正文**（`application/json`）

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `error` | ErrorBody | 是 |  |
| `error.code` | string | 是 | 稳定错误码。用它分支，不要解析 message。`invalid` 是这个接口面上的处理程序对 422 回答的码。<br>可选值：`invalid_request`、`invalid_grant`、`consent_required`、`consent_revoked`、`person_gone`、`unauthorized`、`forbidden`、`not_found`、`method_not_allowed`、`conflict`、`payload_too_large`、`validation_failed`、`rate_limited`、`internal`、`not_implemented`、`capability_off`、`unavailable`、`token_required`、`invalid_token`、`invalid_credential`、`invalid_client`、`unsupported_grant_type`、`tenant_required`、`wrong_environment`、`credential_ended`、`address_not_allowed`、`not_installed`、`no_scope`、`scope_required`、`customer_token_required`、`assertion_stale`、`invalid_phone`、`ambiguous_customer`、`too_many_attempts`、`idempotency_conflict`、`idempotency_in_progress`、`condition_failed`、`invalid` |
| `error.message` | string | 是 | 给人看的说明 |
| `error.requestId` | string |  | 与 X-Request-Id 相同 |
| `error.docUrl` | string |  | 这条错误的文档，形如 https://dev.{domain}/docs/errors#scope_required |
| `error.details` | object |  | 没有内容时是 {} |

### PayloadTooLarge

413 `payload_too_large`：正文太大。

**正文**（`application/json`）

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `error` | ErrorBody | 是 |  |
| `error.code` | string | 是 | 稳定错误码。用它分支，不要解析 message。`invalid` 是这个接口面上的处理程序对 422 回答的码。<br>可选值：`invalid_request`、`invalid_grant`、`consent_required`、`consent_revoked`、`person_gone`、`unauthorized`、`forbidden`、`not_found`、`method_not_allowed`、`conflict`、`payload_too_large`、`validation_failed`、`rate_limited`、`internal`、`not_implemented`、`capability_off`、`unavailable`、`token_required`、`invalid_token`、`invalid_credential`、`invalid_client`、`unsupported_grant_type`、`tenant_required`、`wrong_environment`、`credential_ended`、`address_not_allowed`、`not_installed`、`no_scope`、`scope_required`、`customer_token_required`、`assertion_stale`、`invalid_phone`、`ambiguous_customer`、`too_many_attempts`、`idempotency_conflict`、`idempotency_in_progress`、`condition_failed`、`invalid` |
| `error.message` | string | 是 | 给人看的说明 |
| `error.requestId` | string |  | 与 X-Request-Id 相同 |
| `error.docUrl` | string |  | 这条错误的文档，形如 https://dev.{domain}/docs/errors#scope_required |
| `error.details` | object |  | 没有内容时是 {} |

### RateLimited

429。`rate_limited`：这把凭证的调用预算用完，看 `Retry-After` 和 `X-RateLimit-*`。

**正文**（`application/json`）

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `error` | ErrorBody | 是 |  |
| `error.code` | string | 是 | 稳定错误码。用它分支，不要解析 message。`invalid` 是这个接口面上的处理程序对 422 回答的码。<br>可选值：`invalid_request`、`invalid_grant`、`consent_required`、`consent_revoked`、`person_gone`、`unauthorized`、`forbidden`、`not_found`、`method_not_allowed`、`conflict`、`payload_too_large`、`validation_failed`、`rate_limited`、`internal`、`not_implemented`、`capability_off`、`unavailable`、`token_required`、`invalid_token`、`invalid_credential`、`invalid_client`、`unsupported_grant_type`、`tenant_required`、`wrong_environment`、`credential_ended`、`address_not_allowed`、`not_installed`、`no_scope`、`scope_required`、`customer_token_required`、`assertion_stale`、`invalid_phone`、`ambiguous_customer`、`too_many_attempts`、`idempotency_conflict`、`idempotency_in_progress`、`condition_failed`、`invalid` |
| `error.message` | string | 是 | 给人看的说明 |
| `error.requestId` | string |  | 与 X-Request-Id 相同 |
| `error.docUrl` | string |  | 这条错误的文档，形如 https://dev.{domain}/docs/errors#scope_required |
| `error.details` | object |  | 没有内容时是 {} |

### Refused

422：服务拒绝，或字段不被接受。权益服务的拒绝是给人看的话（例如剩余 0 次不能核销）：`error.code` 是 `invalid`，`error.message` 是原话，`error.details.refused` 为 true；有多项权益时 `error.details.items` 逐项说明（`grant`、`name`、`reason`）。

**正文**（`application/json`）

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `error` | ErrorBody | 是 |  |
| `error.code` | string | 是 | 稳定错误码。用它分支，不要解析 message。`invalid` 是这个接口面上的处理程序对 422 回答的码。<br>可选值：`invalid_request`、`invalid_grant`、`consent_required`、`consent_revoked`、`person_gone`、`unauthorized`、`forbidden`、`not_found`、`method_not_allowed`、`conflict`、`payload_too_large`、`validation_failed`、`rate_limited`、`internal`、`not_implemented`、`capability_off`、`unavailable`、`token_required`、`invalid_token`、`invalid_credential`、`invalid_client`、`unsupported_grant_type`、`tenant_required`、`wrong_environment`、`credential_ended`、`address_not_allowed`、`not_installed`、`no_scope`、`scope_required`、`customer_token_required`、`assertion_stale`、`invalid_phone`、`ambiguous_customer`、`too_many_attempts`、`idempotency_conflict`、`idempotency_in_progress`、`condition_failed`、`invalid` |
| `error.message` | string | 是 | 给人看的说明 |
| `error.requestId` | string |  | 与 X-Request-Id 相同 |
| `error.docUrl` | string |  | 这条错误的文档，形如 https://dev.{domain}/docs/errors#scope_required |
| `error.details` | object |  | 没有内容时是 {} |

### Unauthorized

401。`token_required`：没带 Bearer，先 POST /open/v1/oauth/token。`invalid_token`：令牌无效或过期，重新换（SDK 会自动换一次再重试）。`credential_ended`：凭证吊销、过期，或应用关掉、范围被收光。

**正文**（`application/json`）

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `error` | ErrorBody | 是 |  |
| `error.code` | string | 是 | 稳定错误码。用它分支，不要解析 message。`invalid` 是这个接口面上的处理程序对 422 回答的码。<br>可选值：`invalid_request`、`invalid_grant`、`consent_required`、`consent_revoked`、`person_gone`、`unauthorized`、`forbidden`、`not_found`、`method_not_allowed`、`conflict`、`payload_too_large`、`validation_failed`、`rate_limited`、`internal`、`not_implemented`、`capability_off`、`unavailable`、`token_required`、`invalid_token`、`invalid_credential`、`invalid_client`、`unsupported_grant_type`、`tenant_required`、`wrong_environment`、`credential_ended`、`address_not_allowed`、`not_installed`、`no_scope`、`scope_required`、`customer_token_required`、`assertion_stale`、`invalid_phone`、`ambiguous_customer`、`too_many_attempts`、`idempotency_conflict`、`idempotency_in_progress`、`condition_failed`、`invalid` |
| `error.message` | string | 是 | 给人看的说明 |
| `error.requestId` | string |  | 与 X-Request-Id 相同 |
| `error.docUrl` | string |  | 这条错误的文档，形如 https://dev.{domain}/docs/errors#scope_required |
| `error.details` | object |  | 没有内容时是 {} |

### Unavailable

503 `unavailable`：暂时不可用（这个部署不提供的能力是 501 `capability_off`）。按 `Retry-After` 重试；没有该头时 SDK 用指数退避。

**正文**（`application/json`）

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `error` | ErrorBody | 是 |  |
| `error.code` | string | 是 | 稳定错误码。用它分支，不要解析 message。`invalid` 是这个接口面上的处理程序对 422 回答的码。<br>可选值：`invalid_request`、`invalid_grant`、`consent_required`、`consent_revoked`、`person_gone`、`unauthorized`、`forbidden`、`not_found`、`method_not_allowed`、`conflict`、`payload_too_large`、`validation_failed`、`rate_limited`、`internal`、`not_implemented`、`capability_off`、`unavailable`、`token_required`、`invalid_token`、`invalid_credential`、`invalid_client`、`unsupported_grant_type`、`tenant_required`、`wrong_environment`、`credential_ended`、`address_not_allowed`、`not_installed`、`no_scope`、`scope_required`、`customer_token_required`、`assertion_stale`、`invalid_phone`、`ambiguous_customer`、`too_many_attempts`、`idempotency_conflict`、`idempotency_in_progress`、`condition_failed`、`invalid` |
| `error.message` | string | 是 | 给人看的说明 |
| `error.requestId` | string |  | 与 X-Request-Id 相同 |
| `error.docUrl` | string |  | 这条错误的文档，形如 https://dev.{domain}/docs/errors#scope_required |
| `error.details` | object |  | 没有内容时是 {} |

### Unprocessable

422：字段或参数不被接受（正文读不出来、有未知字段、缺必填、取值越界）。这个接口面上的处理程序回答的 `code` 是 `invalid`。

**正文**（`application/json`）

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `error` | ErrorBody | 是 |  |
| `error.code` | string | 是 | 稳定错误码。用它分支，不要解析 message。`invalid` 是这个接口面上的处理程序对 422 回答的码。<br>可选值：`invalid_request`、`invalid_grant`、`consent_required`、`consent_revoked`、`person_gone`、`unauthorized`、`forbidden`、`not_found`、`method_not_allowed`、`conflict`、`payload_too_large`、`validation_failed`、`rate_limited`、`internal`、`not_implemented`、`capability_off`、`unavailable`、`token_required`、`invalid_token`、`invalid_credential`、`invalid_client`、`unsupported_grant_type`、`tenant_required`、`wrong_environment`、`credential_ended`、`address_not_allowed`、`not_installed`、`no_scope`、`scope_required`、`customer_token_required`、`assertion_stale`、`invalid_phone`、`ambiguous_customer`、`too_many_attempts`、`idempotency_conflict`、`idempotency_in_progress`、`condition_failed`、`invalid` |
| `error.message` | string | 是 | 给人看的说明 |
| `error.requestId` | string |  | 与 X-Request-Id 相同 |
| `error.docUrl` | string |  | 这条错误的文档，形如 https://dev.{domain}/docs/errors#scope_required |
| `error.details` | object |  | 没有内容时是 {} |
