# Plan · Projects

> Projects group boards and carry health, status notes, milestones, members and saved views. Part of the Dailybot Plan API (Beta).

Language: en
Canonical: https://www.dailybot.com/developers/api/plan-projects
Markdown: send header `Accept: text/markdown` on any URL to receive Markdown instead of HTML.
Last Updated: 2026-09-25

---

> **Beta** — Plan is in beta. Everything under `/plan` in the web app, the CLI and agent skill commands for projects, goals, boards and tasks, and the `/v1/plan/` public API may change before general availability. Want to try it with your team? Write to **support@dailybot.com**.

This page is the reference for **Plan · Projects**. Every endpoint lives under `https://api.dailybot.com/v1/plan/` and answers JSON.

Authenticate with a login session or a CLI user token (`Authorization: Bearer …`), or with an API key (`X-API-KEY`). A **personal API key** acts as its person and can do everything that person can do in Dailybot; an **agent or organization key** never acts as a person and is refused on the endpoints that need one. On an endpoint, the *API key* badge means an agent or organization key is accepted too. See [Authentication for Plan](/developers/plan/authentication), [Authentication](/developers/authentication) and [Errors](/developers/errors) for the rules shared by every Dailybot API.

New to Plan? Read the [overview](/developers/plan) for the model: projects, boards, workflow states, keys, ordering, versions and archive.

## Endpoints in this group

Projects group boards and carry health, status notes, milestones, members and saved views. Part of the Dailybot Plan API (Beta).

| Method | Endpoint | Description |
|--------|----------|-------------|
| GET | `/v1/plan/projects/` | List projects |
| POST | `/v1/plan/projects/` | Create a project |
| GET | `/v1/plan/projects/{project_id}/` | Retrieve a project |
| PATCH | `/v1/plan/projects/{project_id}/` | Update a project |
| GET | `/v1/plan/projects/updates/` | The newest updates across every project the caller can see |
| GET | `/v1/plan/projects/{project_id}/updates/` | Status notes on a project, newest first |
| POST | `/v1/plan/projects/{project_id}/updates/` | Post a status note |
| GET | `/v1/plan/projects/{project_id}/milestones/` | Dated commitments inside a project, in date order |
| POST | `/v1/plan/projects/{project_id}/milestones/` | Commit to a dated moment |
| PATCH | `/v1/plan/projects/{project_id}/milestones/{milestone_id}/` | Move or rename a milestone |
| DELETE | `/v1/plan/projects/{project_id}/milestones/{milestone_id}/` | Retire a milestone (archives; tasks keep pointing at it) |
| POST | `/v1/plan/projects/{project_id}/milestones/{milestone_id}/complete/` | Mark a milestone complete |
| POST | `/v1/plan/projects/{project_id}/milestones/{milestone_id}/reopen/` | Clear milestone completion |
| POST | `/v1/plan/projects/{project_id}/archive/` | Archive a project, cascading to its boards and their tasks |
| POST | `/v1/plan/projects/{project_id}/restore/` | Restore an archived project |
| GET | `/v1/plan/milestones/` | Every milestone the viewer may see, across projects |
| GET | `/v1/plan/projects/{project_id}/views/` | This person's saved views inside a project |
| PUT | `/v1/plan/projects/{project_id}/views/` | Replace this person's saved views for a project |
| GET | `/v1/plan/projects/{project_id}/members/` | Members of a project |
| POST | `/v1/plan/projects/{project_id}/members/` | Invite somebody, or a whole team, into a project |
| DELETE | `/v1/plan/projects/{project_id}/members/{user_id}/` | Remove somebody from a project |
| PATCH | `/v1/plan/projects/{project_id}/members/{user_id}/` | Inspect a project membership grant (role is read-only) |
| GET | `/v1/plan/projects/{project_id}/attachments/` | List a project's attachments |
| POST | `/v1/plan/projects/{project_id}/attachments/` | Upload an attachment to a project |
| GET | `/v1/plan/projects/{project_id}/attachments/{attachment_id}/content/` | Download a project attachment's bytes |
| DELETE | `/v1/plan/projects/{project_id}/attachments/{attachment_id}/` | Remove an attachment from a project |
| POST | `/v1/plan/projects/{project_id}/milestones/{milestone_id}/restore/` | Restore a retired milestone |
| GET | `/v1/plan/projects/{project_id}/milestones/{milestone_id}/attachments/` | List a milestone's attachments |
| POST | `/v1/plan/projects/{project_id}/milestones/{milestone_id}/attachments/` | Upload an attachment to a milestone |
| GET | `/v1/plan/projects/{project_id}/milestones/{milestone_id}/attachments/{attachment_id}/` | Retrieve a milestone attachment |
| PATCH | `/v1/plan/projects/{project_id}/milestones/{milestone_id}/attachments/{attachment_id}/` | Rename a milestone attachment |
| DELETE | `/v1/plan/projects/{project_id}/milestones/{milestone_id}/attachments/{attachment_id}/` | Remove a milestone attachment |
| GET | `/v1/plan/projects/{project_id}/milestones/{milestone_id}/attachments/{attachment_id}/content/` | Download a milestone attachment's bytes |
| GET | `/v1/plan/projects/{project_id}/updates/{update_id}/` | Get a project update |
| PATCH | `/v1/plan/projects/{project_id}/updates/{update_id}/` | Edit a project update |
| DELETE | `/v1/plan/projects/{project_id}/updates/{update_id}/` | Delete a project update |
| GET | `/v1/plan/projects/{project_id}/updates/{update_id}/attachments/` | List a update's attachments |
| POST | `/v1/plan/projects/{project_id}/updates/{update_id}/attachments/` | Upload an attachment to a update |
| GET | `/v1/plan/projects/{project_id}/updates/{update_id}/attachments/{attachment_id}/` | Retrieve a update attachment |
| PATCH | `/v1/plan/projects/{project_id}/updates/{update_id}/attachments/{attachment_id}/` | Rename a update attachment |
| DELETE | `/v1/plan/projects/{project_id}/updates/{update_id}/attachments/{attachment_id}/` | Remove a update attachment |
| GET | `/v1/plan/projects/{project_id}/updates/{update_id}/attachments/{attachment_id}/content/` | Download a update attachment's bytes |
| PATCH | `/v1/plan/projects/{project_id}/attachments/{attachment_id}/` | Rename a project attachment |
| GET | `/v1/plan/projects/{project_id}/updates/{update_id}/reactions/` | List who reacted to a project update |
| POST | `/v1/plan/projects/{project_id}/updates/{update_id}/reactions/` | Add an emoji reaction to a project update (idempotent) |
| DELETE | `/v1/plan/projects/{project_id}/updates/{update_id}/reactions/{emoji}/` | Remove the caller's emoji reaction from a project update |

### GET `/v1/plan/projects/` · Beta

**List projects**

The projects you can see, as a page. Search with `search`, filter by dates with `start_date` / `end_date`, and bring archived projects with `include_archived`. `include` adds optional blocks to each row.

- **Auth:** API key (`X-API-KEY`), CLI Bearer (read)
- **Rate limit:** `default`
- **Pagination:** Page-number pagination

#### Query parameters

##### Sorting & expansion

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `include` | string | No | Comma-separated roll-ups to embed. `progress` is the only token. Absent by default because it is an aggregate; when asked for, it is computed over the returned page. An unknown token is `400 invalid_filter_value`; an empty value is a no-op. |

##### Pagination

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `page` | integer | No | 1-based page number. |
| `page_size` | integer | No | Rows per page. Default 50, maximum 100. Out-of-range values are clamped, never rejected: asking for 500 returns 100. |
| `limit` | integer | No | Alias for `page_size`, translated server-side. |
| `offset` | integer | No | Alias translated to `page` server-side. |

##### Filters

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `search` | string | No | Matches title and key. Longer than 256 characters is `400 search_query_too_long`, not truncated. `q` is an alias. |

##### Archived rows

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `is_archived` | boolean | No | `true` returns only archived rows; `false` (the default) only live ones. Archive is the delete, so archived rows stay readable. |
| `include_archived` | boolean | No | Include archived rows alongside live ones. Distinct from `is_archived`, which selects one set or the other: `include_archived=true` is the union. Lists return live rows unless you opt in. |

##### Dates

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `start_date` | string | No | Created-at window start. What the CLI's `--since` produces. |
| `end_date` | string | No | Created-at window end. What the CLI's `--until` produces. |

#### Project object

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `uuid` | uuid | Yes | Stable public identifier. |
| `name` | string | Yes | Display name. Max 120 characters. |
| `slug` | string | No | URL-friendly name. Max 48 characters. |
| `description` | string | null | No | Free-form description. |
| `lead` | UserRef | null | No | The project's lead. See [UserRef](#plan-projects-list-userref). |
| `goals` | array | No | Goals this project points at. A project can serve several goals. Always present: `uuid`. Items: `{uuid, name}`. |
| `goal` | object | No | The goal, when there is exactly one. Shape: `{uuid, name}|null`. |
| `board_count` | integer | No | Number of live boards in the project. |
| `health` | enum | No | Declared health. One of `not_set`, `on_track`, `at_risk`, `off_track`. |
| `start_date` | date | null | No | Planned start date. |
| `target_date` | date | null | No | Planned end date. |
| `progress` | ProjectProgress | null | No | Progress roll-up over the tasks you can see. See [ProjectProgress](#plan-projects-list-projectprogress). |
| `is_archived` | boolean | Yes | Whether the row is archived. Archive is the delete: archived rows stay readable and restorable. |
| `archived_at` | date-time | null | No | When the row was archived. |
| `created_at` | date-time | No | When the row was created. |
| `updated_at` | date-time | No | When the row last changed. |
| `viewer` | object | No | What you can do with this row. Shape: `{can_see_content: boolean, can_manage: boolean} (both required)`. |

#### UserRef object

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `uuid` | uuid | Yes | Stable public identifier. |
| `name` | string | No | Display name. |
| `avatar_url` | string | null | No | — |
| `has_photo` | boolean | No | — |

#### ProjectProgress object

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `total` | integer | Yes | All tasks counted. |
| `completed` | integer | Yes | Tasks in a `done` or `canceled` state. |
| `open` | integer | No | Tasks in a `backlog`, `todo` or `in_progress` state. |
| `blocked` | integer | No | Tasks with a live blocker. |
| `overdue` | integer | No | Open tasks past their due date. |
| `percent_complete` | integer | Yes | `completed` as a percentage of `total`. |

#### Response body

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `count` | integer | Yes | Total number of rows. |
| `next` | uri | Yes | URL of the next page, or `null`. |
| `previous` | uri | Yes | URL of the previous page, or `null`. |
| `results` | array<Project> | Yes | The rows on this page. See [Project](#plan-projects-list-project). |

#### Error codes

| Status | When |
|--------|------|
| `401` | Missing, expired or malformed credential (`credential_absent`, `credential_expired`, `credential_malformed`). |
| `402` | Plan is not enabled for your organization yet (`plan_upgrade_required`). Expected during the Beta: write to support@dailybot.com. |
| `403` | Authenticated but not allowed: missing scope (`insufficient_scope`, which is also what an agent or organization key gets on an operation that needs a person, and what a personal key gets when its explicit Plan scopes do not cover the endpoint) or a guest account (`guest_not_allowed`). |
| `429` | Rate limit reached. Wait the number of seconds in `Retry-After`. |

#### Example (curl)

```bash
curl -sS "https://api.dailybot.com/v1/plan/projects/?include=progress" \
  -H "X-API-KEY: $DAILYBOT_API_KEY"
```

#### Scenario examples

##### CLI

```bash
dailybot plan project list --include progress --json
```

##### Response

```bash
{
  "count": 1,
  "next": null,
  "previous": null,
  "results": [
    {
      "uuid": "00000000-0000-4000-8000-000000000001",
      "name": "Platform",
      "slug": "platform",
      "description": null,
      "lead": {
        "uuid": "00000000-0000-4000-8000-00000000000c",
        "name": "Ada L."
      },
      "goals": [],
      "goal": {},
      "board_count": 1,
      "health": "on_track",
      "start_date": "2026-09-28",
      "target_date": "2026-10-15",
      "progress": {
        "total": 10,
        "completed": 4,
        "percent_complete": 40
      },
      "is_archived": false,
      "archived_at": null,
      "created_at": "2026-09-25T10:14:02Z",
      "updated_at": "2026-09-25T10:14:02Z",
      "viewer": {}
    }
  ]
}
```

#### Notes

- Scope: `tasks:read`.
- Rate limit: 120 reads per minute per actor.
- Works with a login session, a CLI user token, a personal API key, or an agent or organization key. A personal key sees what its person sees; an agent or organization key acts as a system actor and sees organization-visible boards only.

### POST `/v1/plan/projects/` · Beta

**Create a project**

Creates a project, the container that groups boards. Every non-guest member can call it (with a login session or a personal API key); an agent or organization key cannot. Send an `Idempotency-Key` to retry safely; the plan's project limit answers `402 task_projects_limit_reached`.

- **Auth:** CLI Bearer (write)
- **Rate limit:** `default`
- **Pagination:** No pagination

#### Headers

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `Idempotency-Key` | string | No | A key you generate for this intent. A replay with the same key and body returns the first response without a second side effect and carries `Idempotency-Replayed: true`. Keys are kept for 24 hours. The same key with a different body is `409 idempotency_key_payload_mismatch`; a repeat while the first call is still running gets `409 idempotency_in_progress` for up to 120 seconds. |
| `X-Dailybot-Agent-Name` | string | No | The name of the agent that executed this write on the person's behalf. Use it on multipart and body-less writes (DELETE, archive, restore); on JSON writes send the body field `agent_name` instead, which wins if both are present. Percent-encode the value as UTF-8. Control characters are stripped; a blank value means no agent. More than 128 characters, or a value that cannot be decoded, is `400 invalid_agent_attribution` (never truncated). An agent-type key, which is not bound to a person, gets `400 invalid_agent_attribution` if it sends it. The stamp never changes a permission answer. See [Agent attribution](/developers/plan/conventions#agent-attribution). |

#### Request body

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `visibility` | enum | No | `org` (everyone in the organization — shared workspace) or `members` (explicit grants only; invite with POST …/members/). Creating as `members` grants you. One of `org`, `members`. Default `org`. |
| `name` | string | Yes | Display name. Max 120 characters. |
| `description` | string | null | No | Free-form description. Max 2000 characters. |
| `lead` | uuid | null | No | The project's lead. |
| `health` | enum | No | Declared health. One of `not_set`, `on_track`, `at_risk`, `off_track`. |
| `start_date` | date | null | No | Planned start date. |
| `target_date` | date | null | No | Planned end date. |
| `agent_name` | string | No | The name of the agent that executed this write on the person's behalf (max 128 characters, blank means no agent). Takes priority over the `X-Dailybot-Agent-Name` header. See [Agent attribution](/developers/plan/conventions#agent-attribution). |

#### Response body

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `(body)` | Project | Yes | A [Project](#plan-projects-list-project) object. |

#### Error codes

| Status | When |
|--------|------|
| `400` | Validation failed, or a filter, sort or `include` value was not recognised. The response `code` says which. `invalid_agent_attribution` means the agent name is invalid. |
| `401` | Missing, expired or malformed credential (`credential_absent`, `credential_expired`, `credential_malformed`). |
| `402` | Plan is not enabled for your organization yet (`plan_upgrade_required`), or the plan's project ceiling is reached (`task_projects_limit_reached`). |
| `403` | Authenticated but not allowed: missing scope (`insufficient_scope`, which is also what an agent or organization key gets on an operation that needs a person, and what a personal key gets when its explicit Plan scopes do not cover the endpoint) or a guest account (`guest_not_allowed`). |
| `409` | Conflict. The response `code` says which (for example `version_conflict`). |
| `429` | Rate limit reached. Wait the number of seconds in `Retry-After`. |

#### Example (curl)

```bash
curl -sS -X POST "https://api.dailybot.com/v1/plan/projects/" \
  -H "Authorization: Bearer $DAILYBOT_TOKEN" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Q4 Roadmap",
    "visibility": "org",
    "health": "on_track",
    "target_date": "2026-12-18"
  }'
```

#### Scenario examples

##### CLI

```bash
dailybot plan project create -n "Q4 Roadmap" --target-date 2026-12-18
```

#### Notes

- Scope: `tasks:admin` — container writes. A non-guest member can call it with a login session or a personal API key (a key with explicit Plan scopes needs `tasks:write`, which covers it); an agent or organization key gets `403 insufficient_scope`.
- Rate limit: 60 writes per minute per actor.
- Needs a person: call it with a login session, a CLI user token or a personal API key. An agent or organization key gets `403 insufficient_scope`.

### GET `/v1/plan/projects/{project_id}/` · Beta

**Retrieve a project**

One project by uuid. A project you cannot see answers `404`, the same as one that does not exist.

- **Auth:** API key (`X-API-KEY`), CLI Bearer (read)
- **Rate limit:** `default`
- **Pagination:** No pagination

#### Path parameters

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `project_id` | string | Yes | The project's uuid. |

#### Response body

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `(body)` | Project | Yes | A [Project](#plan-projects-list-project) object. |

#### Error codes

| Status | When |
|--------|------|
| `401` | Missing, expired or malformed credential (`credential_absent`, `credential_expired`, `credential_malformed`). |
| `402` | Plan is not enabled for your organization yet (`plan_upgrade_required`). Expected during the Beta: write to support@dailybot.com. |
| `404` | Not found, or not visible to you. Both cases return the same body. |

#### Example (curl)

```bash
curl -sS "https://api.dailybot.com/v1/plan/projects/00000000-0000-4000-8000-000000000001/" \
  -H "X-API-KEY: $DAILYBOT_API_KEY"
```

#### Scenario examples

##### CLI

```bash
dailybot plan project get 00000000-0000-4000-8000-000000000001 --include progress
```

#### Notes

- Scope: `tasks:read`.
- Rate limit: 120 reads per minute per actor.
- Works with a login session, a CLI user token, a personal API key, or an agent or organization key. A personal key sees what its person sees; an agent or organization key acts as a system actor and sees organization-visible boards only.

### PATCH `/v1/plan/projects/{project_id}/` · Beta

**Update a project**

Changes a project's fields. Send only the fields you change. Every non-guest member can call it (with a login session or a personal API key); an agent or organization key cannot. Setting `visibility` to `members` privatizes the project and auto-grants the actor who privatizes.

- **Auth:** CLI Bearer (write)
- **Rate limit:** `default`
- **Pagination:** No pagination

#### Path parameters

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `project_id` | string | Yes | The project's uuid. |

#### Headers

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `Idempotency-Key` | string | No | A key you generate for this intent. A replay with the same key and body returns the first response without a second side effect and carries `Idempotency-Replayed: true`. Keys are kept for 24 hours. The same key with a different body is `409 idempotency_key_payload_mismatch`; a repeat while the first call is still running gets `409 idempotency_in_progress` for up to 120 seconds. |
| `X-Dailybot-Agent-Name` | string | No | The name of the agent that executed this write on the person's behalf. Use it on multipart and body-less writes (DELETE, archive, restore); on JSON writes send the body field `agent_name` instead, which wins if both are present. Percent-encode the value as UTF-8. Control characters are stripped; a blank value means no agent. More than 128 characters, or a value that cannot be decoded, is `400 invalid_agent_attribution` (never truncated). An agent-type key, which is not bound to a person, gets `400 invalid_agent_attribution` if it sends it. The stamp never changes a permission answer. See [Agent attribution](/developers/plan/conventions#agent-attribution). |

#### Request body

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `visibility` | enum | No | `org` (everyone in the organization) or `members` (explicit members only). Changing `org` → `members` auto-grants the actor who privatizes. One of `org`, `members`. |
| `name` | string | No | Display name. Max 120 characters. |
| `description` | string | null | No | Free-form description. Max 2000 characters. |
| `lead` | uuid | null | No | The project's lead. |
| `health` | enum | No | Declared health. One of `not_set`, `on_track`, `at_risk`, `off_track`. |
| `start_date` | date | null | No | Planned start date. |
| `target_date` | date | null | No | Planned end date. |
| `agent_name` | string | No | The name of the agent that executed this write on the person's behalf (max 128 characters, blank means no agent). Takes priority over the `X-Dailybot-Agent-Name` header. See [Agent attribution](/developers/plan/conventions#agent-attribution). |

#### Response body

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `(body)` | Project | Yes | A [Project](#plan-projects-list-project) object. |

#### Error codes

| Status | When |
|--------|------|
| `400` | Validation failed, or a filter, sort or `include` value was not recognised. The response `code` says which. `invalid_agent_attribution` means the agent name is invalid. |
| `401` | Missing, expired or malformed credential (`credential_absent`, `credential_expired`, `credential_malformed`). |
| `402` | Plan is not enabled for your organization yet (`plan_upgrade_required`). Expected during the Beta: write to support@dailybot.com. |
| `403` | Authenticated but not allowed: missing scope (`insufficient_scope`, which is also what an agent or organization key gets on an operation that needs a person, and what a personal key gets when its explicit Plan scopes do not cover the endpoint) or a guest account (`guest_not_allowed`). |
| `404` | Not found, or not visible to you. Both cases return the same body. |

#### Example (curl)

```bash
curl -sS -X PATCH "https://api.dailybot.com/v1/plan/projects/00000000-0000-4000-8000-000000000001/" \
  -H "Authorization: Bearer $DAILYBOT_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "health": "at_risk"
  }'
```

#### Scenario examples

##### CLI

```bash
dailybot plan project update 00000000-0000-4000-8000-000000000001 --health at_risk
```

#### Notes

- Scope: `tasks:admin` — container writes. A non-guest member can call it with a login session or a personal API key (a key with explicit Plan scopes needs `tasks:write`, which covers it); an agent or organization key gets `403 insufficient_scope`.
- Rate limit: 60 writes per minute per actor.
- Needs a person: call it with a login session, a CLI user token or a personal API key. An agent or organization key gets `403 insufficient_scope`.

### GET `/v1/plan/projects/updates/` · Beta

**The newest updates across every project the caller can see**

The batched form of the per-project updates list, for a home screen that would otherwise call it once per project.

Returns the newest `per_project` updates for each visible project as one flat, paginated list; each row carries its `project`, so group by that field. Ordered by project name, then newest first. It is a teaser, not a history: for a full thread or an archived project, use `GET /v1/plan/projects/{project_id}/updates/`.

`projects` narrows to named projects. A project you cannot see is **silently omitted** rather than refused, and archived projects are excluded even when named. `body_html` is rendered and sanitized server-side.

- **Auth:** API key (`X-API-KEY`), CLI Bearer (read)
- **Rate limit:** `default`
- **Pagination:** Page-number pagination

#### Query parameters

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `page` | integer | No | 1-based page number. |
| `page_size` | integer | No | Rows per page. Default 50, maximum 100. Out-of-range values are clamped, never rejected: asking for 500 returns 100. |
| `projects` | array | No | Project uuids to narrow to. Repeatable; values are OR-ed. A project the caller cannot see, or one that is archived, contributes nothing rather than raising. More than the published maximum is `400 too_many_filter_values`. |
| `per_project` | integer | No | How many updates each project contributes. Clamped to the published maximum rather than refused - this is a teaser size, not an identifier, and a home screen asking for too many should get a full page rather than an error. |

#### ProjectUpdate object

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `uuid` | uuid | Yes | Stable public identifier. |
| `project` | uuid | null | Yes | The project. |
| `body` | string | Yes | Markdown as typed. Mention someone with `<@DB@{uuid}>`, using a `uuid` from the mentionables list. |
| `body_html` | string | Yes | `body` rendered and sanitized by the server. Client HTML is never accepted. |
| `mentions` | array<ActorRef> | No | The people mentioned. Read them from here, never by parsing `body`. See [ActorRef](#plan-projects-updates-digest-actorref). |
| `health` | enum | null | No | Declared health. One of `not_set`, `on_track`, `at_risk`, `off_track`. |
| `created_by` | ActorRef | null | No | Who created the row. See [ActorRef](#plan-projects-updates-digest-actorref). |
| `created_at` | date-time | Yes | When the row was created. |
| `updated_at` | date-time | No | When the row last changed. |
| `executed_by_agent` | object | null | No | The agent that executed this person's post, beside `created_by`, or `null` for a plain human post: `{uuid, name, username, avatar}`. |
| `provenance` | enum | No | How the text reached us: `typed`, `agent_authored` or `retrieved`. A note posted through an API key, or stamped with an agent, is `agent_authored`. |
| `edited_at` | date-time | null | No | Null until the first edit. |
| `attachments` | array<TaskAttachment> | No | READY attachments, ordered by position, inline. Upload them with `POST …/updates/{update_id}/attachments/`. |

#### ActorRef object

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `kind` | string | Yes | — |
| `uuid` | string | Yes | Stable public identifier. |
| `name` | string | No | Display name. |
| `username` | string | null | No | — |
| `avatar_url` | string | null | No | — |
| `has_photo` | boolean | No | — |

#### Response body

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `count` | integer | Yes | Total number of rows. |
| `next` | uri | Yes | URL of the next page, or `null`. |
| `previous` | uri | Yes | URL of the previous page, or `null`. |
| `results` | array<ProjectUpdate> | Yes | The rows on this page. See [ProjectUpdate](#plan-projects-updates-digest-projectupdate). |

#### Error codes

| Status | When |
|--------|------|
| `400` | Validation failed, or a filter, sort or `include` value was not recognised. The response `code` says which. |
| `401` | Missing, expired or malformed credential (`credential_absent`, `credential_expired`, `credential_malformed`). |
| `402` | Plan is not enabled for your organization yet (`plan_upgrade_required`). Expected during the Beta: write to support@dailybot.com. |

#### Example (curl)

```bash
curl -sS "https://api.dailybot.com/v1/plan/projects/updates/" \
  -H "X-API-KEY: $DAILYBOT_API_KEY"
```

#### Scenario examples

##### CLI

```bash
dailybot plan project updates --json
```

##### Response

```bash
{
  "count": 1,
  "next": null,
  "previous": null,
  "results": [
    {
      "uuid": "00000000-0000-4000-8000-00000000000e",
      "project": "00000000-0000-4000-8000-000000000001",
      "body": "Staging is green; rolling out Friday.",
      "body_html": "<p>Staging is green; rolling out Friday.</p>",
      "mentions": [],
      "health": "on_track",
      "created_by": {
        "kind": "user",
        "uuid": "00000000-0000-4000-8000-00000000000c",
        "name": "Ada L."
      },
      "created_at": "2026-09-25T10:14:02Z",
      "updated_at": "2026-09-25T10:14:02Z"
    }
  ]
}
```

#### Notes

- Scope: `tasks:read`.
- Rate limit: 120 reads per minute per actor.
- Works with a login session, a CLI user token, a personal API key, or an agent or organization key. A personal key sees what its person sees; an agent or organization key acts as a system actor and sees organization-visible boards only.

### GET `/v1/plan/projects/{project_id}/updates/` · Beta

**Status notes on a project, newest first**

The narrative half of a roadmap: why the health is what it is, with a name and a date on it. `body` is the markdown as typed; `body_html` is rendered server-side through the same sanitizer comments use, so no client-supplied HTML is ever trusted.

- **Auth:** API key (`X-API-KEY`), CLI Bearer (read)
- **Rate limit:** `default`
- **Pagination:** Page-number pagination

#### Path parameters

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `project_id` | string | Yes | The project's uuid. |

#### Query parameters

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `page` | integer | No | 1-based page number. |
| `page_size` | integer | No | Rows per page. Default 50, maximum 100. Out-of-range values are clamped, never rejected: asking for 500 returns 100. |

#### Response body

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `count` | integer | Yes | Total number of rows. |
| `next` | uri | Yes | URL of the next page, or `null`. |
| `previous` | uri | Yes | URL of the previous page, or `null`. |
| `results` | array<ProjectUpdate> | Yes | The rows on this page. See [ProjectUpdate](#plan-projects-updates-digest-projectupdate). |

#### Error codes

| Status | When |
|--------|------|
| `401` | Missing, expired or malformed credential (`credential_absent`, `credential_expired`, `credential_malformed`). |
| `402` | Plan is not enabled for your organization yet (`plan_upgrade_required`). Expected during the Beta: write to support@dailybot.com. |
| `403` | Authenticated but not allowed: missing scope (`insufficient_scope`, which is also what an agent or organization key gets on an operation that needs a person, and what a personal key gets when its explicit Plan scopes do not cover the endpoint) or a guest account (`guest_not_allowed`). |
| `404` | Not found, or not visible to you. Both cases return the same body. |

#### Example (curl)

```bash
curl -sS "https://api.dailybot.com/v1/plan/projects/00000000-0000-4000-8000-000000000001/updates/" \
  -H "X-API-KEY: $DAILYBOT_API_KEY"
```

#### Scenario examples

##### CLI

```bash
dailybot plan project updates 00000000-0000-4000-8000-000000000001 --json
```

#### Notes

- Scope: `tasks:read`.
- Rate limit: 120 reads per minute per actor.
- Works with a login session, a CLI user token, a personal API key, or an agent or organization key. A personal key sees what its person sees; an agent or organization key acts as a system actor and sees organization-visible boards only.

### POST `/v1/plan/projects/{project_id}/updates/` · Beta

**Post a status note**

Send `body` as markdown. A `body_html` is NOT accepted — the server renders and sanitizes it, so the allow-list is ours and there is one of them. `health` records what the author claimed that day and does not change `Project.health`.

- **Auth:** API key (`X-API-KEY`), CLI Bearer (write)
- **Rate limit:** `default`
- **Pagination:** No pagination

#### Path parameters

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `project_id` | string | Yes | The project's uuid. |

#### Headers

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `X-Dailybot-Agent-Name` | string | No | The name of the agent that executed this write on the person's behalf. Use it on multipart and body-less writes (DELETE, archive, restore); on JSON writes send the body field `agent_name` instead, which wins if both are present. Percent-encode the value as UTF-8. Control characters are stripped; a blank value means no agent. More than 128 characters, or a value that cannot be decoded, is `400 invalid_agent_attribution` (never truncated). An agent-type key, which is not bound to a person, gets `400 invalid_agent_attribution` if it sends it. The stamp never changes a permission answer. See [Agent attribution](/developers/plan/conventions#agent-attribution). |

#### Request body

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `body` | string | Yes | Markdown. Mention someone with `<@DB@{uuid}>`, using a `uuid` from the mentionables list. |
| `health` | enum | null | No | Declared health. One of `not_set`, `on_track`, `at_risk`, `off_track`. |
| `agent_name` | string | No | The name of the agent that executed this write on the person's behalf (max 128 characters, blank means no agent). Takes priority over the `X-Dailybot-Agent-Name` header. See [Agent attribution](/developers/plan/conventions#agent-attribution). |

#### Response body

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `(body)` | ProjectUpdate | Yes | A [ProjectUpdate](#plan-projects-updates-digest-projectupdate) object. |

#### Error codes

| Status | When |
|--------|------|
| `400` | Validation failed, or a filter, sort or `include` value was not recognised. The response `code` says which. `invalid_agent_attribution` means the agent name is invalid. |
| `401` | Missing, expired or malformed credential (`credential_absent`, `credential_expired`, `credential_malformed`). |
| `402` | Plan is not enabled for your organization yet (`plan_upgrade_required`). Expected during the Beta: write to support@dailybot.com. |
| `403` | Authenticated but not allowed: missing scope (`insufficient_scope`, which is also what an agent or organization key gets on an operation that needs a person, and what a personal key gets when its explicit Plan scopes do not cover the endpoint) or a guest account (`guest_not_allowed`). |
| `404` | Not found, or not visible to you. Both cases return the same body. |

#### Example (curl)

```bash
curl -sS -X POST "https://api.dailybot.com/v1/plan/projects/00000000-0000-4000-8000-000000000001/updates/" \
  -H "X-API-KEY: $DAILYBOT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "body": "Staging is green; rolling out Friday. <@DB@00000000-0000-4000-8000-00000000000c> owns the release.",
    "health": "on_track"
  }'
```

#### Scenario examples

##### CLI

```bash
dailybot plan project update-post 00000000-0000-4000-8000-000000000001 "Staging is green; rolling out Friday." --health on_track
```

#### Notes

- Scope: `tasks:write`.
- Rate limit: 60 writes per minute per actor.
- Works with a login session, a CLI user token, a personal API key, or an agent or organization key. A personal key sees what its person sees; an agent or organization key acts as a system actor and sees organization-visible boards only.

### GET `/v1/plan/projects/{project_id}/milestones/` · Beta

**Dated commitments inside a project, in date order**

Each row carries `task_count`, annotated in the same query — a roadmap draws every marker at once, so a count per marker would be a query per row.

- **Auth:** API key (`X-API-KEY`), CLI Bearer (read)
- **Rate limit:** `default`
- **Pagination:** Page-number pagination

#### Path parameters

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `project_id` | string | Yes | The project's uuid. |

#### Query parameters

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `page` | integer | No | 1-based page number. |
| `page_size` | integer | No | Rows per page. Default 50, maximum 100. Out-of-range values are clamped, never rejected: asking for 500 returns 100. |
| `include_archived` | boolean | No | Include archived rows alongside live ones. Distinct from `is_archived`, which selects one set or the other: `include_archived=true` is the union. Lists return live rows unless you opt in. |

#### ProjectMilestone object

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `uuid` | uuid | Yes | Stable public identifier. |
| `name` | string | Yes | Display name. |
| `description` | string | null | No | Free-form description. |
| `date` | date | Yes | The milestone date. |
| `task_count` | integer | No | Number of live tasks. |
| `attachment_count` | integer | No | READY attachments on this milestone. Reference them from `description` with `attachment:{uuid}` markers and list them at `…/milestones/{milestone_id}/attachments/`. |
| `is_archived` | boolean | No | Whether the row is archived. Archive is the delete: archived rows stay readable and restorable. |
| `completed_at` | date-time | null | No | When it was completed, or `null`. |
| `created_at` | date-time | No | When the row was created. |
| `updated_at` | date-time | No | When the row last changed. |

#### Response body

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `count` | integer | Yes | Total number of rows. |
| `next` | uri | Yes | URL of the next page, or `null`. |
| `previous` | uri | Yes | URL of the previous page, or `null`. |
| `results` | array<ProjectMilestone> | Yes | The rows on this page. See [ProjectMilestone](#plan-project-milestones-list-projectmilestone). |

#### Error codes

| Status | When |
|--------|------|
| `401` | Missing, expired or malformed credential (`credential_absent`, `credential_expired`, `credential_malformed`). |
| `402` | Plan is not enabled for your organization yet (`plan_upgrade_required`). Expected during the Beta: write to support@dailybot.com. |
| `403` | Authenticated but not allowed: missing scope (`insufficient_scope`, which is also what an agent or organization key gets on an operation that needs a person, and what a personal key gets when its explicit Plan scopes do not cover the endpoint) or a guest account (`guest_not_allowed`). |
| `404` | Not found, or not visible to you. Both cases return the same body. |

#### Example (curl)

```bash
curl -sS "https://api.dailybot.com/v1/plan/projects/00000000-0000-4000-8000-000000000001/milestones/" \
  -H "X-API-KEY: $DAILYBOT_API_KEY"
```

#### Scenario examples

##### CLI

```bash
dailybot plan project milestones 00000000-0000-4000-8000-000000000001
```

##### Response

```bash
{
  "count": 1,
  "next": null,
  "previous": null,
  "results": [
    {
      "uuid": "00000000-0000-4000-8000-000000000007",
      "name": "Beta launch",
      "description": null,
      "date": "2026-09-28",
      "task_count": 12,
      "is_archived": false,
      "completed_at": null,
      "created_at": "2026-09-25T10:14:02Z",
      "updated_at": "2026-09-25T10:14:02Z"
    }
  ]
}
```

#### Notes

- Scope: `tasks:read`.
- Rate limit: 120 reads per minute per actor.
- Works with a login session, a CLI user token, a personal API key, or an agent or organization key. A personal key sees what its person sees; an agent or organization key acts as a system actor and sees organization-visible boards only.

### POST `/v1/plan/projects/{project_id}/milestones/` · Beta

**Commit to a dated moment**

Adds a milestone to a project: a `name`, a `date` and an optional `description`. Complete it later with the complete endpoint.

- **Auth:** API key (`X-API-KEY`), CLI Bearer (write)
- **Rate limit:** `default`
- **Pagination:** No pagination

#### Path parameters

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `project_id` | string | Yes | The project's uuid. |

#### Headers

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `X-Dailybot-Agent-Name` | string | No | The name of the agent that executed this write on the person's behalf. Use it on multipart and body-less writes (DELETE, archive, restore); on JSON writes send the body field `agent_name` instead, which wins if both are present. Percent-encode the value as UTF-8. Control characters are stripped; a blank value means no agent. More than 128 characters, or a value that cannot be decoded, is `400 invalid_agent_attribution` (never truncated). An agent-type key, which is not bound to a person, gets `400 invalid_agent_attribution` if it sends it. The stamp never changes a permission answer. See [Agent attribution](/developers/plan/conventions#agent-attribution). |

#### Request body

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `name` | string | Yes | Display name. |
| `date` | date | Yes | The milestone date. |
| `description` | string | null | No | Free-form description. |
| `agent_name` | string | No | The name of the agent that executed this write on the person's behalf (max 128 characters, blank means no agent). Takes priority over the `X-Dailybot-Agent-Name` header. See [Agent attribution](/developers/plan/conventions#agent-attribution). |

#### Response body

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `(body)` | ProjectMilestone | Yes | A [ProjectMilestone](#plan-project-milestones-list-projectmilestone) object. |

#### Error codes

| Status | When |
|--------|------|
| `400` | Validation failed, or a filter, sort or `include` value was not recognised. The response `code` says which. `invalid_agent_attribution` means the agent name is invalid. |
| `401` | Missing, expired or malformed credential (`credential_absent`, `credential_expired`, `credential_malformed`). |
| `402` | Plan is not enabled for your organization yet (`plan_upgrade_required`). Expected during the Beta: write to support@dailybot.com. |
| `403` | Authenticated but not allowed: missing scope (`insufficient_scope`, which is also what an agent or organization key gets on an operation that needs a person, and what a personal key gets when its explicit Plan scopes do not cover the endpoint) or a guest account (`guest_not_allowed`). |
| `404` | Not found, or not visible to you. Both cases return the same body. |

#### Example (curl)

```bash
curl -sS -X POST "https://api.dailybot.com/v1/plan/projects/00000000-0000-4000-8000-000000000001/milestones/" \
  -H "X-API-KEY: $DAILYBOT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Beta launch",
    "date": "2026-10-15"
  }'
```

#### Scenario examples

##### CLI

```bash
dailybot plan project milestone-create 00000000-0000-4000-8000-000000000001 -n "Beta launch" --date 2026-10-15
```

#### Notes

- Scope: `tasks:write`.
- Rate limit: 60 writes per minute per actor.
- Works with a login session, a CLI user token, a personal API key, or an agent or organization key. A personal key sees what its person sees; an agent or organization key acts as a system actor and sees organization-visible boards only.

### PATCH `/v1/plan/projects/{project_id}/milestones/{milestone_id}/` · Beta

**Move or rename a milestone**

Renames a milestone, moves its date or edits its description. Send only the fields you change.

- **Auth:** API key (`X-API-KEY`), CLI Bearer (write)
- **Rate limit:** `default`
- **Pagination:** No pagination

#### Path parameters

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `project_id` | string | Yes | The project's uuid. |
| `milestone_id` | string | Yes | The milestone's uuid. |

#### Headers

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `X-Dailybot-Agent-Name` | string | No | The name of the agent that executed this write on the person's behalf. Use it on multipart and body-less writes (DELETE, archive, restore); on JSON writes send the body field `agent_name` instead, which wins if both are present. Percent-encode the value as UTF-8. Control characters are stripped; a blank value means no agent. More than 128 characters, or a value that cannot be decoded, is `400 invalid_agent_attribution` (never truncated). An agent-type key, which is not bound to a person, gets `400 invalid_agent_attribution` if it sends it. The stamp never changes a permission answer. See [Agent attribution](/developers/plan/conventions#agent-attribution). |

#### Request body

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `name` | string | No | Display name. |
| `date` | date | No | The milestone date. |
| `description` | string | null | No | Free-form description. |
| `agent_name` | string | No | The name of the agent that executed this write on the person's behalf (max 128 characters, blank means no agent). Takes priority over the `X-Dailybot-Agent-Name` header. See [Agent attribution](/developers/plan/conventions#agent-attribution). |

#### Response body

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `(body)` | ProjectMilestone | Yes | A [ProjectMilestone](#plan-project-milestones-list-projectmilestone) object. |

#### Error codes

| Status | When |
|--------|------|
| `400` | Validation failed, or a filter, sort or `include` value was not recognised. The response `code` says which. `invalid_agent_attribution` means the agent name is invalid. |
| `401` | Missing, expired or malformed credential (`credential_absent`, `credential_expired`, `credential_malformed`). |
| `402` | Plan is not enabled for your organization yet (`plan_upgrade_required`). Expected during the Beta: write to support@dailybot.com. |
| `403` | Authenticated but not allowed: missing scope (`insufficient_scope`, which is also what an agent or organization key gets on an operation that needs a person, and what a personal key gets when its explicit Plan scopes do not cover the endpoint) or a guest account (`guest_not_allowed`). |
| `404` | Not found, or not visible to you. Both cases return the same body. |

#### Example (curl)

```bash
curl -sS -X PATCH "https://api.dailybot.com/v1/plan/projects/00000000-0000-4000-8000-000000000001/milestones/00000000-0000-4000-8000-000000000007/" \
  -H "X-API-KEY: $DAILYBOT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "date": "2026-10-22"
  }'
```

#### Scenario examples

##### CLI

```bash
dailybot plan project milestone-update 00000000-0000-4000-8000-000000000001 00000000-0000-4000-8000-000000000007 --date 2026-10-22
```

#### Notes

- Scope: `tasks:write`.
- Rate limit: 60 writes per minute per actor.
- Works with a login session, a CLI user token, a personal API key, or an agent or organization key. A personal key sees what its person sees; an agent or organization key acts as a system actor and sees organization-visible boards only.

### DELETE `/v1/plan/projects/{project_id}/milestones/{milestone_id}/` · Beta

**Retire a milestone (archives; tasks keep pointing at it)**

Archives rather than hard-deletes, so tasks keep pointing at the milestone and the association is never lost. The change is recorded in the activity feed as `project.milestone_deleted` — read it as "retired". It is not a webhook event.

- **Auth:** API key (`X-API-KEY`), CLI Bearer (write)
- **Rate limit:** `default`
- **Pagination:** No pagination

#### Path parameters

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `project_id` | string | Yes | The project's uuid. |
| `milestone_id` | string | Yes | The milestone's uuid. |

#### Headers

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `X-Dailybot-Agent-Name` | string | No | The name of the agent that executed this write on the person's behalf. Use it on multipart and body-less writes (DELETE, archive, restore); on JSON writes send the body field `agent_name` instead, which wins if both are present. Percent-encode the value as UTF-8. Control characters are stripped; a blank value means no agent. More than 128 characters, or a value that cannot be decoded, is `400 invalid_agent_attribution` (never truncated). An agent-type key, which is not bound to a person, gets `400 invalid_agent_attribution` if it sends it. The stamp never changes a permission answer. See [Agent attribution](/developers/plan/conventions#agent-attribution). |

#### Error codes

| Status | When |
|--------|------|
| `400` | The agent name is invalid (`invalid_agent_attribution`). |
| `401` | Missing, expired or malformed credential (`credential_absent`, `credential_expired`, `credential_malformed`). |
| `402` | Plan is not enabled for your organization yet (`plan_upgrade_required`). Expected during the Beta: write to support@dailybot.com. |
| `403` | Authenticated but not allowed: missing scope (`insufficient_scope`, which is also what an agent or organization key gets on an operation that needs a person, and what a personal key gets when its explicit Plan scopes do not cover the endpoint) or a guest account (`guest_not_allowed`). |
| `404` | Not found, or not visible to you. Both cases return the same body. |

#### Example (curl)

```bash
curl -sS -X DELETE "https://api.dailybot.com/v1/plan/projects/00000000-0000-4000-8000-000000000001/milestones/00000000-0000-4000-8000-000000000007/" \
  -H "X-API-KEY: $DAILYBOT_API_KEY"
```

#### Scenario examples

##### CLI

```bash
dailybot plan project milestone-delete 00000000-0000-4000-8000-000000000001 00000000-0000-4000-8000-000000000007 --yes
```

#### Notes

- Scope: `tasks:write`.
- Rate limit: 60 writes per minute per actor.
- Works with a login session, a CLI user token, a personal API key, or an agent or organization key. A personal key sees what its person sees; an agent or organization key acts as a system actor and sees organization-visible boards only.

### POST `/v1/plan/projects/{project_id}/milestones/{milestone_id}/complete/` · Beta

**Mark a milestone complete**

Completing with open tasks is **allowed**. Those tasks stay open; the response reports `open_task_count`. Reversible via `…/reopen/`.
`?dry_run=true` returns the consequence without writing.

- **Auth:** API key (`X-API-KEY`), CLI Bearer (write)
- **Rate limit:** `default`
- **Pagination:** No pagination

#### Path parameters

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `project_id` | string | Yes | The project's uuid. |
| `milestone_id` | string | Yes | The milestone's uuid. |

#### Query parameters

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `dry_run` | boolean | No | Preview the consequence without performing it. The response has the same shape, `{operation, dry_run, reversible, restore_path, consequence, affects}`, but nothing is written and no event is emitted. Show `consequence` to a person before acting. |

#### Headers

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `Idempotency-Key` | string | No | A key you generate for this intent. A replay with the same key and body returns the first response without a second side effect and carries `Idempotency-Replayed: true`. Keys are kept for 24 hours. The same key with a different body is `409 idempotency_key_payload_mismatch`; a repeat while the first call is still running gets `409 idempotency_in_progress` for up to 120 seconds. |
| `X-Dailybot-Agent-Name` | string | No | The name of the agent that executed this write on the person's behalf. Use it on multipart and body-less writes (DELETE, archive, restore); on JSON writes send the body field `agent_name` instead, which wins if both are present. Percent-encode the value as UTF-8. Control characters are stripped; a blank value means no agent. More than 128 characters, or a value that cannot be decoded, is `400 invalid_agent_attribution` (never truncated). An agent-type key, which is not bound to a person, gets `400 invalid_agent_attribution` if it sends it. The stamp never changes a permission answer. See [Agent attribution](/developers/plan/conventions#agent-attribution). |

#### DryRunPreview object

What the call answers with `?dry_run=true`: the consequence, without performing it. Nothing is written and no event is emitted.


| Name | Type | Required | Description |
|------|------|----------|-------------|
| `operation` | string | Yes | The operation that would run. |
| `dry_run` | boolean | Yes | Always `true`. |
| `reversible` | boolean | Yes | Whether the operation can be undone. |
| `restore_path` | string | null | Yes | The path that would undo it, or `null` when there is none. |
| `consequence` | string | Yes | A sentence to show a person before acting. It states the cascade rather than summarising it. |
| `affects` | object | Yes | What the operation would touch, as counts (integers) by kind. |
| `would_refuse` | boolean | No | Workflow state archive only: `true` when the real call would be refused. |
| `refusal_code` | string | No | Workflow state archive only: the error code the real call would answer with. |

#### Response body

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `(body)` | ProjectMilestone | DryRunPreview | Yes | A [ProjectMilestone](#plan-project-milestones-list-projectmilestone) object. With `?dry_run=true`, a [DryRunPreview](#plan-project-milestones-complete-dryrunpreview) object instead. |
| `open_task_count` | integer | No | Tasks still open in the milestone. Completing with open tasks is allowed. |

#### Error codes

| Status | When |
|--------|------|
| `400` | The agent name is invalid (`invalid_agent_attribution`). |
| `401` | Missing, expired or malformed credential (`credential_absent`, `credential_expired`, `credential_malformed`). |
| `402` | Plan is not enabled for your organization yet (`plan_upgrade_required`). Expected during the Beta: write to support@dailybot.com. |
| `403` | Authenticated but not allowed: missing scope (`insufficient_scope`, which is also what an agent or organization key gets on an operation that needs a person, and what a personal key gets when its explicit Plan scopes do not cover the endpoint) or a guest account (`guest_not_allowed`). |
| `404` | Not found, or not visible to you. Both cases return the same body. |

#### Example (curl)

```bash
curl -sS -X POST "https://api.dailybot.com/v1/plan/projects/00000000-0000-4000-8000-000000000001/milestones/00000000-0000-4000-8000-000000000007/complete/" \
  -H "X-API-KEY: $DAILYBOT_API_KEY"
```

#### Scenario examples

##### CLI

```bash
dailybot plan project milestone-complete 00000000-0000-4000-8000-000000000001 00000000-0000-4000-8000-000000000007 --dry-run
```

##### Response

```bash
{
  "uuid": "00000000-0000-4000-8000-000000000007",
  "name": "Beta launch",
  "description": null,
  "date": "2026-09-28",
  "task_count": 12,
  "is_archived": false,
  "completed_at": null,
  "created_at": "2026-09-25T10:14:02Z",
  "updated_at": "2026-09-25T10:14:02Z",
  "open_task_count": 2
}
```

#### Notes

- Scope: `tasks:write`.
- Rate limit: 60 writes per minute per actor.
- Works with a login session, a CLI user token, a personal API key, or an agent or organization key. A personal key sees what its person sees; an agent or organization key acts as a system actor and sees organization-visible boards only.

### POST `/v1/plan/projects/{project_id}/milestones/{milestone_id}/reopen/` · Beta

**Clear milestone completion**

Clears a milestone's completion, so it counts as open again.

- **Auth:** API key (`X-API-KEY`), CLI Bearer (write)
- **Rate limit:** `default`
- **Pagination:** No pagination

#### Path parameters

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `project_id` | string | Yes | The project's uuid. |
| `milestone_id` | string | Yes | The milestone's uuid. |

#### Headers

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `Idempotency-Key` | string | No | A key you generate for this intent. A replay with the same key and body returns the first response without a second side effect and carries `Idempotency-Replayed: true`. Keys are kept for 24 hours. The same key with a different body is `409 idempotency_key_payload_mismatch`; a repeat while the first call is still running gets `409 idempotency_in_progress` for up to 120 seconds. |
| `X-Dailybot-Agent-Name` | string | No | The name of the agent that executed this write on the person's behalf. Use it on multipart and body-less writes (DELETE, archive, restore); on JSON writes send the body field `agent_name` instead, which wins if both are present. Percent-encode the value as UTF-8. Control characters are stripped; a blank value means no agent. More than 128 characters, or a value that cannot be decoded, is `400 invalid_agent_attribution` (never truncated). An agent-type key, which is not bound to a person, gets `400 invalid_agent_attribution` if it sends it. The stamp never changes a permission answer. See [Agent attribution](/developers/plan/conventions#agent-attribution). |

#### Response body

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `(body)` | ProjectMilestone | Yes | A [ProjectMilestone](#plan-project-milestones-list-projectmilestone) object. |

#### Error codes

| Status | When |
|--------|------|
| `400` | The agent name is invalid (`invalid_agent_attribution`). |
| `401` | Missing, expired or malformed credential (`credential_absent`, `credential_expired`, `credential_malformed`). |
| `402` | Plan is not enabled for your organization yet (`plan_upgrade_required`). Expected during the Beta: write to support@dailybot.com. |
| `403` | Authenticated but not allowed: missing scope (`insufficient_scope`, which is also what an agent or organization key gets on an operation that needs a person, and what a personal key gets when its explicit Plan scopes do not cover the endpoint) or a guest account (`guest_not_allowed`). |
| `404` | Not found, or not visible to you. Both cases return the same body. |

#### Example (curl)

```bash
curl -sS -X POST "https://api.dailybot.com/v1/plan/projects/00000000-0000-4000-8000-000000000001/milestones/00000000-0000-4000-8000-000000000007/reopen/" \
  -H "X-API-KEY: $DAILYBOT_API_KEY"
```

#### Scenario examples

##### CLI

```bash
dailybot plan project milestone-reopen 00000000-0000-4000-8000-000000000001 00000000-0000-4000-8000-000000000007
```

#### Notes

- Scope: `tasks:write`.
- Rate limit: 60 writes per minute per actor.
- Works with a login session, a CLI user token, a personal API key, or an agent or organization key. A personal key sees what its person sees; an agent or organization key acts as a system actor and sees organization-visible boards only.

### POST `/v1/plan/projects/{project_id}/archive/` · Beta

**Archive a project, cascading to its boards and their tasks**

Archive is the delete. Nothing in this API hard-deletes a project; the rows survive so identifiers, links and events keep resolving.

- **Auth:** CLI Bearer (write)
- **Rate limit:** `default`
- **Pagination:** No pagination

#### Path parameters

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `project_id` | string | Yes | The project's uuid. |

#### Query parameters

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `dry_run` | boolean | No | Preview the consequence without performing it. The response has the same shape, `{operation, dry_run, reversible, restore_path, consequence, affects}`, but nothing is written and no event is emitted. Show `consequence` to a person before acting. |

#### Headers

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `Idempotency-Key` | string | No | A key you generate for this intent. A replay with the same key and body returns the first response without a second side effect and carries `Idempotency-Replayed: true`. Keys are kept for 24 hours. The same key with a different body is `409 idempotency_key_payload_mismatch`; a repeat while the first call is still running gets `409 idempotency_in_progress` for up to 120 seconds. |
| `X-Dailybot-Agent-Name` | string | No | The name of the agent that executed this write on the person's behalf. Use it on multipart and body-less writes (DELETE, archive, restore); on JSON writes send the body field `agent_name` instead, which wins if both are present. Percent-encode the value as UTF-8. Control characters are stripped; a blank value means no agent. More than 128 characters, or a value that cannot be decoded, is `400 invalid_agent_attribution` (never truncated). An agent-type key, which is not bound to a person, gets `400 invalid_agent_attribution` if it sends it. The stamp never changes a permission answer. See [Agent attribution](/developers/plan/conventions#agent-attribution). |

#### Response body

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `(body)` | Project | DryRunPreview | Yes | A [Project](#plan-projects-list-project) object. With `?dry_run=true`, a [DryRunPreview](#plan-project-milestones-complete-dryrunpreview) object instead. |

#### Error codes

| Status | When |
|--------|------|
| `400` | The agent name is invalid (`invalid_agent_attribution`). |
| `401` | Missing, expired or malformed credential (`credential_absent`, `credential_expired`, `credential_malformed`). |
| `402` | Plan is not enabled for your organization yet (`plan_upgrade_required`). Expected during the Beta: write to support@dailybot.com. |
| `403` | Authenticated but not allowed: missing scope (`insufficient_scope`, which is also what an agent or organization key gets on an operation that needs a person, and what a personal key gets when its explicit Plan scopes do not cover the endpoint) or a guest account (`guest_not_allowed`). |
| `404` | Not found, or not visible to you. Both cases return the same body. |

#### Example (curl)

```bash
curl -sS -X POST "https://api.dailybot.com/v1/plan/projects/00000000-0000-4000-8000-000000000001/archive/" \
  -H "Authorization: Bearer $DAILYBOT_TOKEN"
```

#### Scenario examples

##### CLI

```bash
dailybot plan project archive 00000000-0000-4000-8000-000000000001 --dry-run
```

#### Notes

- Scope: `tasks:admin` — container writes. A non-guest member can call it with a login session or a personal API key (a key with explicit Plan scopes needs `tasks:write`, which covers it); an agent or organization key gets `403 insufficient_scope`.
- Rate limit: 60 writes per minute per actor.
- Needs a person: call it with a login session, a CLI user token or a personal API key. An agent or organization key gets `403 insufficient_scope`.

### POST `/v1/plan/projects/{project_id}/restore/` · Beta

**Restore an archived project**

Inverse of archive, and the reason archiving a project is no longer the one act in Plan a person cannot undo. Boards and tasks that cascaded on archive stay archived: restore walks back up, never down, because "restore everything archived at the time" cannot tell the cascade apart from a board somebody archived on purpose beforehand. Bring those back with `POST …/boards/{board_id}/restore/`. Restore consumes one project-creation entitlement slot (archive frees one) and answers `402` when the plan has none to give.

- **Auth:** CLI Bearer (write)
- **Rate limit:** `default`
- **Pagination:** No pagination

#### Path parameters

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `project_id` | string | Yes | The project's uuid. |

#### Headers

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `Idempotency-Key` | string | No | A key you generate for this intent. A replay with the same key and body returns the first response without a second side effect and carries `Idempotency-Replayed: true`. Keys are kept for 24 hours. The same key with a different body is `409 idempotency_key_payload_mismatch`; a repeat while the first call is still running gets `409 idempotency_in_progress` for up to 120 seconds. |
| `X-Dailybot-Agent-Name` | string | No | The name of the agent that executed this write on the person's behalf. Use it on multipart and body-less writes (DELETE, archive, restore); on JSON writes send the body field `agent_name` instead, which wins if both are present. Percent-encode the value as UTF-8. Control characters are stripped; a blank value means no agent. More than 128 characters, or a value that cannot be decoded, is `400 invalid_agent_attribution` (never truncated). An agent-type key, which is not bound to a person, gets `400 invalid_agent_attribution` if it sends it. The stamp never changes a permission answer. See [Agent attribution](/developers/plan/conventions#agent-attribution). |

#### Response body

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `(body)` | Project | Yes | A [Project](#plan-projects-list-project) object. |

#### Error codes

| Status | When |
|--------|------|
| `400` | The agent name is invalid (`invalid_agent_attribution`). |
| `401` | Missing, expired or malformed credential (`credential_absent`, `credential_expired`, `credential_malformed`). |
| `402` | Plan is not enabled for your organization yet (`plan_upgrade_required`), or no project slot is free (`task_projects_limit_reached`). |
| `403` | Authenticated but not allowed: missing scope (`insufficient_scope`, which is also what an agent or organization key gets on an operation that needs a person, and what a personal key gets when its explicit Plan scopes do not cover the endpoint) or a guest account (`guest_not_allowed`). |
| `404` | Not found, or not visible to you. Both cases return the same body. |

#### Example (curl)

```bash
curl -sS -X POST "https://api.dailybot.com/v1/plan/projects/00000000-0000-4000-8000-000000000001/restore/" \
  -H "Authorization: Bearer $DAILYBOT_TOKEN"
```

#### Scenario examples

##### CLI

```bash
dailybot plan project restore 00000000-0000-4000-8000-000000000001
```

#### Notes

- Scope: `tasks:admin` — container writes. A non-guest member can call it with a login session or a personal API key (a key with explicit Plan scopes needs `tasks:write`, which covers it); an agent or organization key gets `403 insufficient_scope`.
- Rate limit: 60 writes per minute per actor.
- Needs a person: call it with a login session, a CLI user token or a personal API key. An agent or organization key gets `403 insufficient_scope`.

### GET `/v1/plan/milestones/` · Beta

**Every milestone the viewer may see, across projects**

Every milestone you may see across projects, in one call, for a roadmap's markers. Visibility follows the projects you can open. `project__in` narrows that set and never widens it: an unknown uuid and another organization's uuid both return an empty result. Rows carry `project` as a reference.

- **Auth:** API key (`X-API-KEY`), CLI Bearer (read)
- **Rate limit:** `default`
- **Pagination:** Page-number pagination

#### Query parameters

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `page` | integer | No | 1-based page number. |
| `page_size` | integer | No | Rows per page. Default 50, maximum 100. Out-of-range values are clamped, never rejected: asking for 500 returns 100. |
| `project__in` | string | No | Comma-separated project uuids; at most 50. Narrows the visible set, never widens it. |
| `include_archived` | string | No | Include retired milestones alongside live ones. |

#### OrganizationMilestone object

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `uuid` | string | Yes | Stable public identifier. |
| `name` | string | Yes | Display name. |
| `description` | string | null | No | Free-form description. |
| `date` | string | Yes | The milestone date. |
| `task_count` | integer | No | Number of live tasks. |
| `attachment_count` | integer | No | READY attachments on this milestone. Reference them from `description` with `attachment:{uuid}` markers and list them at `…/milestones/{milestone_id}/attachments/`. |
| `is_archived` | boolean | No | Whether the row is archived. Archive is the delete: archived rows stay readable and restorable. |
| `project` | object | Yes | The project. A reference object. |
| `created_at` | string | No | When the row was created. |
| `updated_at` | string | No | When the row last changed. |

#### Response body

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `count` | integer | Yes | Total number of rows. |
| `next` | uri | Yes | URL of the next page, or `null`. |
| `previous` | uri | Yes | URL of the previous page, or `null`. |
| `results` | array<OrganizationMilestone> | Yes | The rows on this page. See [OrganizationMilestone](#plan-milestones-list-organizationmilestone). |

#### Error codes

| Status | When |
|--------|------|
| `400` | Validation failed, or a filter, sort or `include` value was not recognised. The response `code` says which. |
| `401` | Missing, expired or malformed credential (`credential_absent`, `credential_expired`, `credential_malformed`). |
| `402` | Plan is not enabled for your organization yet (`plan_upgrade_required`). Expected during the Beta: write to support@dailybot.com. |
| `403` | Authenticated but not allowed: missing scope (`insufficient_scope`, which is also what an agent or organization key gets on an operation that needs a person, and what a personal key gets when its explicit Plan scopes do not cover the endpoint) or a guest account (`guest_not_allowed`). |

#### Example (curl)

```bash
curl -sS "https://api.dailybot.com/v1/plan/milestones/?project__in=00000000-0000-4000-8000-000000000001" \
  -H "X-API-KEY: $DAILYBOT_API_KEY"
```

#### Scenario examples

##### CLI

```bash
dailybot plan project milestones
```

##### Response

```bash
{
  "count": 1,
  "next": null,
  "previous": null,
  "results": [
    {
      "uuid": "00000000-0000-4000-8000-000000000007",
      "name": "Beta launch",
      "description": null,
      "date": "example",
      "task_count": 12,
      "is_archived": false,
      "project": {},
      "created_at": "example",
      "updated_at": "example"
    }
  ]
}
```

#### Notes

- Scope: `tasks:read`.
- Rate limit: 120 reads per minute per actor.
- Works with a login session, a CLI user token, a personal API key, or an agent or organization key. A personal key sees what its person sees; an agent or organization key acts as a system actor and sees organization-visible boards only.

### GET `/v1/plan/projects/{project_id}/views/` · Beta

**This person's saved views inside a project**

Your saved views inside a project: named filter sets that span every board in it. Views are personal and scoped by project, so the same name can exist in two projects. Needs a person: agent and organization keys are refused; a personal API key works.

- **Auth:** CLI Bearer (read)
- **Rate limit:** `default`
- **Pagination:** Page-number pagination

#### Path parameters

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `project_id` | string | Yes | The project's uuid. |

#### SavedView object

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `name` | string | Yes | Display name. Max 64 characters. |
| `view_mode` | enum | No | How the filtered set is drawn. Reads always return `board` for the kanban layout. One of `list`, `board`, `timeline`, `calendar`. |
| `group_by` | enum | No | The grouping dimension. One of `state`, `owner`, `priority`, `category`. |
| `sort` | string | No | A sort key, `-` prefixed for descending. |
| `filters` | object | Yes | The view's filters, in the shared task filter grammar. |
| `schema_version` | integer | No | Version of the view's stored format. |
| `visibility` | enum | No | `personal` (default) is yours alone. `shared` and `board_default` (the default view for that board or project) are readable by everyone who can see the board or project. Setting them needs a board manager on board views, and project oversight (an organization admin or a manager of all teams) on project views; otherwise `403 view_visibility_forbidden`. One of `personal`, `shared`, `board_default`. |
| `collapsed` | object | array | string | number | boolean | No | Client UI state stored verbatim (which groups are collapsed). Only size and depth are validated. |
| `columns` | object | array | string | number | boolean | No | Client UI state stored verbatim (which columns are shown). Only size and depth are validated. |
| `uuid` | uuid | No | Stable public identifier. |
| `scope` | enum | No | Which container the view belongs to: `board` or `project`. Read-only. One of `board`, `project`. |
| `board` | uuid | null | No | The board's uuid when `scope` is `board`; `null` for a project view. Read-only. |
| `owner` | object | No | Who owns the view. Shape: `{uuid, name}`. |
| `created_at` | date-time | No | When the row was created. |
| `updated_at` | date-time | No | When the row last changed. |

#### Response body

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `count` | integer | Yes | Total number of rows. |
| `next` | uri | Yes | URL of the next page, or `null`. |
| `previous` | uri | Yes | URL of the previous page, or `null`. |
| `results` | array<SavedView> | Yes | The rows on this page. See [SavedView](#plan-project-views-list-savedview). |

#### Error codes

| Status | When |
|--------|------|
| `401` | Missing, expired or malformed credential (`credential_absent`, `credential_expired`, `credential_malformed`). |
| `402` | Plan is not enabled for your organization yet (`plan_upgrade_required`). Expected during the Beta: write to support@dailybot.com. |
| `403` | Authenticated but not allowed: missing scope (`insufficient_scope`, which is also what an agent or organization key gets on an operation that needs a person, and what a personal key gets when its explicit Plan scopes do not cover the endpoint) or a guest account (`guest_not_allowed`). |
| `404` | Not found, or not visible to you. Both cases return the same body. |
| `429` | Rate limit reached. Wait the number of seconds in `Retry-After`. |

#### Example (curl)

```bash
curl -sS "https://api.dailybot.com/v1/plan/projects/00000000-0000-4000-8000-000000000001/views/" \
  -H "Authorization: Bearer $DAILYBOT_TOKEN"
```

#### Scenario examples

##### CLI

```bash
dailybot plan project views 00000000-0000-4000-8000-000000000001 --etag
```

##### Response

```bash
{
  "count": 1,
  "next": null,
  "previous": null,
  "results": [
    {
      "name": "My open work",
      "view_mode": "list",
      "group_by": "state",
      "sort": "-updated_at",
      "filters": {},
      "schema_version": 1,
      "visibility": "personal",
      "collapsed": {},
      "columns": {}
    }
  ]
}
```

#### Notes

- Scope: `tasks:read`.
- Rate limit: 120 reads per minute per actor.
- Needs a person: call it with a login session, a CLI user token or a personal API key. An agent or organization key gets `403 insufficient_scope`.

### PUT `/v1/plan/projects/{project_id}/views/` · Beta

**Replace this person's saved views for a project**

Replaces your whole saved-view array for the project. `If-Match` is required, for the same reason as board views.

- **Auth:** CLI Bearer (write)
- **Rate limit:** `default`
- **Pagination:** No pagination

#### Path parameters

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `project_id` | string | Yes | The project's uuid. |

#### Headers

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `If-Match` | string | Yes | The `ETag` you received from `GET .../views/`, quoted. **Required**, because this `PUT` replaces the whole array: without a precondition two concurrent saves silently drop one another's view. A stale validator is `412 precondition_failed`; a missing one is `428 precondition_required`. |
| `X-Dailybot-Agent-Name` | string | No | The name of the agent that executed this write on the person's behalf. Use it on multipart and body-less writes (DELETE, archive, restore); on JSON writes send the body field `agent_name` instead, which wins if both are present. Percent-encode the value as UTF-8. Control characters are stripped; a blank value means no agent. More than 128 characters, or a value that cannot be decoded, is `400 invalid_agent_attribution` (never truncated). An agent-type key, which is not bound to a person, gets `400 invalid_agent_attribution` if it sends it. The stamp never changes a permission answer. See [Agent attribution](/developers/plan/conventions#agent-attribution). |

#### Response body

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `(body)` | array<SavedView> | Yes | A JSON array of [SavedView](#plan-project-views-list-savedview) objects. |

#### Error codes

| Status | When |
|--------|------|
| `400` | Validation failed, or a filter, sort or `include` value was not recognised. The response `code` says which. `invalid_agent_attribution` means the agent name is invalid. |
| `401` | Missing, expired or malformed credential (`credential_absent`, `credential_expired`, `credential_malformed`). |
| `402` | Plan is not enabled for your organization yet (`plan_upgrade_required`). Expected during the Beta: write to support@dailybot.com. |
| `403` | Authenticated but not allowed: missing scope (`insufficient_scope`, which is also what an agent or organization key gets on an operation that needs a person, and what a personal key gets when its explicit Plan scopes do not cover the endpoint) or a guest account (`guest_not_allowed`). |
| `404` | Not found, or not visible to you. Both cases return the same body. |
| `412` | The `If-Match` validator is stale (`precondition_failed`). Read again and retry. |
| `428` | `If-Match` is required (`precondition_required`). |
| `429` | Rate limit reached. Wait the number of seconds in `Retry-After`. |

#### Example (curl)

```bash
curl -sS -X PUT "https://api.dailybot.com/v1/plan/projects/00000000-0000-4000-8000-000000000001/views/" \
  -H "Authorization: Bearer $DAILYBOT_TOKEN" \
  -H "If-Match: $VIEWS_ETAG" \
  -H "Content-Type: application/json" \
  -d '[
    {
      "name": "Overdue",
      "view_mode": "list",
      "filters": {
        "due_before": "2026-09-25",
        "state": [
          "open"
        ]
      }
    }
  ]'
```

#### Scenario examples

##### CLI

```bash
dailybot plan project view save 00000000-0000-4000-8000-000000000001 -f views.json --fetch-etag
```

##### Response

```bash
[
  {
    "name": "My open work",
    "view_mode": "list",
    "group_by": "state",
    "sort": "-updated_at",
    "filters": {},
    "schema_version": 1,
    "visibility": "personal",
    "collapsed": {},
    "columns": {}
  }
]
```

#### Notes

- Scope: `tasks:write`.
- Rate limit: 60 writes per minute per actor.
- Needs a person: call it with a login session, a CLI user token or a personal API key. An agent or organization key gets `403 insufficient_scope`.

### GET `/v1/plan/projects/{project_id}/members/` · Beta

**Members of a project**

Visible to anyone who can see the project. On a `members` project this is the membership that grants sight of the project and its boards.

- **Auth:** CLI Bearer (read)
- **Rate limit:** `default`
- **Pagination:** Page-number pagination

#### Path parameters

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `project_id` | string | Yes | The project's uuid. |

#### Query parameters

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `page` | integer | No | 1-based page number. |
| `page_size` | integer | No | Rows per page. Default 50, maximum 100. Out-of-range values are clamped, never rejected: asking for 500 returns 100. |

#### BoardMember object

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `subject_type` | enum | Yes | One of `user`, `team`. |
| `user_uuid` | uuid | null | No | The person's user uuid. |
| `uuid` | uuid | null | No | Stable public identifier. |
| `full_name` | string | No | — |
| `name` | string | No | Display name. |
| `role` | enum | null | No | Participant role. One of `admin`, `member`, `guest`. |
| `team_uuid` | uuid | null | No | A team's uuid, instead of `user_uuid`. It creates one live team grant: whoever joins the team later is in, and whoever leaves is out. |
| `team_name` | string | No | — |
| `added_at` | date-time | Yes | — |
| `added_by_uuid` | uuid | null | No | — |

#### Response body

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `count` | integer | Yes | Total number of rows. |
| `next` | uri | Yes | URL of the next page, or `null`. |
| `previous` | uri | Yes | URL of the previous page, or `null`. |
| `results` | array<BoardMember> | Yes | The rows on this page. See [BoardMember](#plan-project-members-list-boardmember). |

#### Error codes

| Status | When |
|--------|------|
| `401` | Missing, expired or malformed credential (`credential_absent`, `credential_expired`, `credential_malformed`). |
| `402` | Plan is not enabled for your organization yet (`plan_upgrade_required`). Expected during the Beta: write to support@dailybot.com. |
| `404` | Not found, or not visible to you. Both cases return the same body. |

#### Example (curl)

```bash
curl -sS "https://api.dailybot.com/v1/plan/projects/00000000-0000-4000-8000-000000000001/members/" \
  -H "Authorization: Bearer $DAILYBOT_TOKEN"
```

#### Scenario examples

##### CLI

```bash
dailybot plan project members 00000000-0000-4000-8000-000000000001
```

##### Response

```bash
{
  "count": 1,
  "next": null,
  "previous": null,
  "results": [
    {
      "subject_type": "user",
      "user_uuid": "00000000-0000-4000-8000-00000000000c",
      "uuid": "00000000-0000-4000-8000-00000000000c",
      "full_name": "Ada L.",
      "name": "Ada L.",
      "role": "admin",
      "team_uuid": null,
      "team_name": "example",
      "added_at": "2026-09-25T10:14:02Z",
      "added_by_uuid": null
    }
  ]
}
```

#### Notes

- Scope: `tasks:read`.
- Rate limit: 120 reads per minute per actor.
- Needs a person: call it with a login session, a CLI user token or a personal API key. An agent or organization key gets `403 insufficient_scope`.

### POST `/v1/plan/projects/{project_id}/members/` · Beta

**Invite somebody, or a whole team, into a project**

Grants one person (`user_uuid`) or one team (`team_uuid`) access to the project: send exactly one of them; both or neither is `400 invalid_filter_value`. A team grant is live: whoever joins the team later is in, and whoever leaves is out. Every non-guest member can call it (with a login session or a personal API key); an agent or organization key gets `403 insufficient_scope`.

Writes a `project.member_added` event carrying `actor_is_self`, so the project's members can tell an invitation from somebody letting themselves in.

- **Auth:** CLI Bearer (write)
- **Rate limit:** `default`
- **Pagination:** No pagination

#### Path parameters

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `project_id` | string | Yes | The project's uuid. |

#### Headers

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `X-Dailybot-Agent-Name` | string | No | The name of the agent that executed this write on the person's behalf. Use it on multipart and body-less writes (DELETE, archive, restore); on JSON writes send the body field `agent_name` instead, which wins if both are present. Percent-encode the value as UTF-8. Control characters are stripped; a blank value means no agent. More than 128 characters, or a value that cannot be decoded, is `400 invalid_agent_attribution` (never truncated). An agent-type key, which is not bound to a person, gets `400 invalid_agent_attribution` if it sends it. The stamp never changes a permission answer. See [Agent attribution](/developers/plan/conventions#agent-attribution). |

#### Request body

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `user_uuid` | uuid | No | The person's user uuid. |
| `team_uuid` | uuid | No | A team's uuid, instead of `user_uuid`. It creates one live team grant: whoever joins the team later is in, and whoever leaves is out. |
| `agent_name` | string | No | The name of the agent that executed this write on the person's behalf (max 128 characters, blank means no agent). Takes priority over the `X-Dailybot-Agent-Name` header. See [Agent attribution](/developers/plan/conventions#agent-attribution). |

#### Response body

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `(body)` | BoardMember | Yes | A [BoardMember](#plan-project-members-list-boardmember) object. |

#### Error codes

| Status | When |
|--------|------|
| `400` | Send exactly one of `user_uuid` and `team_uuid`; both or neither is `invalid_filter_value`. `invalid_agent_attribution` means the agent name is invalid. |
| `401` | Missing, expired or malformed credential (`credential_absent`, `credential_expired`, `credential_malformed`). |
| `402` | Plan is not enabled for your organization yet (`plan_upgrade_required`). Expected during the Beta: write to support@dailybot.com. |
| `403` | Authenticated but not allowed: missing scope (`insufficient_scope`, which is also what an agent or organization key gets on an operation that needs a person, and what a personal key gets when its explicit Plan scopes do not cover the endpoint) or a guest account (`guest_not_allowed`). |
| `404` | Not found, or not visible to you. Both cases return the same body. |

#### Example (curl)

```bash
curl -sS -X POST "https://api.dailybot.com/v1/plan/projects/00000000-0000-4000-8000-000000000001/members/" \
  -H "Authorization: Bearer $DAILYBOT_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "user_uuid": "00000000-0000-4000-8000-00000000000c"
  }'
```

#### Scenario examples

##### CLI

```bash
dailybot plan project member add 00000000-0000-4000-8000-000000000001 --user 00000000-0000-4000-8000-00000000000c
```

#### Notes

- Scope: `tasks:admin` — container writes. A non-guest member can call it with a login session or a personal API key (a key with explicit Plan scopes needs `tasks:write`, which covers it); an agent or organization key gets `403 insufficient_scope`.
- Rate limit: 60 writes per minute per actor.
- Needs a person: call it with a login session, a CLI user token or a personal API key. An agent or organization key gets `403 insufficient_scope`.

### DELETE `/v1/plan/projects/{project_id}/members/{user_id}/` · Beta

**Remove somebody from a project**

Removes a person's explicit grant on the project. Every non-guest member can call it (with a login session or a personal API key); an agent or organization key cannot.

- **Auth:** CLI Bearer (write)
- **Rate limit:** `default`
- **Pagination:** No pagination

#### Path parameters

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `project_id` | string | Yes | The project's uuid. |
| `user_id` | string | Yes | The member's user uuid. |

#### Headers

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `X-Dailybot-Agent-Name` | string | No | The name of the agent that executed this write on the person's behalf. Use it on multipart and body-less writes (DELETE, archive, restore); on JSON writes send the body field `agent_name` instead, which wins if both are present. Percent-encode the value as UTF-8. Control characters are stripped; a blank value means no agent. More than 128 characters, or a value that cannot be decoded, is `400 invalid_agent_attribution` (never truncated). An agent-type key, which is not bound to a person, gets `400 invalid_agent_attribution` if it sends it. The stamp never changes a permission answer. See [Agent attribution](/developers/plan/conventions#agent-attribution). |

#### Error codes

| Status | When |
|--------|------|
| `400` | The agent name is invalid (`invalid_agent_attribution`). |
| `401` | Missing, expired or malformed credential (`credential_absent`, `credential_expired`, `credential_malformed`). |
| `402` | Plan is not enabled for your organization yet (`plan_upgrade_required`). Expected during the Beta: write to support@dailybot.com. |
| `403` | Authenticated but not allowed: missing scope (`insufficient_scope`, which is also what an agent or organization key gets on an operation that needs a person, and what a personal key gets when its explicit Plan scopes do not cover the endpoint) or a guest account (`guest_not_allowed`). |
| `404` | Not found, or not visible to you. Both cases return the same body. |

#### Example (curl)

```bash
curl -sS -X DELETE "https://api.dailybot.com/v1/plan/projects/00000000-0000-4000-8000-000000000001/members/00000000-0000-4000-8000-00000000000c/" \
  -H "Authorization: Bearer $DAILYBOT_TOKEN"
```

#### Scenario examples

##### CLI

```bash
dailybot plan project member remove 00000000-0000-4000-8000-000000000001 00000000-0000-4000-8000-00000000000c --yes
```

#### Notes

- Scope: `tasks:admin` — container writes. A non-guest member can call it with a login session or a personal API key (a key with explicit Plan scopes needs `tasks:write`, which covers it); an agent or organization key gets `403 insufficient_scope`.
- Rate limit: 60 writes per minute per actor.
- Needs a person: call it with a login session, a CLI user token or a personal API key. An agent or organization key gets `403 insufficient_scope`.

### PATCH `/v1/plan/projects/{project_id}/members/{user_id}/` · Beta

**Inspect a project membership grant (role is read-only)**

Project membership has no role column — org roles plus project visibility are the access model. Sending `role` returns `400`. An empty PATCH returns the current grant row.

- **Auth:** CLI Bearer (write)
- **Rate limit:** `default`
- **Pagination:** No pagination

#### Path parameters

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `project_id` | string | Yes | The project's uuid. |
| `user_id` | string | Yes | The member's user uuid. |

#### Headers

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `X-Dailybot-Agent-Name` | string | No | The name of the agent that executed this write on the person's behalf. Use it on multipart and body-less writes (DELETE, archive, restore); on JSON writes send the body field `agent_name` instead, which wins if both are present. Percent-encode the value as UTF-8. Control characters are stripped; a blank value means no agent. More than 128 characters, or a value that cannot be decoded, is `400 invalid_agent_attribution` (never truncated). An agent-type key, which is not bound to a person, gets `400 invalid_agent_attribution` if it sends it. The stamp never changes a permission answer. See [Agent attribution](/developers/plan/conventions#agent-attribution). |

#### Response body

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `(body)` | BoardMember | Yes | A [BoardMember](#plan-project-members-list-boardmember) object. |

#### Error codes

| Status | When |
|--------|------|
| `400` | Validation failed, or a filter, sort or `include` value was not recognised. The response `code` says which. `invalid_agent_attribution` means the agent name is invalid. |
| `401` | Missing, expired or malformed credential (`credential_absent`, `credential_expired`, `credential_malformed`). |
| `402` | Plan is not enabled for your organization yet (`plan_upgrade_required`). Expected during the Beta: write to support@dailybot.com. |
| `404` | Not found, or not visible to you. Both cases return the same body. |

#### Example (curl)

```bash
curl -sS -X PATCH "https://api.dailybot.com/v1/plan/projects/00000000-0000-4000-8000-000000000001/members/00000000-0000-4000-8000-00000000000c/" \
  -H "Authorization: Bearer $DAILYBOT_TOKEN"
```

#### Notes

- Scope: `tasks:admin` — container writes. A non-guest member can call it with a login session or a personal API key (a key with explicit Plan scopes needs `tasks:write`, which covers it); an agent or organization key gets `403 insufficient_scope`.
- Rate limit: 60 writes per minute per actor.
- Needs a person: call it with a login session, a CLI user token or a personal API key. An agent or organization key gets `403 insufficient_scope`.

### GET `/v1/plan/projects/{project_id}/attachments/` · Beta

**List a project's attachments**

The project's attachments, ordered by position. Anyone who can see the project can list its attachments; a project you cannot see is `404`. Each `url` is a download link. Do not store it: keep the attachment `uuid` and read it again when you need the file. To show an image in the project's description, reference it as `attachment:{uuid}` and resolve it when you render, using the fresh `url` from this list.

- **Auth:** API key (`X-API-KEY`), CLI Bearer (read)
- **Rate limit:** `default`
- **Pagination:** Page-number pagination

#### Path parameters

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `project_id` | string | Yes | The project's uuid. |

#### Query parameters

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `page` | integer | No | 1-based page number. |
| `page_size` | integer | No | Rows per page. Default 50, maximum 100. Out-of-range values are clamped, never rejected: asking for 500 returns 100. |

#### TaskAttachment object

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `uuid` | uuid | Yes | Stable public identifier. |
| `filename` | string | Yes | File name. |
| `content_type` | string | Yes | MIME type. |
| `size` | integer | Yes | Size in bytes. |
| `url` | string | Yes | Where to download the file. |
| `thumbnail_url` | uri | null | No | Thumbnail for images. |
| `width` | integer | null | No | — |
| `height` | integer | null | No | — |
| `status` | enum | Yes | Current status. One of `pending`, `ready`, `scanning`, `rejected`. |
| `uploaded_by` | ActorRef | null | No | Who uploaded the file. See [ActorRef](#plan-projects-updates-digest-actorref). |
| `executed_by_agent` | object | null | No | The agent that executed this on behalf of the person, or `null` when no agent was named: an object with `uuid`, `name`, `username` and `avatar`. The person in the author field is still the author; the agent is shown as the one who executed it. |
| `created_at` | date-time | Yes | When the row was created. |

#### Response body

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `count` | integer | Yes | Total number of rows. |
| `next` | uri | Yes | URL of the next page, or `null`. |
| `previous` | uri | Yes | URL of the previous page, or `null`. |
| `results` | array<TaskAttachment> | Yes | The rows on this page. See [TaskAttachment](#plan-project-attachments-list-taskattachment). |

#### Error codes

| Status | When |
|--------|------|
| `401` | Missing, expired or malformed credential (`credential_absent`, `credential_expired`, `credential_malformed`). |
| `402` | Plan is not enabled for your organization yet (`plan_upgrade_required`). Expected during the Beta: write to support@dailybot.com. |
| `404` | The project or attachment does not exist or you cannot see it (`not_found`), never a 403. |

#### Example (curl)

```bash
curl -sS "https://api.dailybot.com/v1/plan/projects/00000000-0000-4000-8000-000000000001/attachments/" \
  -H "X-API-KEY: $DAILYBOT_API_KEY"
```

#### Scenario examples

##### CLI

```bash
dailybot plan project attachments 00000000-0000-4000-8000-000000000001 --json
```

##### Response

```bash
{
  "count": 1,
  "next": null,
  "previous": null,
  "results": [
    {
      "uuid": "00000000-0000-4000-8000-000000000009",
      "filename": "screenshot.png",
      "content_type": "image/png",
      "size": 1,
      "url": "https://your.app/files/screenshot.png",
      "thumbnail_url": null,
      "width": null,
      "height": null,
      "status": "ready",
      "uploaded_by": {
        "kind": "user",
        "uuid": "00000000-0000-4000-8000-00000000000c",
        "name": "Ada L."
      },
      "created_at": "2026-09-25T10:14:02Z"
    }
  ]
}
```

#### Notes

- Scope: `tasks:read`.
- Rate limit: 120 reads per minute per actor.
- Works with a login session, a CLI user token, a personal API key, or an agent or organization key. A personal key sees what its person sees; an agent or organization key acts as a system actor and sees organization-visible boards only.

### POST `/v1/plan/projects/{project_id}/attachments/` · Beta

**Upload an attachment to a project**

Attach a file to a project. Send `multipart/form-data` with the `file` field and an optional `caption`; there is no presign flow here. The limit is **5 MiB** in every environment: a larger file is `400 attachment_too_large`, with `extra.max_size_bytes`. The file type is checked from its content against the same list as task attachments (`attachment_invalid_type`). A project holds at most 50 attachments (`attachment_limit_reached`).

- **Auth:** CLI Bearer (write)
- **Rate limit:** `default`
- **Pagination:** No pagination

#### Path parameters

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `project_id` | string | Yes | The project's uuid. |

#### Headers

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `X-Dailybot-Agent-Name` | string | No | The name of the agent that executed this write on the person's behalf. Use it on multipart and body-less writes (DELETE, archive, restore); on JSON writes send the body field `agent_name` instead, which wins if both are present. Percent-encode the value as UTF-8. Control characters are stripped; a blank value means no agent. More than 128 characters, or a value that cannot be decoded, is `400 invalid_agent_attribution` (never truncated). An agent-type key, which is not bound to a person, gets `400 invalid_agent_attribution` if it sends it. The stamp never changes a permission answer. See [Agent attribution](/developers/plan/conventions#agent-attribution). |

#### Request body

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `file` | binary | Yes | The file to upload (max 5 MiB this way). |
| `caption` | string | No | Optional caption. Max 255 characters. |

#### Response body

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `(body)` | TaskAttachment | Yes | A [TaskAttachment](#plan-project-attachments-list-taskattachment) object. |

#### Error codes

| Status | When |
|--------|------|
| `400` | The file is missing, too large (`attachment_too_large`, over 5 MiB), of an unsupported type (`attachment_invalid_type`), or the limit of 50 is reached (`attachment_limit_reached`). `invalid_agent_attribution` means the agent name is invalid. |
| `401` | Missing, expired or malformed credential (`credential_absent`, `credential_expired`, `credential_malformed`). |
| `402` | Plan is not enabled for your organization yet (`plan_upgrade_required`). Expected during the Beta: write to support@dailybot.com. |
| `403` | Not a non-guest member acting with a login session or a personal API key (`insufficient_scope`); an agent or organization key always gets this. |
| `404` | The project or attachment does not exist or you cannot see it (`not_found`), never a 403. |

#### Example (curl)

```bash
curl -sS -X POST "https://api.dailybot.com/v1/plan/projects/00000000-0000-4000-8000-000000000001/attachments/" \
  -H "Authorization: Bearer $DAILYBOT_TOKEN" \
  -F "file=@./screenshot.png" \
  -F "caption=Staging dashboard"
```

#### Scenario examples

##### CLI

```bash
dailybot plan project attach 00000000-0000-4000-8000-000000000001 ./plan.pdf --caption "Launch plan"
```

#### Notes

- Scope: `tasks:admin` — container writes. A non-guest member can call it with a login session or a personal API key (a key with explicit Plan scopes needs `tasks:write`, which covers it); an agent or organization key gets `403 insufficient_scope`.
- Rate limit: 60 writes per minute per actor.
- Needs a person: call it with a login session, a CLI user token or a personal API key. An agent or organization key gets `403 insufficient_scope`.

### GET `/v1/plan/projects/{project_id}/attachments/{attachment_id}/content/` · Beta

**Download a project attachment's bytes**

Streams the file with the content type recorded at upload, `X-Content-Type-Options: nosniff` and `Cache-Control: no-store`. It never redirects to storage. Anyone who can see the project can download it.

- **Auth:** API key (`X-API-KEY`), CLI Bearer (read)
- **Rate limit:** `default`
- **Pagination:** No pagination

#### Path parameters

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `project_id` | string | Yes | The project's uuid. |
| `attachment_id` | string | Yes | The attachment's uuid. |

#### Error codes

| Status | When |
|--------|------|
| `401` | Missing, expired or malformed credential (`credential_absent`, `credential_expired`, `credential_malformed`). |
| `402` | Plan is not enabled for your organization yet (`plan_upgrade_required`). Expected during the Beta: write to support@dailybot.com. |
| `404` | The project or attachment does not exist or you cannot see it (`not_found`), never a 403. |

#### Example (curl)

```bash
curl -sS "https://api.dailybot.com/v1/plan/projects/00000000-0000-4000-8000-000000000001/attachments/00000000-0000-4000-8000-000000000009/content/" \
  -H "X-API-KEY: $DAILYBOT_API_KEY"
```

#### Scenario examples

##### CLI

```bash
dailybot plan project attachment get 00000000-0000-4000-8000-000000000001 00000000-0000-4000-8000-000000000009 -o ./plan.pdf
```

#### Notes

- Scope: `tasks:read`.
- Rate limit: 120 reads per minute per actor.
- Works with a login session, a CLI user token, a personal API key, or an agent or organization key. A personal key sees what its person sees; an agent or organization key acts as a system actor and sees organization-visible boards only.

### DELETE `/v1/plan/projects/{project_id}/attachments/{attachment_id}/` · Beta

**Remove an attachment from a project**

Removes the attachment from the project.

- **Auth:** CLI Bearer (write)
- **Rate limit:** `default`
- **Pagination:** No pagination

#### Path parameters

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `project_id` | string | Yes | The project's uuid. |
| `attachment_id` | string | Yes | The attachment's uuid. |

#### Headers

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `X-Dailybot-Agent-Name` | string | No | The name of the agent that executed this write on the person's behalf. Use it on multipart and body-less writes (DELETE, archive, restore); on JSON writes send the body field `agent_name` instead, which wins if both are present. Percent-encode the value as UTF-8. Control characters are stripped; a blank value means no agent. More than 128 characters, or a value that cannot be decoded, is `400 invalid_agent_attribution` (never truncated). An agent-type key, which is not bound to a person, gets `400 invalid_agent_attribution` if it sends it. The stamp never changes a permission answer. See [Agent attribution](/developers/plan/conventions#agent-attribution). |

#### Error codes

| Status | When |
|--------|------|
| `400` | The agent name is invalid (`invalid_agent_attribution`). |
| `401` | Missing, expired or malformed credential (`credential_absent`, `credential_expired`, `credential_malformed`). |
| `402` | Plan is not enabled for your organization yet (`plan_upgrade_required`). Expected during the Beta: write to support@dailybot.com. |
| `403` | Not a non-guest member acting with a login session or a personal API key (`insufficient_scope`); an agent or organization key always gets this. |
| `404` | The project or attachment does not exist or you cannot see it (`not_found`), never a 403. |

#### Example (curl)

```bash
curl -sS -X DELETE "https://api.dailybot.com/v1/plan/projects/00000000-0000-4000-8000-000000000001/attachments/00000000-0000-4000-8000-000000000009/" \
  -H "Authorization: Bearer $DAILYBOT_TOKEN"
```

#### Scenario examples

##### CLI

```bash
dailybot plan project attachment delete 00000000-0000-4000-8000-000000000001 00000000-0000-4000-8000-000000000009 --yes
```

#### Notes

- Scope: `tasks:admin` — container writes. A non-guest member can call it with a login session or a personal API key (a key with explicit Plan scopes needs `tasks:write`, which covers it); an agent or organization key gets `403 insufficient_scope`.
- Rate limit: 60 writes per minute per actor.
- Needs a person: call it with a login session, a CLI user token or a personal API key. An agent or organization key gets `403 insufficient_scope`.

### POST `/v1/plan/projects/{project_id}/milestones/{milestone_id}/restore/` · Beta

**Restore a retired milestone**

Brings a retired milestone back. It is the inverse of retiring a milestone with `DELETE`. Idempotent: a milestone that is not retired is returned unchanged. Send an `Idempotency-Key` to retry safely.

- **Auth:** API key (`X-API-KEY`), CLI Bearer (write)
- **Rate limit:** `default`
- **Pagination:** No pagination

#### Path parameters

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `project_id` | string | Yes | The project's uuid. |
| `milestone_id` | string | Yes | The milestone's uuid. |

#### Headers

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `X-Dailybot-Agent-Name` | string | No | The name of the agent that executed this write on the person's behalf. Use it on multipart and body-less writes (DELETE, archive, restore); on JSON writes send the body field `agent_name` instead, which wins if both are present. Percent-encode the value as UTF-8. Control characters are stripped; a blank value means no agent. More than 128 characters, or a value that cannot be decoded, is `400 invalid_agent_attribution` (never truncated). An agent-type key, which is not bound to a person, gets `400 invalid_agent_attribution` if it sends it. The stamp never changes a permission answer. See [Agent attribution](/developers/plan/conventions#agent-attribution). |

#### Response body

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `(body)` | ProjectMilestone | Yes | A [ProjectMilestone](#plan-project-milestones-list-projectmilestone) object. |

#### Error codes

| Status | When |
|--------|------|
| `400` | The agent name is invalid (`invalid_agent_attribution`). |
| `401` | Missing, expired or malformed credential (`credential_absent`, `credential_expired`, `credential_malformed`). |
| `402` | Plan is not enabled for your organization yet (`plan_upgrade_required`). Expected during the Beta: write to support@dailybot.com. |
| `403` | The credential cannot write to Plan (`insufficient_scope`), or the caller is a guest (`guest_not_allowed`). |
| `404` | The project or milestone does not exist or you cannot see it (`not_found`), never a 403. |

#### Example (curl)

```bash
curl -sS -X POST "https://api.dailybot.com/v1/plan/projects/00000000-0000-4000-8000-000000000001/milestones/00000000-0000-4000-8000-000000000007/restore/" \
  -H "Authorization: Bearer $DAILYBOT_TOKEN"
```

#### Notes

- Scope: `tasks:write`.
- Rate limit: 60 writes per minute per actor.
- Works with a login session, a CLI user token, a personal API key, or an agent or organization key. A personal key sees what its person sees; an agent or organization key acts as a system actor and sees organization-visible boards only.

### GET `/v1/plan/projects/{project_id}/milestones/{milestone_id}/attachments/` · Beta

**List a milestone's attachments**

The milestone's READY attachments, ordered by position. Anyone who can see the project can list them; a project you cannot see is `404`. Each `url` is a download link. Do not store it: keep the attachment `uuid` and read it again when you need the file. Reference it from the milestone `description` with an `attachment:{uuid}` marker; resolve it with `GET /v1/plan/attachments/resolve/` when you render.

- **Auth:** API key (`X-API-KEY`), CLI Bearer (read)
- **Rate limit:** `default`
- **Pagination:** Page-number pagination

#### Path parameters

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `project_id` | string | Yes | The project's uuid. |
| `milestone_id` | string | Yes | The milestone's uuid. |

#### Query parameters

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `page` | integer | No | 1-based page number. |
| `page_size` | integer | No | Rows per page. Default 50, maximum 100. Out-of-range values are clamped, never rejected: asking for 500 returns 100. |

#### TaskAttachment object

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `uuid` | uuid | Yes | Stable public identifier. |
| `filename` | string | Yes | File name. |
| `content_type` | string | Yes | MIME type. |
| `size` | integer | Yes | Size in bytes. |
| `url` | string | Yes | Where to download the file. |
| `thumbnail_url` | uri | null | No | Thumbnail for images. |
| `width` | integer | null | No | — |
| `height` | integer | null | No | — |
| `status` | enum | Yes | Current status. One of `pending`, `ready`, `scanning`, `rejected`. |
| `uploaded_by` | ActorRef | null | No | Who uploaded the file. See [ActorRef](#plan-projects-updates-digest-actorref). |
| `executed_by_agent` | object | null | No | The agent that executed this on behalf of the person, or `null` when no agent was named: an object with `uuid`, `name`, `username` and `avatar`. The person in the author field is still the author; the agent is shown as the one who executed it. |
| `created_at` | date-time | Yes | When the row was created. |

#### Response body

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `count` | integer | Yes | Total number of rows. |
| `next` | uri | Yes | URL of the next page, or `null`. |
| `previous` | uri | Yes | URL of the previous page, or `null`. |
| `results` | array<TaskAttachment> | Yes | The rows on this page. See [TaskAttachment](#plan-milestone-attachments-list-taskattachment). |

#### Error codes

| Status | When |
|--------|------|
| `401` | Missing, expired or malformed credential (`credential_absent`, `credential_expired`, `credential_malformed`). |
| `402` | Plan is not enabled for your organization yet (`plan_upgrade_required`). Expected during the Beta: write to support@dailybot.com. |
| `404` | The project or milestone does not exist or you cannot see it (`not_found`), never a 403. |

#### Example (curl)

```bash
curl -sS "https://api.dailybot.com/v1/plan/projects/00000000-0000-4000-8000-000000000001/milestones/00000000-0000-4000-8000-000000000007/attachments/" \
  -H "Authorization: Bearer $DAILYBOT_TOKEN"
```

#### Scenario examples

##### Response

```bash
{
  "count": 1,
  "next": null,
  "previous": null,
  "results": [
    {
      "uuid": "00000000-0000-4000-8000-000000000009",
      "filename": "roadmap.png",
      "content_type": "image/png",
      "size": 48213,
      "url": "https://your.app/files/roadmap.png",
      "thumbnail_url": null,
      "width": null,
      "height": null,
      "status": "ready",
      "uploaded_by": {
        "kind": "user",
        "uuid": "00000000-0000-4000-8000-00000000000c",
        "name": "Ada L."
      },
      "executed_by_agent": null,
      "created_at": "2026-09-29T10:14:02Z"
    }
  ]
}
```

#### Notes

- Scope: `tasks:read`.
- Rate limit: 120 reads per minute per actor.
- Works with a login session, a CLI user token, a personal API key, or an agent or organization key. A personal key sees what its person sees; an agent or organization key acts as a system actor and sees organization-visible boards only.

### POST `/v1/plan/projects/{project_id}/milestones/{milestone_id}/attachments/` · Beta

**Upload an attachment to a milestone**

Attaches a file to the milestone. Send `multipart/form-data` with the `file` field and an optional `caption`; there is no presign flow here. The limit is **5 MiB**: a larger file is `400 attachment_too_large`, with `extra.max_size_bytes`. The file type is checked from its content against the same list as project attachments (`attachment_invalid_type`). Attaching follows the milestone's own write rules. Reference it from the milestone `description` with an `attachment:{uuid}` marker; resolve it with `GET /v1/plan/attachments/resolve/` when you render.

- **Auth:** API key (`X-API-KEY`), CLI Bearer (write)
- **Rate limit:** `default`
- **Pagination:** No pagination

#### Path parameters

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `project_id` | string | Yes | The project's uuid. |
| `milestone_id` | string | Yes | The milestone's uuid. |

#### Headers

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `X-Dailybot-Agent-Name` | string | No | The name of the agent that executed this write on the person's behalf. Use it on multipart and body-less writes (DELETE, archive, restore); on JSON writes send the body field `agent_name` instead, which wins if both are present. Percent-encode the value as UTF-8. Control characters are stripped; a blank value means no agent. More than 128 characters, or a value that cannot be decoded, is `400 invalid_agent_attribution` (never truncated). An agent-type key, which is not bound to a person, gets `400 invalid_agent_attribution` if it sends it. The stamp never changes a permission answer. See [Agent attribution](/developers/plan/conventions#agent-attribution). |

#### Request body

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `file` | binary | Yes | The file to upload (max 5 MiB this way). |
| `caption` | string | No | Optional caption. Max 255 characters. |

#### Response body

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `(body)` | TaskAttachment | Yes | A [TaskAttachment](#plan-milestone-attachments-list-taskattachment) object. |

#### Error codes

| Status | When |
|--------|------|
| `400` | The file is missing, too large (`attachment_too_large`, over 5 MiB), of an unsupported type (`attachment_invalid_type`), or the limit of 50 is reached (`attachment_limit_reached`). `invalid_agent_attribution` means the agent name is invalid. |
| `401` | Missing, expired or malformed credential (`credential_absent`, `credential_expired`, `credential_malformed`). |
| `402` | Plan is not enabled for your organization yet (`plan_upgrade_required`). Expected during the Beta: write to support@dailybot.com. |
| `403` | The credential cannot write to Plan (`insufficient_scope`), or the caller is a guest (`guest_not_allowed`). |
| `404` | The project or milestone does not exist or you cannot see it (`not_found`), never a 403. |

#### Example (curl)

```bash
curl -sS -X POST "https://api.dailybot.com/v1/plan/projects/00000000-0000-4000-8000-000000000001/milestones/00000000-0000-4000-8000-000000000007/attachments/" \
  -H "Authorization: Bearer $DAILYBOT_TOKEN" \
  -F "file=@roadmap.png"
```

#### Scenario examples

##### Response

```bash
{
  "uuid": "00000000-0000-4000-8000-000000000009",
  "filename": "roadmap.png",
  "content_type": "image/png",
  "size": 48213,
  "url": "https://your.app/files/roadmap.png",
  "thumbnail_url": null,
  "width": null,
  "height": null,
  "status": "ready",
  "uploaded_by": {
    "kind": "user",
    "uuid": "00000000-0000-4000-8000-00000000000c",
    "name": "Ada L."
  },
  "executed_by_agent": null,
  "created_at": "2026-09-29T10:14:02Z"
}
```

#### Notes

- Scope: `tasks:write`.
- Rate limit: 60 writes per minute per actor.
- Works with a login session, a CLI user token, a personal API key, or an agent or organization key. A personal key sees what its person sees; an agent or organization key acts as a system actor and sees organization-visible boards only.

### GET `/v1/plan/projects/{project_id}/milestones/{milestone_id}/attachments/{attachment_id}/` · Beta

**Retrieve a milestone attachment**

One attachment of the milestone. Anyone who can see the project can read it.

- **Auth:** API key (`X-API-KEY`), CLI Bearer (read)
- **Rate limit:** `default`
- **Pagination:** No pagination

#### Path parameters

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `project_id` | string | Yes | The project's uuid. |
| `milestone_id` | string | Yes | The milestone's uuid. |
| `attachment_id` | string | Yes | The attachment's uuid. |

#### Response body

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `(body)` | TaskAttachment | Yes | A [TaskAttachment](#plan-milestone-attachments-list-taskattachment) object. |

#### Error codes

| Status | When |
|--------|------|
| `401` | Missing, expired or malformed credential (`credential_absent`, `credential_expired`, `credential_malformed`). |
| `402` | Plan is not enabled for your organization yet (`plan_upgrade_required`). Expected during the Beta: write to support@dailybot.com. |
| `404` | The project, the milestone or the attachment does not exist or you cannot see it (`not_found`), never a 403. |

#### Example (curl)

```bash
curl -sS "https://api.dailybot.com/v1/plan/projects/00000000-0000-4000-8000-000000000001/milestones/00000000-0000-4000-8000-000000000007/attachments/00000000-0000-4000-8000-000000000009/" \
  -H "Authorization: Bearer $DAILYBOT_TOKEN"
```

#### Scenario examples

##### Response

```bash
{
  "uuid": "00000000-0000-4000-8000-000000000009",
  "filename": "roadmap.png",
  "content_type": "image/png",
  "size": 48213,
  "url": "https://your.app/files/roadmap.png",
  "thumbnail_url": null,
  "width": null,
  "height": null,
  "status": "ready",
  "uploaded_by": {
    "kind": "user",
    "uuid": "00000000-0000-4000-8000-00000000000c",
    "name": "Ada L."
  },
  "executed_by_agent": null,
  "created_at": "2026-09-29T10:14:02Z"
}
```

#### Notes

- Scope: `tasks:read`.
- Rate limit: 120 reads per minute per actor.
- Works with a login session, a CLI user token, a personal API key, or an agent or organization key. A personal key sees what its person sees; an agent or organization key acts as a system actor and sees organization-visible boards only.

### PATCH `/v1/plan/projects/{project_id}/milestones/{milestone_id}/attachments/{attachment_id}/` · Beta

**Rename a milestone attachment**

Changes the attachment's file name; the content does not change. The rules are the milestone's own.

- **Auth:** API key (`X-API-KEY`), CLI Bearer (write)
- **Rate limit:** `default`
- **Pagination:** No pagination

#### Path parameters

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `project_id` | string | Yes | The project's uuid. |
| `milestone_id` | string | Yes | The milestone's uuid. |
| `attachment_id` | string | Yes | The attachment's uuid. |

#### Headers

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `X-Dailybot-Agent-Name` | string | No | The name of the agent that executed this write on the person's behalf. Use it on multipart and body-less writes (DELETE, archive, restore); on JSON writes send the body field `agent_name` instead, which wins if both are present. Percent-encode the value as UTF-8. Control characters are stripped; a blank value means no agent. More than 128 characters, or a value that cannot be decoded, is `400 invalid_agent_attribution` (never truncated). An agent-type key, which is not bound to a person, gets `400 invalid_agent_attribution` if it sends it. The stamp never changes a permission answer. See [Agent attribution](/developers/plan/conventions#agent-attribution). |

#### Request body

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `filename` | string | Yes | The new file name. |
| `agent_name` | string | No | The name of the agent that executed this write on the person's behalf (max 128 characters, blank means no agent). Takes priority over the `X-Dailybot-Agent-Name` header. See [Agent attribution](/developers/plan/conventions#agent-attribution). |

#### Response body

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `(body)` | TaskAttachment | Yes | A [TaskAttachment](#plan-milestone-attachments-list-taskattachment) object. |

#### Error codes

| Status | When |
|--------|------|
| `400` | The name is missing or not valid. `invalid_agent_attribution` means the agent name is invalid. |
| `401` | Missing, expired or malformed credential (`credential_absent`, `credential_expired`, `credential_malformed`). |
| `402` | Plan is not enabled for your organization yet (`plan_upgrade_required`). Expected during the Beta: write to support@dailybot.com. |
| `403` | The credential cannot write to Plan (`insufficient_scope`), or the caller is a guest (`guest_not_allowed`). |
| `404` | The project, the milestone or the attachment does not exist or you cannot see it (`not_found`), never a 403. |

#### Example (curl)

```bash
curl -sS -X PATCH "https://api.dailybot.com/v1/plan/projects/00000000-0000-4000-8000-000000000001/milestones/00000000-0000-4000-8000-000000000007/attachments/00000000-0000-4000-8000-000000000009/" \
  -H "Authorization: Bearer $DAILYBOT_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"filename": "roadmap-v2.png"}'
```

#### Scenario examples

##### Response

```bash
{
  "uuid": "00000000-0000-4000-8000-000000000009",
  "filename": "roadmap-v2.png",
  "content_type": "image/png",
  "size": 48213,
  "url": "https://your.app/files/roadmap-v2.png",
  "thumbnail_url": null,
  "width": null,
  "height": null,
  "status": "ready",
  "uploaded_by": {
    "kind": "user",
    "uuid": "00000000-0000-4000-8000-00000000000c",
    "name": "Ada L."
  },
  "executed_by_agent": null,
  "created_at": "2026-09-29T10:14:02Z"
}
```

#### Notes

- Scope: `tasks:write`.
- Rate limit: 60 writes per minute per actor.
- Works with a login session, a CLI user token, a personal API key, or an agent or organization key. A personal key sees what its person sees; an agent or organization key acts as a system actor and sees organization-visible boards only.

### DELETE `/v1/plan/projects/{project_id}/milestones/{milestone_id}/attachments/{attachment_id}/` · Beta

**Remove a milestone attachment**

Removes the attachment. The rules are the milestone's own.

- **Auth:** API key (`X-API-KEY`), CLI Bearer (write)
- **Rate limit:** `default`
- **Pagination:** No pagination

#### Path parameters

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `project_id` | string | Yes | The project's uuid. |
| `milestone_id` | string | Yes | The milestone's uuid. |
| `attachment_id` | string | Yes | The attachment's uuid. |

#### Headers

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `X-Dailybot-Agent-Name` | string | No | The name of the agent that executed this write on the person's behalf. Use it on multipart and body-less writes (DELETE, archive, restore); on JSON writes send the body field `agent_name` instead, which wins if both are present. Percent-encode the value as UTF-8. Control characters are stripped; a blank value means no agent. More than 128 characters, or a value that cannot be decoded, is `400 invalid_agent_attribution` (never truncated). An agent-type key, which is not bound to a person, gets `400 invalid_agent_attribution` if it sends it. The stamp never changes a permission answer. See [Agent attribution](/developers/plan/conventions#agent-attribution). |

#### Error codes

| Status | When |
|--------|------|
| `400` | The agent name is invalid (`invalid_agent_attribution`). |
| `401` | Missing, expired or malformed credential (`credential_absent`, `credential_expired`, `credential_malformed`). |
| `402` | Plan is not enabled for your organization yet (`plan_upgrade_required`). Expected during the Beta: write to support@dailybot.com. |
| `403` | The credential cannot write to Plan (`insufficient_scope`), or the caller is a guest (`guest_not_allowed`). |
| `404` | The project, the milestone or the attachment does not exist or you cannot see it (`not_found`), never a 403. |

#### Example (curl)

```bash
curl -sS -X DELETE "https://api.dailybot.com/v1/plan/projects/00000000-0000-4000-8000-000000000001/milestones/00000000-0000-4000-8000-000000000007/attachments/00000000-0000-4000-8000-000000000009/" \
  -H "Authorization: Bearer $DAILYBOT_TOKEN"
```

#### Notes

- Scope: `tasks:write`.
- Rate limit: 60 writes per minute per actor.
- Works with a login session, a CLI user token, a personal API key, or an agent or organization key. A personal key sees what its person sees; an agent or organization key acts as a system actor and sees organization-visible boards only.

### GET `/v1/plan/projects/{project_id}/milestones/{milestone_id}/attachments/{attachment_id}/content/` · Beta

**Download a milestone attachment's bytes**

Streams the file with the content type recorded at upload, `X-Content-Type-Options: nosniff` and `Cache-Control: no-store`. It never redirects to storage. Anyone who can see the project can download it.

- **Auth:** API key (`X-API-KEY`), CLI Bearer (read)
- **Rate limit:** `default`
- **Pagination:** No pagination

#### Path parameters

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `project_id` | string | Yes | The project's uuid. |
| `milestone_id` | string | Yes | The milestone's uuid. |
| `attachment_id` | string | Yes | The attachment's uuid. |

#### Error codes

| Status | When |
|--------|------|
| `401` | Missing, expired or malformed credential (`credential_absent`, `credential_expired`, `credential_malformed`). |
| `402` | Plan is not enabled for your organization yet (`plan_upgrade_required`). Expected during the Beta: write to support@dailybot.com. |
| `404` | The project, the milestone or the attachment does not exist or you cannot see it (`not_found`), never a 403. |
| `409` | The attachment is not ready yet (`attachment_not_ready`). |

#### Example (curl)

```bash
curl -sS "https://api.dailybot.com/v1/plan/projects/00000000-0000-4000-8000-000000000001/milestones/00000000-0000-4000-8000-000000000007/attachments/00000000-0000-4000-8000-000000000009/content/" \
  -H "Authorization: Bearer $DAILYBOT_TOKEN" -o roadmap.png
```

#### Notes

- Scope: `tasks:read`.
- Rate limit: 120 reads per minute per actor.
- Works with a login session, a CLI user token, a personal API key, or an agent or organization key. A personal key sees what its person sees; an agent or organization key acts as a system actor and sees organization-visible boards only.

### GET `/v1/plan/projects/{project_id}/updates/{update_id}/` · Beta

**Get a project update**

One status note, with its READY `attachments` inline, `provenance`, `edited_at` (null until the first edit) and `executed_by_agent`.

- **Auth:** API key (`X-API-KEY`), CLI Bearer (read)
- **Rate limit:** `default`
- **Pagination:** No pagination

#### Path parameters

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `project_id` | string | Yes | The project's uuid. |
| `update_id` | string | Yes | The update's uuid. |

#### Response body

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `(body)` | ProjectUpdate | Yes | A [ProjectUpdate](#plan-projects-updates-digest-projectupdate) object. |

#### Error codes

| Status | When |
|--------|------|
| `401` | Missing, expired or malformed credential (`credential_absent`, `credential_expired`, `credential_malformed`). |
| `402` | Plan is not enabled for your organization yet (`plan_upgrade_required`). Expected during the Beta: write to support@dailybot.com. |
| `404` | The project or update does not exist or you cannot see it (`not_found`), never a 403. |

#### Example (curl)

```bash
curl -sS "https://api.dailybot.com/v1/plan/projects/00000000-0000-4000-8000-000000000001/updates/00000000-0000-4000-8000-00000000000a/" \
  -H "Authorization: Bearer $DAILYBOT_TOKEN"
```

#### Scenario examples

##### Response

```bash
{
  "uuid": "00000000-0000-4000-8000-00000000000a",
  "project": "00000000-0000-4000-8000-000000000001",
  "body": "Staging is green; rolling out Friday.",
  "body_html": "<p>Staging is green; rolling out Friday.</p>",
  "mentions": [],
  "health": "on_track",
  "created_by": {
    "kind": "user",
    "uuid": "00000000-0000-4000-8000-00000000000c",
    "name": "Ada L."
  },
  "executed_by_agent": null,
  "provenance": "typed",
  "edited_at": null,
  "attachments": [],
  "created_at": "2026-09-29T10:14:02Z",
  "updated_at": "2026-09-29T10:14:02Z"
}
```

#### Notes

- Scope: `tasks:read`.
- Rate limit: 120 reads per minute per actor.
- Works with a login session, a CLI user token, a personal API key, or an agent or organization key. A personal key sees what its person sees; an agent or organization key acts as a system actor and sees organization-visible boards only.

### PATCH `/v1/plan/projects/{project_id}/updates/{update_id}/` · Beta

**Edit a project update**

Changes `body` and/or `health` (`null` clears it) and stamps `edited_at`. Only the author can edit (`403 update_not_author`). `created_by` and the original `executed_by_agent` never change; an edit made through an API key, or stamped with an agent, makes `provenance` `agent_authored`. To place images inline, upload them to the update's attachments and add `attachment:{uuid}` markers to `body`. Send `If-Match` or a body `version` to avoid overwriting a concurrent edit.

- **Auth:** API key (`X-API-KEY`), CLI Bearer (write)
- **Rate limit:** `default`
- **Pagination:** No pagination

#### Path parameters

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `project_id` | string | Yes | The project's uuid. |
| `update_id` | string | Yes | The update's uuid. |

#### Headers

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `X-Dailybot-Agent-Name` | string | No | The name of the agent that executed this write on the person's behalf. Use it on multipart and body-less writes (DELETE, archive, restore); on JSON writes send the body field `agent_name` instead, which wins if both are present. Percent-encode the value as UTF-8. Control characters are stripped; a blank value means no agent. More than 128 characters, or a value that cannot be decoded, is `400 invalid_agent_attribution` (never truncated). An agent-type key, which is not bound to a person, gets `400 invalid_agent_attribution` if it sends it. The stamp never changes a permission answer. See [Agent attribution](/developers/plan/conventions#agent-attribution). |

#### Request body

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `body` | string | No | Markdown. Max 20000 characters. |
| `health` | enum | null | No | Declared health. One of `not_set`, `on_track`, `at_risk`, `off_track`. |
| `agent_name` | string | No | The name of the agent that executed this write on the person's behalf (max 128 characters, blank means no agent). Takes priority over the `X-Dailybot-Agent-Name` header. See [Agent attribution](/developers/plan/conventions#agent-attribution). |

#### Response body

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `(body)` | ProjectUpdate | Yes | A [ProjectUpdate](#plan-projects-updates-digest-projectupdate) object. |

#### Error codes

| Status | When |
|--------|------|
| `400` | The body is empty or too long (`update_body_too_long`), or `health` is not valid. `invalid_agent_attribution` means the agent name is invalid. |
| `401` | Missing, expired or malformed credential (`credential_absent`, `credential_expired`, `credential_malformed`). |
| `402` | Plan is not enabled for your organization yet (`plan_upgrade_required`). Expected during the Beta: write to support@dailybot.com. |
| `403` | Only the update's author can do this (`update_not_author`). |
| `404` | The project or update does not exist or you cannot see it (`not_found`), never a 403. |

#### Example (curl)

```bash
curl -sS -X PATCH "https://api.dailybot.com/v1/plan/projects/00000000-0000-4000-8000-000000000001/updates/00000000-0000-4000-8000-00000000000a/" \
  -H "Authorization: Bearer $DAILYBOT_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"body": "Staging is green; rolling out Friday.", "health": "on_track"}'
```

#### Scenario examples

##### Response

```bash
{
  "uuid": "00000000-0000-4000-8000-00000000000a",
  "project": "00000000-0000-4000-8000-000000000001",
  "body": "Staging is green; rolling out Friday.",
  "body_html": "<p>Staging is green; rolling out Friday.</p>",
  "mentions": [],
  "health": "on_track",
  "created_by": {
    "kind": "user",
    "uuid": "00000000-0000-4000-8000-00000000000c",
    "name": "Ada L."
  },
  "executed_by_agent": null,
  "provenance": "typed",
  "edited_at": "2026-09-29T11:02:00Z",
  "attachments": [],
  "created_at": "2026-09-29T10:14:02Z",
  "updated_at": "2026-09-29T11:02:00Z"
}
```

#### Notes

- Scope: `tasks:write`.
- Rate limit: 60 writes per minute per actor.
- Works with a login session, a CLI user token, a personal API key, or an agent or organization key. A personal key sees what its person sees; an agent or organization key acts as a system actor and sees organization-visible boards only.

### DELETE `/v1/plan/projects/{project_id}/updates/{update_id}/` · Beta

**Delete a project update**

Removes the update and its attachments; a stored file is deleted once nothing else references it. The author or an organization admin can delete (`403 update_not_author` for anyone else).

- **Auth:** API key (`X-API-KEY`), CLI Bearer (write)
- **Rate limit:** `default`
- **Pagination:** No pagination

#### Path parameters

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `project_id` | string | Yes | The project's uuid. |
| `update_id` | string | Yes | The update's uuid. |

#### Headers

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `X-Dailybot-Agent-Name` | string | No | The name of the agent that executed this write on the person's behalf. Use it on multipart and body-less writes (DELETE, archive, restore); on JSON writes send the body field `agent_name` instead, which wins if both are present. Percent-encode the value as UTF-8. Control characters are stripped; a blank value means no agent. More than 128 characters, or a value that cannot be decoded, is `400 invalid_agent_attribution` (never truncated). An agent-type key, which is not bound to a person, gets `400 invalid_agent_attribution` if it sends it. The stamp never changes a permission answer. See [Agent attribution](/developers/plan/conventions#agent-attribution). |

#### Error codes

| Status | When |
|--------|------|
| `400` | The agent name is invalid (`invalid_agent_attribution`). |
| `401` | Missing, expired or malformed credential (`credential_absent`, `credential_expired`, `credential_malformed`). |
| `402` | Plan is not enabled for your organization yet (`plan_upgrade_required`). Expected during the Beta: write to support@dailybot.com. |
| `403` | Only the update's author can do this (`update_not_author`). |
| `404` | The project or update does not exist or you cannot see it (`not_found`), never a 403. |

#### Example (curl)

```bash
curl -sS -X DELETE "https://api.dailybot.com/v1/plan/projects/00000000-0000-4000-8000-000000000001/updates/00000000-0000-4000-8000-00000000000a/" \
  -H "Authorization: Bearer $DAILYBOT_TOKEN"
```

#### Notes

- Scope: `tasks:write`.
- Rate limit: 60 writes per minute per actor.
- Works with a login session, a CLI user token, a personal API key, or an agent or organization key. A personal key sees what its person sees; an agent or organization key acts as a system actor and sees organization-visible boards only.

### GET `/v1/plan/projects/{project_id}/updates/{update_id}/attachments/` · Beta

**List a update's attachments**

The update's READY attachments, ordered by position. Anyone who can see the project can list them; a project you cannot see is `404`. Each `url` is a download link. Do not store it: keep the attachment `uuid` and read it again when you need the file. Reference it from the update `body` with an `attachment:{uuid}` marker; resolve it with `GET /v1/plan/attachments/resolve/` when you render.

- **Auth:** API key (`X-API-KEY`), CLI Bearer (read)
- **Rate limit:** `default`
- **Pagination:** Page-number pagination

#### Path parameters

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `project_id` | string | Yes | The project's uuid. |
| `update_id` | string | Yes | The update's uuid. |

#### Query parameters

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `page` | integer | No | 1-based page number. |
| `page_size` | integer | No | Rows per page. Default 50, maximum 100. Out-of-range values are clamped, never rejected: asking for 500 returns 100. |

#### TaskAttachment object

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `uuid` | uuid | Yes | Stable public identifier. |
| `filename` | string | Yes | File name. |
| `content_type` | string | Yes | MIME type. |
| `size` | integer | Yes | Size in bytes. |
| `url` | string | Yes | Where to download the file. |
| `thumbnail_url` | uri | null | No | Thumbnail for images. |
| `width` | integer | null | No | — |
| `height` | integer | null | No | — |
| `status` | enum | Yes | Current status. One of `pending`, `ready`, `scanning`, `rejected`. |
| `uploaded_by` | ActorRef | null | No | Who uploaded the file. See [ActorRef](#plan-projects-updates-digest-actorref). |
| `executed_by_agent` | object | null | No | The agent that executed this on behalf of the person, or `null` when no agent was named: an object with `uuid`, `name`, `username` and `avatar`. The person in the author field is still the author; the agent is shown as the one who executed it. |
| `created_at` | date-time | Yes | When the row was created. |

#### Response body

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `count` | integer | Yes | Total number of rows. |
| `next` | uri | Yes | URL of the next page, or `null`. |
| `previous` | uri | Yes | URL of the previous page, or `null`. |
| `results` | array<TaskAttachment> | Yes | The rows on this page. See [TaskAttachment](#plan-project-update-attachments-list-taskattachment). |

#### Error codes

| Status | When |
|--------|------|
| `401` | Missing, expired or malformed credential (`credential_absent`, `credential_expired`, `credential_malformed`). |
| `402` | Plan is not enabled for your organization yet (`plan_upgrade_required`). Expected during the Beta: write to support@dailybot.com. |
| `404` | The project or update does not exist or you cannot see it (`not_found`), never a 403. |

#### Example (curl)

```bash
curl -sS "https://api.dailybot.com/v1/plan/projects/00000000-0000-4000-8000-000000000001/updates/00000000-0000-4000-8000-00000000000a/attachments/" \
  -H "Authorization: Bearer $DAILYBOT_TOKEN"
```

#### Scenario examples

##### Response

```bash
{
  "count": 1,
  "next": null,
  "previous": null,
  "results": [
    {
      "uuid": "00000000-0000-4000-8000-000000000009",
      "filename": "roadmap.png",
      "content_type": "image/png",
      "size": 48213,
      "url": "https://your.app/files/roadmap.png",
      "thumbnail_url": null,
      "width": null,
      "height": null,
      "status": "ready",
      "uploaded_by": {
        "kind": "user",
        "uuid": "00000000-0000-4000-8000-00000000000c",
        "name": "Ada L."
      },
      "executed_by_agent": null,
      "created_at": "2026-09-29T10:14:02Z"
    }
  ]
}
```

#### Notes

- Scope: `tasks:read`.
- Rate limit: 120 reads per minute per actor.
- Works with a login session, a CLI user token, a personal API key, or an agent or organization key. A personal key sees what its person sees; an agent or organization key acts as a system actor and sees organization-visible boards only.

### POST `/v1/plan/projects/{project_id}/updates/{update_id}/attachments/` · Beta

**Upload an attachment to a update**

Attaches a file to the update. Send `multipart/form-data` with the `file` field and an optional `caption`; there is no presign flow here. The limit is **5 MiB**: a larger file is `400 attachment_too_large`, with `extra.max_size_bytes`. The file type is checked from its content against the same list as project attachments (`attachment_invalid_type`). Only the update's author can attach (`403 update_not_author`). Upload first, then add the marker to the update's `body` with a `PATCH`. Reference it from the update `body` with an `attachment:{uuid}` marker; resolve it with `GET /v1/plan/attachments/resolve/` when you render.

- **Auth:** API key (`X-API-KEY`), CLI Bearer (write)
- **Rate limit:** `default`
- **Pagination:** No pagination

#### Path parameters

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `project_id` | string | Yes | The project's uuid. |
| `update_id` | string | Yes | The update's uuid. |

#### Headers

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `X-Dailybot-Agent-Name` | string | No | The name of the agent that executed this write on the person's behalf. Use it on multipart and body-less writes (DELETE, archive, restore); on JSON writes send the body field `agent_name` instead, which wins if both are present. Percent-encode the value as UTF-8. Control characters are stripped; a blank value means no agent. More than 128 characters, or a value that cannot be decoded, is `400 invalid_agent_attribution` (never truncated). An agent-type key, which is not bound to a person, gets `400 invalid_agent_attribution` if it sends it. The stamp never changes a permission answer. See [Agent attribution](/developers/plan/conventions#agent-attribution). |

#### Request body

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `file` | binary | Yes | The file to upload (max 5 MiB this way). |
| `caption` | string | No | Optional caption. Max 255 characters. |

#### Response body

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `(body)` | TaskAttachment | Yes | A [TaskAttachment](#plan-project-update-attachments-list-taskattachment) object. |

#### Error codes

| Status | When |
|--------|------|
| `400` | The file is missing, too large (`attachment_too_large`, over 5 MiB), of an unsupported type (`attachment_invalid_type`), or the limit of 50 is reached (`attachment_limit_reached`). `invalid_agent_attribution` means the agent name is invalid. |
| `401` | Missing, expired or malformed credential (`credential_absent`, `credential_expired`, `credential_malformed`). |
| `402` | Plan is not enabled for your organization yet (`plan_upgrade_required`). Expected during the Beta: write to support@dailybot.com. |
| `403` | Only the update's author can do this (`update_not_author`). |
| `404` | The project or update does not exist or you cannot see it (`not_found`), never a 403. |

#### Example (curl)

```bash
curl -sS -X POST "https://api.dailybot.com/v1/plan/projects/00000000-0000-4000-8000-000000000001/updates/00000000-0000-4000-8000-00000000000a/attachments/" \
  -H "Authorization: Bearer $DAILYBOT_TOKEN" \
  -F "file=@roadmap.png"
```

#### Scenario examples

##### Response

```bash
{
  "uuid": "00000000-0000-4000-8000-000000000009",
  "filename": "roadmap.png",
  "content_type": "image/png",
  "size": 48213,
  "url": "https://your.app/files/roadmap.png",
  "thumbnail_url": null,
  "width": null,
  "height": null,
  "status": "ready",
  "uploaded_by": {
    "kind": "user",
    "uuid": "00000000-0000-4000-8000-00000000000c",
    "name": "Ada L."
  },
  "executed_by_agent": null,
  "created_at": "2026-09-29T10:14:02Z"
}
```

#### Notes

- Scope: `tasks:write`.
- Rate limit: 60 writes per minute per actor.
- Works with a login session, a CLI user token, a personal API key, or an agent or organization key. A personal key sees what its person sees; an agent or organization key acts as a system actor and sees organization-visible boards only.

### GET `/v1/plan/projects/{project_id}/updates/{update_id}/attachments/{attachment_id}/` · Beta

**Retrieve a update attachment**

One attachment of the update. Anyone who can see the project can read it.

- **Auth:** API key (`X-API-KEY`), CLI Bearer (read)
- **Rate limit:** `default`
- **Pagination:** No pagination

#### Path parameters

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `project_id` | string | Yes | The project's uuid. |
| `update_id` | string | Yes | The update's uuid. |
| `attachment_id` | string | Yes | The attachment's uuid. |

#### Response body

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `(body)` | TaskAttachment | Yes | A [TaskAttachment](#plan-project-update-attachments-list-taskattachment) object. |

#### Error codes

| Status | When |
|--------|------|
| `401` | Missing, expired or malformed credential (`credential_absent`, `credential_expired`, `credential_malformed`). |
| `402` | Plan is not enabled for your organization yet (`plan_upgrade_required`). Expected during the Beta: write to support@dailybot.com. |
| `404` | The project, the update or the attachment does not exist or you cannot see it (`not_found`), never a 403. |

#### Example (curl)

```bash
curl -sS "https://api.dailybot.com/v1/plan/projects/00000000-0000-4000-8000-000000000001/updates/00000000-0000-4000-8000-00000000000a/attachments/00000000-0000-4000-8000-000000000009/" \
  -H "Authorization: Bearer $DAILYBOT_TOKEN"
```

#### Scenario examples

##### Response

```bash
{
  "uuid": "00000000-0000-4000-8000-000000000009",
  "filename": "roadmap.png",
  "content_type": "image/png",
  "size": 48213,
  "url": "https://your.app/files/roadmap.png",
  "thumbnail_url": null,
  "width": null,
  "height": null,
  "status": "ready",
  "uploaded_by": {
    "kind": "user",
    "uuid": "00000000-0000-4000-8000-00000000000c",
    "name": "Ada L."
  },
  "executed_by_agent": null,
  "created_at": "2026-09-29T10:14:02Z"
}
```

#### Notes

- Scope: `tasks:read`.
- Rate limit: 120 reads per minute per actor.
- Works with a login session, a CLI user token, a personal API key, or an agent or organization key. A personal key sees what its person sees; an agent or organization key acts as a system actor and sees organization-visible boards only.

### PATCH `/v1/plan/projects/{project_id}/updates/{update_id}/attachments/{attachment_id}/` · Beta

**Rename a update attachment**

Changes the attachment's file name; the content does not change. Only the update's author can rename it (`403 update_not_author`).

- **Auth:** API key (`X-API-KEY`), CLI Bearer (write)
- **Rate limit:** `default`
- **Pagination:** No pagination

#### Path parameters

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `project_id` | string | Yes | The project's uuid. |
| `update_id` | string | Yes | The update's uuid. |
| `attachment_id` | string | Yes | The attachment's uuid. |

#### Headers

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `X-Dailybot-Agent-Name` | string | No | The name of the agent that executed this write on the person's behalf. Use it on multipart and body-less writes (DELETE, archive, restore); on JSON writes send the body field `agent_name` instead, which wins if both are present. Percent-encode the value as UTF-8. Control characters are stripped; a blank value means no agent. More than 128 characters, or a value that cannot be decoded, is `400 invalid_agent_attribution` (never truncated). An agent-type key, which is not bound to a person, gets `400 invalid_agent_attribution` if it sends it. The stamp never changes a permission answer. See [Agent attribution](/developers/plan/conventions#agent-attribution). |

#### Request body

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `filename` | string | Yes | The new file name. |
| `agent_name` | string | No | The name of the agent that executed this write on the person's behalf (max 128 characters, blank means no agent). Takes priority over the `X-Dailybot-Agent-Name` header. See [Agent attribution](/developers/plan/conventions#agent-attribution). |

#### Response body

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `(body)` | TaskAttachment | Yes | A [TaskAttachment](#plan-project-update-attachments-list-taskattachment) object. |

#### Error codes

| Status | When |
|--------|------|
| `400` | The name is missing or not valid. `invalid_agent_attribution` means the agent name is invalid. |
| `401` | Missing, expired or malformed credential (`credential_absent`, `credential_expired`, `credential_malformed`). |
| `402` | Plan is not enabled for your organization yet (`plan_upgrade_required`). Expected during the Beta: write to support@dailybot.com. |
| `403` | Only the update's author can do this (`update_not_author`). |
| `404` | The project, the update or the attachment does not exist or you cannot see it (`not_found`), never a 403. |

#### Example (curl)

```bash
curl -sS -X PATCH "https://api.dailybot.com/v1/plan/projects/00000000-0000-4000-8000-000000000001/updates/00000000-0000-4000-8000-00000000000a/attachments/00000000-0000-4000-8000-000000000009/" \
  -H "Authorization: Bearer $DAILYBOT_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"filename": "roadmap-v2.png"}'
```

#### Scenario examples

##### Response

```bash
{
  "uuid": "00000000-0000-4000-8000-000000000009",
  "filename": "roadmap-v2.png",
  "content_type": "image/png",
  "size": 48213,
  "url": "https://your.app/files/roadmap-v2.png",
  "thumbnail_url": null,
  "width": null,
  "height": null,
  "status": "ready",
  "uploaded_by": {
    "kind": "user",
    "uuid": "00000000-0000-4000-8000-00000000000c",
    "name": "Ada L."
  },
  "executed_by_agent": null,
  "created_at": "2026-09-29T10:14:02Z"
}
```

#### Notes

- Scope: `tasks:write`.
- Rate limit: 60 writes per minute per actor.
- Works with a login session, a CLI user token, a personal API key, or an agent or organization key. A personal key sees what its person sees; an agent or organization key acts as a system actor and sees organization-visible boards only.

### DELETE `/v1/plan/projects/{project_id}/updates/{update_id}/attachments/{attachment_id}/` · Beta

**Remove a update attachment**

Removes the attachment. The update's author can remove it, and so can an organization admin (`403 update_not_author` for anyone else).

- **Auth:** API key (`X-API-KEY`), CLI Bearer (write)
- **Rate limit:** `default`
- **Pagination:** No pagination

#### Path parameters

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `project_id` | string | Yes | The project's uuid. |
| `update_id` | string | Yes | The update's uuid. |
| `attachment_id` | string | Yes | The attachment's uuid. |

#### Headers

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `X-Dailybot-Agent-Name` | string | No | The name of the agent that executed this write on the person's behalf. Use it on multipart and body-less writes (DELETE, archive, restore); on JSON writes send the body field `agent_name` instead, which wins if both are present. Percent-encode the value as UTF-8. Control characters are stripped; a blank value means no agent. More than 128 characters, or a value that cannot be decoded, is `400 invalid_agent_attribution` (never truncated). An agent-type key, which is not bound to a person, gets `400 invalid_agent_attribution` if it sends it. The stamp never changes a permission answer. See [Agent attribution](/developers/plan/conventions#agent-attribution). |

#### Error codes

| Status | When |
|--------|------|
| `400` | The agent name is invalid (`invalid_agent_attribution`). |
| `401` | Missing, expired or malformed credential (`credential_absent`, `credential_expired`, `credential_malformed`). |
| `402` | Plan is not enabled for your organization yet (`plan_upgrade_required`). Expected during the Beta: write to support@dailybot.com. |
| `403` | Only the update's author can do this (`update_not_author`). |
| `404` | The project, the update or the attachment does not exist or you cannot see it (`not_found`), never a 403. |

#### Example (curl)

```bash
curl -sS -X DELETE "https://api.dailybot.com/v1/plan/projects/00000000-0000-4000-8000-000000000001/updates/00000000-0000-4000-8000-00000000000a/attachments/00000000-0000-4000-8000-000000000009/" \
  -H "Authorization: Bearer $DAILYBOT_TOKEN"
```

#### Notes

- Scope: `tasks:write`.
- Rate limit: 60 writes per minute per actor.
- Works with a login session, a CLI user token, a personal API key, or an agent or organization key. A personal key sees what its person sees; an agent or organization key acts as a system actor and sees organization-visible boards only.

### GET `/v1/plan/projects/{project_id}/updates/{update_id}/attachments/{attachment_id}/content/` · Beta

**Download a update attachment's bytes**

Streams the file with the content type recorded at upload, `X-Content-Type-Options: nosniff` and `Cache-Control: no-store`. It never redirects to storage. Anyone who can see the project can download it.

- **Auth:** API key (`X-API-KEY`), CLI Bearer (read)
- **Rate limit:** `default`
- **Pagination:** No pagination

#### Path parameters

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `project_id` | string | Yes | The project's uuid. |
| `update_id` | string | Yes | The update's uuid. |
| `attachment_id` | string | Yes | The attachment's uuid. |

#### Error codes

| Status | When |
|--------|------|
| `401` | Missing, expired or malformed credential (`credential_absent`, `credential_expired`, `credential_malformed`). |
| `402` | Plan is not enabled for your organization yet (`plan_upgrade_required`). Expected during the Beta: write to support@dailybot.com. |
| `404` | The project, the update or the attachment does not exist or you cannot see it (`not_found`), never a 403. |
| `409` | The attachment is not ready yet (`attachment_not_ready`). |

#### Example (curl)

```bash
curl -sS "https://api.dailybot.com/v1/plan/projects/00000000-0000-4000-8000-000000000001/updates/00000000-0000-4000-8000-00000000000a/attachments/00000000-0000-4000-8000-000000000009/content/" \
  -H "Authorization: Bearer $DAILYBOT_TOKEN" -o roadmap.png
```

#### Notes

- Scope: `tasks:read`.
- Rate limit: 120 reads per minute per actor.
- Works with a login session, a CLI user token, a personal API key, or an agent or organization key. A personal key sees what its person sees; an agent or organization key acts as a system actor and sees organization-visible boards only.

### PATCH `/v1/plan/projects/{project_id}/attachments/{attachment_id}/` · Beta

**Rename a project attachment**

Changes the display file name; the stored bytes do not change. The rules are the container's own: organization administrators only.

- **Auth:** CLI Bearer (write)
- **Rate limit:** `default`
- **Pagination:** No pagination

#### Path parameters

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `project_id` | string | Yes | The project's uuid. |
| `attachment_id` | uuid | Yes | The attachment's uuid. |

#### Headers

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `X-Dailybot-Agent-Name` | string | No | The name of the agent that executed this write on the person's behalf. Use it on multipart and body-less writes (DELETE, archive, restore); on JSON writes send the body field `agent_name` instead, which wins if both are present. Percent-encode the value as UTF-8. Control characters are stripped; a blank value means no agent. More than 128 characters, or a value that cannot be decoded, is `400 invalid_agent_attribution` (never truncated). An agent-type key, which is not bound to a person, gets `400 invalid_agent_attribution` if it sends it. The stamp never changes a permission answer. See [Agent attribution](/developers/plan/conventions#agent-attribution). |

#### Request body

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `filename` | string | Yes | The new file name (1–255 characters). The stored bytes do not change. |
| `agent_name` | string | No | The name of the agent that executed this write on the person's behalf (max 128 characters, blank means no agent). Takes priority over the `X-Dailybot-Agent-Name` header. See [Agent attribution](/developers/plan/conventions#agent-attribution). |

#### Response body

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `(body)` | TaskAttachment | Yes | A [TaskAttachment](#plan-project-attachments-list-taskattachment) object. |

#### Error codes

| Status | When |
|--------|------|
| `400` | Validation failed; the response `code` says which field. `invalid_agent_attribution` means the agent name is invalid. |
| `401` | Missing, expired or malformed credential (`credential_absent`, `credential_expired`, `credential_malformed`). |
| `402` | Plan is not enabled for your organization yet (`plan_upgrade_required`). Expected during the Beta: write to support@dailybot.com. |
| `403` | You are not an organization administrator (`insufficient_scope`), or you are a guest (`guest_not_allowed`). An agent or organization key is refused here too. |
| `404` | The parent or the attachment does not exist or you cannot see it (`not_found`), never a 403. |

#### Example (curl)

```bash
curl -sS -X PATCH "https://api.dailybot.com/v1/plan/projects/00000000-0000-4000-8000-000000000003/attachments/00000000-0000-4000-8000-000000000009/" \
  -H "Authorization: Bearer $DAILYBOT_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
  "filename": "spec-v2.pdf"
}'
```

#### Scenario examples

##### CLI

```bash
dailybot plan project attachments rename 00000000-0000-4000-8000-000000000003 00000000-0000-4000-8000-000000000009 spec-v2.pdf
```

#### Notes

- Scope: `tasks:admin` — container writes. A non-guest member can call it with a login session or a personal API key (a key with explicit Plan scopes needs `tasks:write`, which covers it); an agent or organization key gets `403 insufficient_scope`.
- Rate limit: 60 writes per minute per actor.
- Needs a person: call it with a login session, a CLI user token or a personal API key. An agent or organization key gets `403 insufficient_scope`.

### GET `/v1/plan/projects/{project_id}/updates/{update_id}/reactions/` · Beta

**List who reacted to a project update**

Everyone who reacted to the update, oldest first, as a page. `emoji` narrows to one emoji. The same shape as a comment's reactor list.

- **Auth:** API key (`X-API-KEY`), CLI Bearer (read)
- **Rate limit:** `default`
- **Pagination:** Page-number pagination

#### Path parameters

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `project_id` | string | Yes | The project's uuid. |
| `update_id` | string | Yes | The update's uuid. |

#### Query parameters

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `page` | integer | No | 1-based page number. |
| `page_size` | integer | No | Rows per page. Default 50, maximum 100. Out-of-range values are clamped, never rejected: asking for 500 returns 100. |
| `emoji` | string | No | One emoji; every emoji when omitted. The same rule as writes: anything else is `400 reaction_invalid_emoji`. |

#### Reactor object

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `emoji` | string | Yes | — |
| `user` | ActorRef | Yes | Who reacted. |
| `executed_by_agent` | AgentRef | null | No | The agent that executed the reaction for that person, or `null`. |
| `created_at` | datetime | Yes | — |

#### ActorRef object

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `kind` | string | Yes | — |
| `uuid` | string | Yes | Stable public identifier. |
| `name` | string | No | Display name. |
| `username` | string | null | No | — |
| `avatar_url` | string | null | No | — |
| `has_photo` | boolean | No | — |

#### Response body

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `count` | integer | Yes | Total number of rows. |
| `next` | uri | Yes | URL of the next page, or `null`. |
| `previous` | uri | Yes | URL of the previous page, or `null`. |
| `results` | array<Reactor> | Yes | The page of [Reactor](#plan-project-update-reactions-list-reactor) objects. |

#### Error codes

| Status | When |
|--------|------|
| `400` | `emoji` is not a single emoji (`reaction_invalid_emoji`), or a paging value is not valid. |
| `401` | Missing, expired or malformed credential (`credential_absent`, `credential_expired`, `credential_malformed`). |
| `402` | Plan is not enabled for your organization yet (`plan_upgrade_required`). Expected during the Beta: write to support@dailybot.com. |
| `404` | The project or the update does not exist or you cannot see it (`not_found`), never a 403. |

#### Example (curl)

```bash
curl -sS "https://api.dailybot.com/v1/plan/projects/00000000-0000-4000-8000-000000000003/updates/00000000-0000-4000-8000-000000000007/reactions/" \
  -H "X-API-KEY: $DAILYBOT_API_KEY"
```

#### Scenario examples

##### CLI

```bash
dailybot plan project update reactions 00000000-0000-4000-8000-000000000003 00000000-0000-4000-8000-000000000007
```

#### Notes

- Scope: `tasks:read`.
- Rate limit: 120 reads per minute per actor.
- Works with a login session, a CLI user token, a personal API key, or an agent or organization key. A personal key sees what its person sees; an agent or organization key acts as a system actor and sees organization-visible boards only.

### POST `/v1/plan/projects/{project_id}/updates/{update_id}/reactions/` · Beta

**Add an emoji reaction to a project update (idempotent)**

Adds your `emoji` reaction to the update; adding it again changes nothing. The same rules as comment reactions: one emoji, one reaction per person per emoji, and a person behind the credential. A person holds at most 20 different emojis on one update (`400 reaction_limit_reached`, `extra.limit`). The response is the whole update with its reactions.

- **Auth:** API key (`X-API-KEY`), CLI Bearer (write)
- **Rate limit:** `default`
- **Pagination:** No pagination

#### Path parameters

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `project_id` | string | Yes | The project's uuid. |
| `update_id` | string | Yes | The update's uuid. |

#### Headers

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `X-Dailybot-Agent-Name` | string | No | The name of the agent that executed this write on the person's behalf. Use it on multipart and body-less writes (DELETE, archive, restore); on JSON writes send the body field `agent_name` instead, which wins if both are present. Percent-encode the value as UTF-8. Control characters are stripped; a blank value means no agent. More than 128 characters, or a value that cannot be decoded, is `400 invalid_agent_attribution` (never truncated). An agent-type key, which is not bound to a person, gets `400 invalid_agent_attribution` if it sends it. The stamp never changes a permission answer. See [Agent attribution](/developers/plan/conventions#agent-attribution). |

#### Request body

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `emoji` | string | Yes | The emoji. Max 32 characters. |
| `agent_name` | string | No | The name of the agent that executed this write on the person's behalf (max 128 characters, blank means no agent). Takes priority over the `X-Dailybot-Agent-Name` header. See [Agent attribution](/developers/plan/conventions#agent-attribution). |

#### Response body

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `(body)` | ProjectUpdate | Yes | A [ProjectUpdate](#plan-projects-updates-digest-projectupdate) object. |

#### Error codes

| Status | When |
|--------|------|
| `400` | Not a single emoji (`reaction_invalid_emoji`), an agent or organization key (`actor_required`), too many different emojis from you on this update (`reaction_limit_reached`), or an invalid agent name (`invalid_agent_attribution`). |
| `401` | Missing, expired or malformed credential (`credential_absent`, `credential_expired`, `credential_malformed`). |
| `402` | Plan is not enabled for your organization yet (`plan_upgrade_required`). Expected during the Beta: write to support@dailybot.com. |
| `404` | The project or the update does not exist or you cannot see it (`not_found`), never a 403. |

#### Example (curl)

```bash
curl -sS -X POST "https://api.dailybot.com/v1/plan/projects/00000000-0000-4000-8000-000000000003/updates/00000000-0000-4000-8000-000000000007/reactions/" \
  -H "X-API-KEY: $DAILYBOT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "emoji": "👍"
}'
```

#### Scenario examples

##### CLI

```bash
dailybot plan project update react 00000000-0000-4000-8000-000000000003 00000000-0000-4000-8000-000000000007 👍
```

#### Notes

- Scope: `tasks:write`.
- Rate limit: 60 writes per minute per actor.
- Works with a login session, a CLI user token, a personal API key, or an agent or organization key. A personal key sees what its person sees; an agent or organization key acts as a system actor and sees organization-visible boards only.

### DELETE `/v1/plan/projects/{project_id}/updates/{update_id}/reactions/{emoji}/` · Beta

**Remove the caller's emoji reaction from a project update**

Removes your reaction with this emoji from the update. It answers `204` even when the reaction was already gone.

- **Auth:** API key (`X-API-KEY`), CLI Bearer (write)
- **Rate limit:** `default`
- **Pagination:** No pagination

#### Path parameters

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `project_id` | string | Yes | The project's uuid. |
| `update_id` | string | Yes | The update's uuid. |
| `emoji` | string | Yes | The emoji, percent-encoded as UTF-8. |

#### Headers

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `X-Dailybot-Agent-Name` | string | No | The name of the agent that executed this write on the person's behalf. Use it on multipart and body-less writes (DELETE, archive, restore); on JSON writes send the body field `agent_name` instead, which wins if both are present. Percent-encode the value as UTF-8. Control characters are stripped; a blank value means no agent. More than 128 characters, or a value that cannot be decoded, is `400 invalid_agent_attribution` (never truncated). An agent-type key, which is not bound to a person, gets `400 invalid_agent_attribution` if it sends it. The stamp never changes a permission answer. See [Agent attribution](/developers/plan/conventions#agent-attribution). |

#### Error codes

| Status | When |
|--------|------|
| `400` | Validation failed; the response `code` says which field. `invalid_agent_attribution` means the agent name is invalid. |
| `401` | Missing, expired or malformed credential (`credential_absent`, `credential_expired`, `credential_malformed`). |
| `402` | Plan is not enabled for your organization yet (`plan_upgrade_required`). Expected during the Beta: write to support@dailybot.com. |
| `404` | The project or the update does not exist or you cannot see it (`not_found`), never a 403. |

#### Example (curl)

```bash
curl -sS -X DELETE "https://api.dailybot.com/v1/plan/projects/00000000-0000-4000-8000-000000000003/updates/00000000-0000-4000-8000-000000000007/reactions/%F0%9F%91%8D/" \
  -H "X-API-KEY: $DAILYBOT_API_KEY"
```

#### Scenario examples

##### CLI

```bash
dailybot plan project update unreact 00000000-0000-4000-8000-000000000003 00000000-0000-4000-8000-000000000007 👍
```

#### Notes

- Scope: `tasks:write`.
- Rate limit: 60 writes per minute per actor.
- Works with a login session, a CLI user token, a personal API key, or an agent or organization key. A personal key sees what its person sees; an agent or organization key acts as a system actor and sees organization-visible boards only.

---

## Developer portal navigation

**Getting Started**

- [Overview](/developers)
- [Quick start](/developers/getting-started)
- [Authentication](/developers/authentication)

**API Reference**

- [API Overview](/developers/api)
- [Users](/developers/api/users)
- [Organization](/developers/api/organization)
- [Teams](/developers/api/teams)
- [Invitations](/developers/api/invitations)
- [Check-ins](/developers/api/check-ins)
- [Forms](/developers/api/forms)
- [Labels](/developers/api/labels)
- [Report channels](/developers/api/report-channels)
- [Templates](/developers/api/templates)
- [Kudos](/developers/api/kudos)
- [Mood tracking](/developers/api/mood)
- [Important dates](/developers/api/important-dates)
- [Messaging](/developers/api/messaging)
- [Automations](/developers/api/workflows)
- [Webhooks](/developers/api/webhooks)
- [Commands platform](/developers/api/commands-platform)
- [Agents](/developers/api/agents)
- [OAuth2](/developers/api/oauth2)
- [Integrations](/developers/api/integrations)
- [CLI](/developers/api/cli)
- [Plan · Projects](/developers/api/plan-projects) (this page)
- [Plan · Goals](/developers/api/plan-goals)
- [Plan · Boards](/developers/api/plan-boards)
- [Plan · Tasks](/developers/api/plan-tasks)
- [Plan · Comments & files](/developers/api/plan-collaboration)
- [Plan · Home & search](/developers/api/plan-home)
- [Plan · Notifications & reports](/developers/api/plan-notifications)

**Dailybot Plan**

- [Overview](/developers/plan)
- [Concepts](/developers/plan/concepts)
- [Quickstart](/developers/plan/quickstart)
- [Authentication & scopes](/developers/plan/authentication)
- [Agents on Plan](/developers/plan/agents)
- [Conventions](/developers/plan/conventions)
- [Errors](/developers/plan/errors)
- [CLI for Plan](/developers/plan/cli)
- [Agent skill](/developers/plan/agent-skill)
- [Recipe: live board](/developers/plan/recipes/board-live-updates)
- [Recipe: home in one request](/developers/plan/recipes/home-in-one-request)
- [Recipe: bulk create](/developers/plan/recipes/bulk-create)
- [Recipe: move on PR merge](/developers/plan/recipes/move-on-pr-merge)
- [Recipe: goal progress](/developers/plan/recipes/goal-progress)
- [Recipe: webhooks](/developers/plan/recipes/webhooks)

**API guides**

- [Errors & Status Codes](/developers/errors)
- [Rate Limits](/developers/rate-limits)
- [Conventions](/developers/conventions)
- [API Changelog](/developers/api-changelog)
- [Recipes](/developers/recipes)

**Developer Features**

- [Custom commands](/developers/custom-commands)
- [Serverless commands](/developers/serverless)
- [Webhooks & events](/developers/webhooks)
- [Automation API trigger](/developers/workflow-trigger)
- [Activity API](/developers/activity-api)

**CLI**

- [Overview](/developers/cli)
- [Authentication](/developers/cli-authentication)
- [Command reference](/developers/cli-reference)
- [CI/CD recipes](/developers/cli-ci-cd)
- [Configuration](/developers/cli-configuration)
- [Troubleshooting](/developers/cli-troubleshooting)

**Agent Skill**

- [Overview](/developers/agent-skill)
- [Skills catalog](/skills)

---

## Site navigation

**Product:**
- [Home](/)
- [Product](/product)
- [Pricing](/pricing)
- [Enterprise](/enterprise)
- [Integrations](/integrations)
- [Templates](/templates)

**Resources:**
- [Blog](/blog)
- [Academy](/academy)
- [Changelog](/changelog)
- [Help Center](/help)
- [Developers](/developers)
- [Agents](/agents)

**Company:**
- [About](/about)
- [Careers](/careers)
- [Security](/security)
- [Contact Sales](/demo)

**Connect:**
- [LinkedIn](https://www.linkedin.com/company/dailybot/)
- [X/Twitter](https://twitter.com/dailybot)
- [GitHub](https://github.com/Dailybot-Inc)
- [YouTube](https://www.youtube.com/channel/UC3uM9V52vwX7e3vQpCc4qvA)

