# 日程与目标 API

> 日程和参与者、日历订阅，以及目标。

原文：https://qunli.co/docs/developers/api/schedules

由群历 REST API 的 OpenAPI 契约（`openapi/qunli-api.yaml`，qunli-api 提交 `f18e40d`）生成。路径都相对于 `https://qunli.co`；认证见[认证](https://qunli.co/docs/developers/authentication)，响应包装、分页和错误格式见[约定](https://qunli.co/docs/developers/conventions)。字段说明是契约里的英文原文。

## 日程

### `GET /api/v1/accounts/{account_id}/projects/{project_id}/schedules`

Active schedules of the project overlapping [from, to) — starts_at < to AND ends_at > from — ordered by starts_at (all-day first on ties). Omit both from and to for the current month in Asia/Shanghai. Not paginated: at most 1000 items; narrow the window for more. Items carry every field (same shape as GET /schedules/{id}).

参数：

| 参数 | 位置 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- | --- |
| `account_id` | 路径 | string | 是 | 团队的数字 id，和网页地址里的相同，例如 `2025`。 |
| `project_id` | 路径 | string | 是 |  |
| `content` | 查询 | `full` \| `summary` | 否 | full (default): rows carry their whole body. summary: rows leave out content_html, content_json, content_markdown and content_version and carry only preview_text / excerpt (documents: also preview_html); read one record for its body. Other values → 400 param_invalid. 默认 `full`。 |
| `from` | 查询 | string | 否 | Window start (inclusive). Date (YYYY-MM-DD) or ISO 8601 date-time; percent-encode a + offset as %2B (or use Z). Defaults with `to` to the current month. |
| `to` | 查询 | string | 否 | Window end (exclusive). Date or date-time; percent-encode a + offset as %2B. |

响应：

| 状态 | 说明 | 返回 |
| --- | --- | --- |
| `200` | Schedules overlapping the window | `data`: `Schedule`[] |
| `400` | Malformed request or invalid parameter. | `ProblemDetails` |
| `401` | Missing or invalid authentication. | `ProblemDetails` |
| `404` | Resource does not exist or has been trashed. | `ProblemDetails` |

### `POST /api/v1/accounts/{account_id}/projects/{project_id}/schedules`

Creates an active schedule. Timed events send schedule.starts_at (and optionally ends_at, default +1 hour); all-day events send all_day: true with starts_on (and optionally ends_on, inclusive). schedule.participants (member refs "U12" or user UUIDs) become participants: each is notified (reason invited) and auto-follows the schedule. The creator is not added as a participant.

参数：

| 参数 | 位置 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- | --- |
| `account_id` | 路径 | string | 是 | 团队的数字 id，和网页地址里的相同，例如 `2025`。 |
| `project_id` | 路径 | string | 是 |  |

请求体（`application/json`，必填）：

| 字段 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- |
| `edit_scope` | string | 否 | Update only. this (default) creates an exception; following edits the selected future instance and later instances, keeping earlier meetings and exceptions. Recurrence changes require following; recurrence null stops repeating after this instance. |
| `recording` | object | 否 |  |
| `recording.id` | string (uuid) | 否 |  |
| `recording.title` | string | 否 | 1–255 characters after trimming; required on create. |
| `recording.content_markdown` | string \| null | 否 | Agenda / minutes (GFM, ≤ 128KB). |
| `recording.content_html` | string \| null | 否 | 已弃用。 |
| `recording.content_json` | any | 否 | 已弃用。 |
| `recording.content_version` | integer \| null | 否 | Update only. The content_version the edit started from (recommended for every body edit). When present and the request changes the body, a stale value is refused with 409 content_conflict and nothing is written; the problem body carries the current content_version. Omit it (or send null) for last-write-wins. Title- or metadata-only updates never conflict. |
| `schedule` | object | 否 |  |
| `schedule.id` | string (uuid) | 否 |  |
| `schedule.all_day` | boolean | 否 | Explicit null or invalid values return 422 validation_error (all_day_invalid). |
| `schedule.starts_at` | string | 否 | ISO 8601 date-time with offset, e.g. 2026-10-20T14:00:00+08:00. |
| `schedule.ends_at` | string | 否 | Exclusive end; default starts_at + 1 hour on create. |
| `schedule.starts_on` | string | 否 | YYYY-MM-DD (all-day). |
| `schedule.ends_on` | string | 否 | YYYY-MM-DD, inclusive (all-day); default starts_on. |
| `schedule.time_zone` | string | 否 | IANA name; default Asia/Shanghai. |
| `schedule.location` | string \| null | 否 |  |
| `schedule.link` | string \| null | 否 | https:// URL, ≤ 2048 characters. |
| `schedule.recurrence` | `ScheduleRecurrence` | 否 | Calendar-date rule in the event timezone. Daily/weekly/monthly/yearly; invalid month-end or leap dates are skipped. until is inclusive, count includes the first occurrence. Specify at most one of until/count. Open-ended series materialize one year ahead and renew daily. On a schedule it is the rule the series still follows: once the series is stopped (PATCH edit_scope=following with recurrence null, or DELETE edit_scope=following), the meetings it kept report the shortened rule — count becomes the number of kept meetings (count and open-ended rules), until becomes the last kept meeting's date (until rules) — and null when no meeting of the series was kept. |
| `schedule.recurrence.freq` | string | 否 |  |
| `schedule.recurrence.interval` | integer | 否 | 1–365; defaults to 1. |
| `schedule.recurrence.by_weekday` | integer[] | 否 | Weekly only. 0=Sunday to 6=Saturday; must include the initial weekday. |
| `schedule.recurrence.until` | string \| null | 否 | Inclusive YYYY-MM-DD; not before the first occurrence. |
| `schedule.recurrence.count` | integer \| null | 否 | 1–1000 occurrences. |
| `schedule.participants_version` | string | 否 | Required on update when participants is present; use the token from the schedule snapshot. |
| `schedule.participants` | string[] | 否 | Member refs ("U12") or user UUIDs; replaces the list when present. Only [] clears it; null is invalid. |

响应：

| 状态 | 说明 | 返回 |
| --- | --- | --- |
| `201` | Created schedule | `data`: `Schedule` |
| `400` | Malformed request or invalid parameter. | `ProblemDetails` |
| `401` | Missing or invalid authentication. | `ProblemDetails` |
| `403` | Authenticated but not permitted. | `ProblemDetails` |
| `404` | Resource does not exist or has been trashed. | `ProblemDetails` |
| `422` | Validation failed. | `ProblemDetails` |

### `GET /api/v1/accounts/{account_id}/schedules`

The account calendar: schedules overlapping [from, to) across every project the caller can access. scope=mine (default) keeps schedules the caller created or participates in; scope=all keeps every accessible one. project_ids narrows to some projects (inaccessible ids are ignored). Same window rules, order and 1000-item cap as the project list.

参数：

| 参数 | 位置 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- | --- |
| `account_id` | 路径 | string | 是 | 团队的数字 id，和网页地址里的相同，例如 `2025`。 |
| `content` | 查询 | `full` \| `summary` | 否 | full (default): rows carry their whole body. summary: rows leave out content_html, content_json, content_markdown and content_version and carry only preview_text / excerpt (documents: also preview_html); read one record for its body. Other values → 400 param_invalid. 默认 `full`。 |
| `from` | 查询 | string | 否 | Window start (inclusive). Date (YYYY-MM-DD) or ISO 8601 date-time; percent-encode a + offset as %2B (or use Z). Defaults with `to` to the current month. |
| `to` | 查询 | string | 否 | Window end (exclusive). Date or date-time; percent-encode a + offset as %2B. |
| `scope` | 查询 | string | 否 | mine (default) = created by me or I participate; all = every accessible project; members = selected creators or participants (member_ids required). |
| `project_ids` | 查询 | string 或 string[] | 否 | Comma-separated project IDs or project_ids[]=12&project_ids[]=13. |
| `member_ids` | 查询 | string 或 string[] | 否 | Required for scope=members. 1–100 live account member refs (U12) or user UUIDs, comma-separated or array. OR across selected members; intersects project filters and caller access. |

响应：

| 状态 | 说明 | 返回 |
| --- | --- | --- |
| `200` | Schedules overlapping the window | `data`: `Schedule`[] |
| `400` | Malformed request or invalid parameter. | `ProblemDetails` |
| `401` | Missing or invalid authentication. | `ProblemDetails` |

### `GET /api/v1/accounts/{account_id}/schedules/{id}`

参数：

| 参数 | 位置 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- | --- |
| `account_id` | 路径 | string | 是 | 团队的数字 id，和网页地址里的相同，例如 `2025`。 |
| `id` | 路径 | string (ref) | 是 | 引用号，例如 `T1042`，大小写不敏感，可带前导 `#`。 |

响应：

| 状态 | 说明 | 返回 |
| --- | --- | --- |
| `200` | Schedule | `data`: `Schedule` |
| `401` | Missing or invalid authentication. | `ProblemDetails` |
| `404` | Resource does not exist or has been trashed. | `ProblemDetails` |

### `PATCH /api/v1/accounts/{account_id}/schedules/{id}`

Updates any subset of fields. Sending starts_at without ends_at (or starts_on without ends_on) shifts the event and keeps its duration; sending both sets both as given. Changing the time notifies participants and the creator (reason rescheduled), records a rescheduled activity and re-plans reminders. schedule.participants, when present, replaces the participant list (same as PUT .../participants). recording.content_version guards the body like the other recording updates: stale → 409 content_conflict (current content_version in the problem body), nothing written; only the selected instance's version is checked when edit_scope is following.

参数：

| 参数 | 位置 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- | --- |
| `account_id` | 路径 | string | 是 | 团队的数字 id，和网页地址里的相同，例如 `2025`。 |
| `id` | 路径 | string (ref) | 是 | 引用号，例如 `T1042`，大小写不敏感，可带前导 `#`。 |

请求体（`application/json`，必填）：

| 字段 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- |
| `edit_scope` | string | 否 | Update only. this (default) creates an exception; following edits the selected future instance and later instances, keeping earlier meetings and exceptions. Recurrence changes require following; recurrence null stops repeating after this instance. |
| `recording` | object | 否 |  |
| `recording.id` | string (uuid) | 否 |  |
| `recording.title` | string | 否 | 1–255 characters after trimming; required on create. |
| `recording.content_markdown` | string \| null | 否 | Agenda / minutes (GFM, ≤ 128KB). |
| `recording.content_html` | string \| null | 否 | 已弃用。 |
| `recording.content_json` | any | 否 | 已弃用。 |
| `recording.content_version` | integer \| null | 否 | Update only. The content_version the edit started from (recommended for every body edit). When present and the request changes the body, a stale value is refused with 409 content_conflict and nothing is written; the problem body carries the current content_version. Omit it (or send null) for last-write-wins. Title- or metadata-only updates never conflict. |
| `schedule` | object | 否 |  |
| `schedule.id` | string (uuid) | 否 |  |
| `schedule.all_day` | boolean | 否 | Explicit null or invalid values return 422 validation_error (all_day_invalid). |
| `schedule.starts_at` | string | 否 | ISO 8601 date-time with offset, e.g. 2026-10-20T14:00:00+08:00. |
| `schedule.ends_at` | string | 否 | Exclusive end; default starts_at + 1 hour on create. |
| `schedule.starts_on` | string | 否 | YYYY-MM-DD (all-day). |
| `schedule.ends_on` | string | 否 | YYYY-MM-DD, inclusive (all-day); default starts_on. |
| `schedule.time_zone` | string | 否 | IANA name; default Asia/Shanghai. |
| `schedule.location` | string \| null | 否 |  |
| `schedule.link` | string \| null | 否 | https:// URL, ≤ 2048 characters. |
| `schedule.recurrence` | `ScheduleRecurrence` | 否 | Calendar-date rule in the event timezone. Daily/weekly/monthly/yearly; invalid month-end or leap dates are skipped. until is inclusive, count includes the first occurrence. Specify at most one of until/count. Open-ended series materialize one year ahead and renew daily. On a schedule it is the rule the series still follows: once the series is stopped (PATCH edit_scope=following with recurrence null, or DELETE edit_scope=following), the meetings it kept report the shortened rule — count becomes the number of kept meetings (count and open-ended rules), until becomes the last kept meeting's date (until rules) — and null when no meeting of the series was kept. |
| `schedule.recurrence.freq` | string | 否 |  |
| `schedule.recurrence.interval` | integer | 否 | 1–365; defaults to 1. |
| `schedule.recurrence.by_weekday` | integer[] | 否 | Weekly only. 0=Sunday to 6=Saturday; must include the initial weekday. |
| `schedule.recurrence.until` | string \| null | 否 | Inclusive YYYY-MM-DD; not before the first occurrence. |
| `schedule.recurrence.count` | integer \| null | 否 | 1–1000 occurrences. |
| `schedule.participants_version` | string | 否 | Required on update when participants is present; use the token from the schedule snapshot. |
| `schedule.participants` | string[] | 否 | Member refs ("U12") or user UUIDs; replaces the list when present. Only [] clears it; null is invalid. |

响应：

| 状态 | 说明 | 返回 |
| --- | --- | --- |
| `200` | Updated schedule | `data`: `Schedule` |
| `400` | Malformed request or invalid parameter. | `ProblemDetails` |
| `401` | Missing or invalid authentication. | `ProblemDetails` |
| `403` | Authenticated but not permitted. | `ProblemDetails` |
| `404` | Resource does not exist or has been trashed. | `ProblemDetails` |
| `409` | State conflict. | `ProblemDetails` |
| `422` | Validation failed. | `ProblemDetails` |

### `DELETE /api/v1/accounts/{account_id}/schedules/{id}`

Cancel (trash) a schedule — notifies participants and the creator

Cancelling is trashing: the response is the schedule with status trashed; participants and the creator (except the caller) are notified with reason cancelled. Restore with PUT /recordings/{ref}/status/active.

参数：

| 参数 | 位置 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- | --- |
| `account_id` | 路径 | string | 是 | 团队的数字 id，和网页地址里的相同，例如 `2025`。 |
| `id` | 路径 | string (ref) | 是 | 引用号，例如 `T1042`，大小写不敏感，可带前导 `#`。 |
| `edit_scope` | 查询 | string | 否 | this (default) cancels one occurrence; following stops the series from this future occurrence, including its later exceptions. Restoring one meeting does not restart the series. |

响应：

| 状态 | 说明 | 返回 |
| --- | --- | --- |
| `200` | Trashed schedule | `data`: `Schedule` |
| `401` | Missing or invalid authentication. | `ProblemDetails` |
| `403` | Authenticated but not permitted. | `ProblemDetails` |
| `404` | Resource does not exist or has been trashed. | `ProblemDetails` |

### `PUT /api/v1/accounts/{account_id}/schedules/{schedule_id}/participants`

Replaces the participant list. members are member refs ("U12") or user UUIDs of people who can access the project; an empty array clears it. Newly added people are notified (reason invited) and follow the schedule; removed people stop following it (unless they created it).

参数：

| 参数 | 位置 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- | --- |
| `account_id` | 路径 | string | 是 | 团队的数字 id，和网页地址里的相同，例如 `2025`。 |
| `schedule_id` | 路径 | string (ref) | 是 | 引用号，例如 `T1042`，大小写不敏感，可带前导 `#`。 |

请求体（`application/json`，必填）：

| 字段 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- |
| `participants` | object | 否 | Required. |
| `participants.version` | string | 否 | Required. The participants_version from the opened schedule snapshot. |
| `participants.members` | string[] | 否 | Required. Member refs ("U12") or user UUIDs; [] clears the list. |

响应：

| 状态 | 说明 | 返回 |
| --- | --- | --- |
| `200` | The schedule with its new participants | `data`: `Schedule` |
| `400` | Malformed request or invalid parameter. | `ProblemDetails` |
| `401` | Missing or invalid authentication. | `ProblemDetails` |
| `403` | Authenticated but not permitted. | `ProblemDetails` |
| `404` | Resource does not exist or has been trashed. | `ProblemDetails` |
| `409` | State conflict. | `ProblemDetails` |
| `422` | Validation failed. | `ProblemDetails` |

### `GET /api/v1/accounts/{account_id}/calendar_feeds`

List only the current user's calendar subscriptions

参数：

| 参数 | 位置 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- | --- |
| `account_id` | 路径 | string | 是 | 团队的数字 id，和网页地址里的相同，例如 `2025`。 |

响应：

| 状态 | 说明 | 返回 |
| --- | --- | --- |
| `200` | Subscriptions (no raw access tokens) | `data`: `CalendarFeed`[] |

### `POST /api/v1/accounts/{account_id}/calendar_feeds`

Create a revocable read-only calendar subscription with saved filters

参数：

| 参数 | 位置 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- | --- |
| `account_id` | 路径 | string | 是 | 团队的数字 id，和网页地址里的相同，例如 `2025`。 |

请求体（`application/json`，必填）：

| 字段 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- |
| `calendar_feed` | object | 否 |  |
| `calendar_feed.name` | string | 否 |  |
| `calendar_feed.scope` | string | 否 |  |
| `calendar_feed.project_ids` | string[] | 否 |  |
| `calendar_feed.member_ids` | string[] | 否 |  |
| `calendar_feed.include_tasks` | boolean | 否 |  |

响应：

| 状态 | 说明 | 返回 |
| --- | --- | --- |
| `201` | Subscription with its one-time token URL | `data`: `CalendarFeed` |
| `400` | Malformed request or invalid parameter. | `ProblemDetails` |
| `422` | Validation failed. | `ProblemDetails` |

### `DELETE /api/v1/accounts/{account_id}/calendar_feeds/{id}`

Revoke a calendar subscription

参数：

| 参数 | 位置 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- | --- |
| `account_id` | 路径 | string | 是 | 团队的数字 id，和网页地址里的相同，例如 `2025`。 |
| `id` | 路径 | string | 是 |  |

响应：

| 状态 | 说明 | 返回 |
| --- | --- | --- |
| `200` | Revoked (`{ "data": null }`); the old subscription URL now answers 404. | `data`: null |
| `404` | Resource does not exist or has been trashed. | `ProblemDetails` |

### `POST /api/v1/accounts/{account_id}/calendar_feeds/{id}/rotate`

参数：

| 参数 | 位置 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- | --- |
| `account_id` | 路径 | string | 是 | 团队的数字 id，和网页地址里的相同，例如 `2025`。 |
| `id` | 路径 | string | 是 |  |

响应：

| 状态 | 说明 | 返回 |
| --- | --- | --- |
| `200` | New URL; previous token immediately invalidated | `data`: `CalendarFeed` |
| `404` | Resource does not exist or has been trashed. | `ProblemDetails` |

### `GET /api/v1/accounts/{account_id}/calendar_subscriptions/{id}.ics`

Read-only ICS authenticated by the URL token. Rechecks owner membership and project access on every request. Rolling past 90 days and next year; cancelled events retain stable UIDs. External clients choose refresh timing.

认证：无需认证。

参数：

| 参数 | 位置 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- | --- |
| `account_id` | 路径 | string | 是 | 团队的数字 id，和网页地址里的相同，例如 `2025`。 |
| `id` | 路径 | string | 是 |  |
| `token` | 查询 | string | 是 |  |

响应：

| 状态 | 说明 | 返回 |
| --- | --- | --- |
| `200` | RFC 5545 calendar | string |
| `404` | Resource does not exist or has been trashed. | `ProblemDetails` |

### `GET /api/v1/accounts/{account_id}/schedules/{schedule_id}/calendar.ics`

Download one meeting (requires normal session/API authentication and project access)

参数：

| 参数 | 位置 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- | --- |
| `account_id` | 路径 | string | 是 | 团队的数字 id，和网页地址里的相同，例如 `2025`。 |
| `schedule_id` | 路径 | string | 是 |  |

响应：

| 状态 | 说明 | 返回 |
| --- | --- | --- |
| `200` | RFC 5545 calendar containing one event | string |
| `404` | Resource does not exist or has been trashed. | `ProblemDetails` |

## 目标

### `GET /api/v1/accounts/{account_id}/goals`

参数：

| 参数 | 位置 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- | --- |
| `account_id` | 路径 | string | 是 | 团队的数字 id，和网页地址里的相同，例如 `2025`。 |
| `content` | 查询 | `full` \| `summary` | 否 | full (default): rows carry their whole body. summary: rows leave out content_html, content_json, content_markdown and content_version and carry only preview_text / excerpt (documents: also preview_html); read one record for its body. Other values → 400 param_invalid. 默认 `full`。 |

响应：

| 状态 | 说明 | 返回 |
| --- | --- | --- |
| `200` | Goal list | `data`: `Goal`[] |
| `401` | Missing or invalid authentication. | `ProblemDetails` |

### `GET /api/v1/accounts/{account_id}/goals/{id}`

参数：

| 参数 | 位置 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- | --- |
| `account_id` | 路径 | string | 是 | 团队的数字 id，和网页地址里的相同，例如 `2025`。 |
| `id` | 路径 | string (ref) | 是 | 引用号，例如 `T1042`，大小写不敏感，可带前导 `#`。 |

响应：

| 状态 | 说明 | 返回 |
| --- | --- | --- |
| `200` | Goal | `data`: `Goal` |
| `401` | Missing or invalid authentication. | `ProblemDetails` |
| `404` | Resource does not exist or has been trashed. | `ProblemDetails` |

### `PATCH /api/v1/accounts/{account_id}/goals/{id}`

参数：

| 参数 | 位置 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- | --- |
| `account_id` | 路径 | string | 是 | 团队的数字 id，和网页地址里的相同，例如 `2025`。 |
| `id` | 路径 | string (ref) | 是 | 引用号，例如 `T1042`，大小写不敏感，可带前导 `#`。 |

请求体（`application/json`，必填）：

| 字段 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- |
| `recording` | `RecordingFields` | 否 | The fields every recording type shares, sent under `recording`. `id` is create-only (a client-generated UUID). |
| `recording.id` | string (uuid) | 否 |  |
| `recording.title` | string | 否 |  |
| `recording.status` | string | 否 |  |
| `recording.content_html` | string \| null | 否 | 已弃用。 |
| `recording.content_json` | any | 否 | 已弃用。 |
| `recording.content_markdown` | string \| null | 否 | Canonical body (GFM). When present the server renders and sanitizes content_html from it, ignoring any client-sent content_html/content_json. Preferred over content_html and content_json for new integrations. Hard cap 131072 bytes (128KB, UTF-8 bytes — not characters); a larger body is rejected with 422 validation_error ("正文超出 128KB 上限"): upload long content as an attachment or split it into multiple documents. Pre-existing over-limit rows stay readable and editable as long as the body is untouched. |
| `recording.content_version` | integer \| null | 否 | Update only. The content_version the edit started from (recommended for every body edit). When present and the request changes the body, a stale value is refused with 409 content_conflict and nothing is written; the problem body carries the current content_version. Omit it (or send null) for last-write-wins. Title- or metadata-only updates never conflict. |
| `goal` | `GoalFields` | 否 | Goal fields (goals_controller#recordable_params). Whole numbers; target must be greater than starting. |
| `goal.id` | string (uuid) | 否 |  |
| `goal.starting` | integer | 否 |  |
| `goal.target` | integer | 否 |  |
| `goal.current` | integer | 否 |  |

响应：

| 状态 | 说明 | 返回 |
| --- | --- | --- |
| `200` | Updated goal | `data`: `Goal` |
| `401` | Missing or invalid authentication. | `ProblemDetails` |
| `403` | Authenticated but not permitted. | `ProblemDetails` |
| `404` | Resource does not exist or has been trashed. | `ProblemDetails` |
| `409` | content_conflict — the body was saved elsewhere since the request's recording.content_version; nothing was written. Rebase the edit onto the returned content_version (or reload) and save again. | `ProblemDetails` |
| `422` | Validation failed. | `ProblemDetails` |

### `DELETE /api/v1/accounts/{account_id}/goals/{id}`

Soft-delete a goal (returns the trashed recording)

参数：

| 参数 | 位置 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- | --- |
| `account_id` | 路径 | string | 是 | 团队的数字 id，和网页地址里的相同，例如 `2025`。 |
| `id` | 路径 | string (ref) | 是 | 引用号，例如 `T1042`，大小写不敏感，可带前导 `#`。 |

响应：

| 状态 | 说明 | 返回 |
| --- | --- | --- |
| `200` | Trashed goal | `data`: `Goal` |
| `401` | Missing or invalid authentication. | `ProblemDetails` |
| `403` | Authenticated but not permitted. | `ProblemDetails` |
| `404` | Resource does not exist or has been trashed. | `ProblemDetails` |

### `GET /api/v1/accounts/{account_id}/projects/{project_id}/goals`

参数：

| 参数 | 位置 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- | --- |
| `account_id` | 路径 | string | 是 | 团队的数字 id，和网页地址里的相同，例如 `2025`。 |
| `project_id` | 路径 | string | 是 |  |
| `content` | 查询 | `full` \| `summary` | 否 | full (default): rows carry their whole body. summary: rows leave out content_html, content_json, content_markdown and content_version and carry only preview_text / excerpt (documents: also preview_html); read one record for its body. Other values → 400 param_invalid. 默认 `full`。 |

响应：

| 状态 | 说明 | 返回 |
| --- | --- | --- |
| `200` | Goal list | `data`: `Goal`[] |
| `401` | Missing or invalid authentication. | `ProblemDetails` |

### `POST /api/v1/accounts/{account_id}/projects/{project_id}/goals`

参数：

| 参数 | 位置 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- | --- |
| `account_id` | 路径 | string | 是 | 团队的数字 id，和网页地址里的相同，例如 `2025`。 |
| `project_id` | 路径 | string | 是 |  |

请求体（`application/json`，必填）：

| 字段 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- |
| `recording` | `RecordingFields` | 否 | The fields every recording type shares, sent under `recording`. `id` is create-only (a client-generated UUID). |
| `recording.id` | string (uuid) | 否 |  |
| `recording.title` | string | 否 |  |
| `recording.status` | string | 否 |  |
| `recording.content_html` | string \| null | 否 | 已弃用。 |
| `recording.content_json` | any | 否 | 已弃用。 |
| `recording.content_markdown` | string \| null | 否 | Canonical body (GFM). When present the server renders and sanitizes content_html from it, ignoring any client-sent content_html/content_json. Preferred over content_html and content_json for new integrations. Hard cap 131072 bytes (128KB, UTF-8 bytes — not characters); a larger body is rejected with 422 validation_error ("正文超出 128KB 上限"): upload long content as an attachment or split it into multiple documents. Pre-existing over-limit rows stay readable and editable as long as the body is untouched. |
| `recording.content_version` | integer \| null | 否 | Update only. The content_version the edit started from (recommended for every body edit). When present and the request changes the body, a stale value is refused with 409 content_conflict and nothing is written; the problem body carries the current content_version. Omit it (or send null) for last-write-wins. Title- or metadata-only updates never conflict. |
| `goal` | `GoalFields` | 否 | Goal fields (goals_controller#recordable_params). Whole numbers; target must be greater than starting. |
| `goal.id` | string (uuid) | 否 |  |
| `goal.starting` | integer | 否 |  |
| `goal.target` | integer | 否 |  |
| `goal.current` | integer | 否 |  |

响应：

| 状态 | 说明 | 返回 |
| --- | --- | --- |
| `201` | Created goal | `data`: `Goal` |
| `401` | Missing or invalid authentication. | `ProblemDetails` |
| `403` | Authenticated but not permitted. | `ProblemDetails` |
| `422` | Validation failed. | `ProblemDetails` |

## 数据结构

上面的响应用到的对象。字段的类型是另一个对象时，它也列在这里。

### Schedule

| 字段 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- |
| `id` | string (uuid) | 是 |  |
| `public_id` | string \| null | 否 |  |
| `legacy_id` | string \| null | 是 |  |
| `title` | string | 是 |  |
| `content_html` | string \| null | 否 |  |
| `content_json` | object \| null | 否 |  |
| `content_markdown` | string \| null | 否 | Canonical body (GFM). content_html is rendered server-side from it; null on rows not yet migrated from Tiptap JSON. |
| `content_version` | integer | 否 | Body version for compare-and-set saves. Starts at 0 and increases by one each time content_markdown / content_html / content_json actually changes (any write path). Send it back as recording.content_version when saving a body edit. |
| `preview_text` | string | 否 | Lists only. The body as plain text (HTML tags removed, entities left encoded, e.g. "&amp;"), cut to 80 UTF-16 units; "" without a body. |
| `excerpt` | string \| null | 否 | Lists only. Up to 100 characters of the body's text, images and videos shown as [图片] / [视频]; null without a body. |
| `preview_html` | string | 否 | Document lists only. The first 3000 characters of content_html (may end inside a tag), for thumbnails. |
| `comments_count` | integer | 是 |  |
| `type` | `Schedule` | 是 |  |
| `status` | `draft` \| `active` \| `archived` \| `trashed` | 是 |  |
| `recordable_type` | `Schedule` | 是 |  |
| `app_url` | string \| null | 是 |  |
| `account_id` | string (uuid) | 是 |  |
| `account_public_id` | string | 是 |  |
| `project_id` | string (uuid) | 是 |  |
| `project_public_id` | string | 是 |  |
| `creator_id` | string (uuid) | 是 |  |
| `parent_id` | string (uuid) \| null | 否 |  |
| `parent_public_id` | string \| null | 否 |  |
| `ancestors` | `FolderCrumb`[] | 否 |  |
| `created_at` | string (date-time) | 是 |  |
| `updated_at` | string (date-time) | 是 | Last change of the recording or of its body, whichever is later. |
| `reactions` | `Reaction`[] | 是 |  |
| `edited_by` | `UserRef` | 否 | Comments and discussions only: the last member other than the author who edited the title or body (the author's own later edits keep it); null when only the author edited it. |
| `trashed_by` | `UserRef` | 否 | Comments and discussions only: the member other than the author who trashed it; null otherwise (cleared by the author's own trash and on restore). A comment with status "trashed" and a trashed_by is a placeholder in GET .../recordings/:id/comments — its title and content are empty. |
| `recordable_id` | string (uuid) | 是 | The schedules row id. |
| `starts_at` | string (date-time) | 是 | Start instant (UTC). All-day events start at 00:00 in time_zone. |
| `ends_at` | string (date-time) | 是 | End instant (UTC), EXCLUSIVE. |
| `all_day` | boolean | 是 |  |
| `time_zone` | string | 是 | IANA time zone the event is defined in. |
| `starts_on` | string (date) \| null | 是 | All-day only — first day (local); null for timed events. |
| `ends_on` | string (date) \| null | 是 | All-day only — last day (local), INCLUSIVE; null for timed events. |
| `location` | string \| null | 是 |  |
| `link` | string \| null | 是 | Meeting / live link (https). |
| `participants` | `UserRef`[] | 是 |  |
| `participant_ids` | string (uuid)[] | 是 | User UUIDs of participants, same order as participants. |
| `participants_version` | string | 是 | Opaque participant snapshot token for replacement writes. |
| `project` | `ProjectSummary` | 是 |  |
| `series_id` | string (uuid) \| null | 否 |  |
| `recurrence` | `ScheduleRecurrence` | 否 | Calendar-date rule in the event timezone. Daily/weekly/monthly/yearly; invalid month-end or leap dates are skipped. until is inclusive, count includes the first occurrence. Specify at most one of until/count. Open-ended series materialize one year ahead and renew daily. On a schedule it is the rule the series still follows: once the series is stopped (PATCH edit_scope=following with recurrence null, or DELETE edit_scope=following), the meetings it kept report the shortened rule — count becomes the number of kept meetings (count and open-ended rules), until becomes the last kept meeting's date (until rules) — and null when no meeting of the series was kept. |
| `detached` | boolean | 否 | This occurrence was individually changed or cancelled. |
| `occurrence_number` | integer \| null | 否 |  |

### CalendarFeed

| 字段 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- |
| `id` | string (uuid) | 是 |  |
| `name` | string | 是 |  |
| `scope` | `mine` \| `all` \| `members` | 是 |  |
| `project_ids` | string (uuid)[] | 是 |  |
| `member_ids` | string (uuid)[] | 是 |  |
| `include_tasks` | boolean | 是 |  |
| `created_at` | string (date-time) | 是 |  |
| `subscription_path` | string \| null | 是 | Token-bearing relative ICS URL; returned only on creation/rotation. Store privately; listing returns null. |

### Goal

| 字段 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- |
| `id` | string (uuid) | 是 |  |
| `public_id` | string \| null | 否 |  |
| `legacy_id` | string \| null | 是 |  |
| `title` | string | 是 |  |
| `content_html` | string \| null | 否 |  |
| `content_json` | object \| null | 否 |  |
| `content_markdown` | string \| null | 否 | Canonical body (GFM). content_html is rendered server-side from it; null on rows not yet migrated from Tiptap JSON. |
| `content_version` | integer | 否 | Body version for compare-and-set saves. Starts at 0 and increases by one each time content_markdown / content_html / content_json actually changes (any write path). Send it back as recording.content_version when saving a body edit. |
| `preview_text` | string | 否 | Lists only. The body as plain text (HTML tags removed, entities left encoded, e.g. "&amp;"), cut to 80 UTF-16 units; "" without a body. |
| `excerpt` | string \| null | 否 | Lists only. Up to 100 characters of the body's text, images and videos shown as [图片] / [视频]; null without a body. |
| `preview_html` | string | 否 | Document lists only. The first 3000 characters of content_html (may end inside a tag), for thumbnails. |
| `comments_count` | integer | 是 |  |
| `type` | string | 是 |  |
| `status` | `draft` \| `active` \| `archived` \| `trashed` | 是 |  |
| `recordable_type` | `Goal` | 是 |  |
| `app_url` | string \| null | 是 |  |
| `account_id` | string (uuid) | 是 |  |
| `account_public_id` | string | 是 |  |
| `project_id` | string (uuid) | 是 |  |
| `project_public_id` | string | 是 |  |
| `creator_id` | string (uuid) | 是 |  |
| `parent_id` | string (uuid) \| null | 否 |  |
| `parent_public_id` | string \| null | 否 |  |
| `ancestors` | `FolderCrumb`[] | 否 |  |
| `created_at` | string (date-time) | 是 |  |
| `updated_at` | string (date-time) | 是 | Last change of the recording or of its body, whichever is later. |
| `reactions` | `Reaction`[] | 是 |  |
| `edited_by` | `UserRef` | 否 | Comments and discussions only: the last member other than the author who edited the title or body (the author's own later edits keep it); null when only the author edited it. |
| `trashed_by` | `UserRef` | 否 | Comments and discussions only: the member other than the author who trashed it; null otherwise (cleared by the author's own trash and on restore). A comment with status "trashed" and a trashed_by is a placeholder in GET .../recordings/:id/comments — its title and content are empty. |
| `starting` | integer | 是 |  |
| `target` | integer | 是 |  |
| `current` | integer | 是 |  |

### FolderCrumb

| 字段 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- |
| `id` | string (uuid) | 是 |  |
| `public_id` | string \| null | 是 |  |
| `title` | string | 是 |  |

### Reaction

| 字段 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- |
| `id` | string (uuid) | 是 |  |
| `actor_id` | string (uuid) | 是 |  |
| `reactable_id` | string (uuid) | 是 |  |
| `reactable_type` | string | 是 |  |
| `reaction_type_id` | string (uuid) | 是 |  |
| `reaction_emoji` | string | 是 |  |
| `reaction_name` | string | 是 |  |
| `created_at` | string (date-time) | 是 |  |
| `updated_at` | string (date-time) | 是 |  |

### UserRef

| 字段 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- |
| `id` | string (uuid) | 是 | User UUID. |
| `public_id` | string \| null | 是 | Member ref ("U12") in this account. |
| `name` | string \| null | 是 |  |
| `avatar_url` | string \| null | 是 |  |
| `kind` | `human` \| `agent` \| null | 是 | null only for an unknown (deleted) user. |

### ProjectSummary

| 字段 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- |
| `id` | string (uuid) | 是 |  |
| `public_id` | string | 是 |  |
| `name` | string | 是 |  |
| `icon` | string \| null | 是 |  |

### ScheduleRecurrence

Calendar-date rule in the event timezone. Daily/weekly/monthly/yearly; invalid month-end or leap dates are skipped. until is inclusive, count includes the first occurrence. Specify at most one of until/count. Open-ended series materialize one year ahead and renew daily. On a schedule it is the rule the series still follows: once the series is stopped (PATCH edit_scope=following with recurrence null, or DELETE edit_scope=following), the meetings it kept report the shortened rule — count becomes the number of kept meetings (count and open-ended rules), until becomes the last kept meeting's date (until rules) — and null when no meeting of the series was kept.

| 字段 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- |
| `freq` | string | 否 |  |
| `interval` | integer | 否 | 1–365; defaults to 1. |
| `by_weekday` | integer[] | 否 | Weekly only. 0=Sunday to 6=Saturday; must include the initial weekday. |
| `until` | string \| null | 否 | Inclusive YYYY-MM-DD; not before the first occurrence. |
| `count` | integer \| null | 否 | 1–1000 occurrences. |
