diff --git a/README.md b/README.md
index d68f593..bd8e7bc 100644
--- a/README.md
+++ b/README.md
@@ -300,6 +300,7 @@ API endpoints
- [Cloud files](sections/cloud_files.md#cloud-files)
- [Comments](sections/comments.md#comments)
- [Documents](sections/documents.md#documents)
+- [Drafts](sections/drafts.md#drafts)
- [Events](sections/events.md#events)
- [Everything](sections/everything.md#everything)
- [External links](sections/external_links.md#external-links)
diff --git a/sections/card_table_cards.md b/sections/card_table_cards.md
index e48aacd..66e0022 100644
--- a/sections/card_table_cards.md
+++ b/sections/card_table_cards.md
@@ -392,7 +392,9 @@ _Optional parameters_:
* `title` - of the card.
* `content` - containing information about the card. See our [Rich text guide][rich] for what HTML tags are allowed.
* `due_on` - due date (ISO 8601) of the card
-* `assignee_ids` - an array of people that will be assigned to this to-do. Please see the [Get people][people] endpoints to retrieve them.
+* `assignee_ids` - an array of people that will be assigned to this card. Please see the [Get people][people] endpoints to retrieve them.
+
+**Note:** omitting `title`, `content`, or `assignee_ids` leaves those values unchanged, but omitting `due_on` **clears** the due date. To keep an existing due date while changing something else, resend the current `due_on`.
This endpoint will return `200 OK` with the current JSON representation of the card if the update was a success. See the [Get a card](#get-a-card) endpoint for more info on the payload.
@@ -400,7 +402,8 @@ This endpoint will return `200 OK` with the current JSON representation of the c
```json
{
- "title": "Updated investigation"
+ "title": "Updated investigation",
+ "due_on": "2016-05-01"
}
```
@@ -408,7 +411,7 @@ This endpoint will return `200 OK` with the current JSON representation of the c
```shell
curl -s -H "Authorization: Bearer $ACCESS_TOKEN" -H "Content-Type: application/json" \
- -d '{"title":"Updated investigation"}' -X PUT \
+ -d '{"title":"Updated investigation","due_on":"2016-05-01"}' -X PUT \
https://3.basecampapi.com/$ACCOUNT_ID/card_tables/cards/2.json
```
diff --git a/sections/card_table_steps.md b/sections/card_table_steps.md
index e45ae21..763b51f 100644
--- a/sections/card_table_steps.md
+++ b/sections/card_table_steps.md
@@ -61,18 +61,22 @@ Update a step
_Optional parameters_:
-* `title` - of the card.
+* `title` - of the step.
* `due_on` - due date (ISO 8601) of the step.
* `assignee_ids` - an array of people ids that will be assigned to this step. Please see the [Get people][people] endpoints to retrieve them.
* `assignees` - a comma separated list of people ids (legacy format, prefer `assignee_ids`).
+**Note:** this update **replaces** the step — any of `title`, `due_on`, or `assignee_ids` you omit is cleared. Resend the values you want to keep.
+
This endpoint will return `200 OK` with the current JSON representation of the step if the update was a success. See the step property of the [Get a card][card] endpoint for more info on the payload.
###### Example JSON Request
```json
{
- "title": "Updated inspiration"
+ "title": "Updated inspiration",
+ "due_on": "2021-01-01",
+ "assignee_ids": [30068628, 270913789]
}
```
@@ -80,7 +84,7 @@ This endpoint will return `200 OK` with the current JSON representation of the s
```shell
curl -s -H "Authorization: Bearer $ACCESS_TOKEN" -H "Content-Type: application/json" \
- -d '{"title": "Updated inspiration"}' -X PUT \
+ -d '{"title": "Updated inspiration", "due_on": "2021-01-01", "assignee_ids": [30068628, 270913789]}' -X PUT \
https://3.basecampapi.com/$ACCOUNT_ID/card_tables/steps/2.json
```
diff --git a/sections/client_approvals.md b/sections/client_approvals.md
index 17fc1d6..b3796dd 100644
--- a/sections/client_approvals.md
+++ b/sections/client_approvals.md
@@ -38,7 +38,7 @@ _Optional query parameters_:
"title": "The Clientside",
"type": "Client::Board",
"url": "https://3.basecampapi.com/195539477/buckets/2085958506/client/board.json",
- "app_url": "https://3.basecamp.com/195539477/buckets/2085958506/client/board.1069480059"
+ "app_url": "https://3.basecamp.com/195539477/buckets/2085958506/client/board"
},
"bucket": {
"id": 2085958506,
@@ -144,7 +144,7 @@ Get a client approval
"title": "The Clientside",
"type": "Client::Board",
"url": "https://3.basecampapi.com/195539477/buckets/2085958506/client/board.json",
- "app_url": "https://3.basecamp.com/195539477/buckets/2085958506/client/board.1069480059"
+ "app_url": "https://3.basecamp.com/195539477/buckets/2085958506/client/board"
},
"bucket": {
"id": 2085958506,
diff --git a/sections/client_correspondences.md b/sections/client_correspondences.md
index a930da8..afe07b2 100644
--- a/sections/client_correspondences.md
+++ b/sections/client_correspondences.md
@@ -38,7 +38,7 @@ _Optional query parameters_:
"title": "The Clientside",
"type": "Client::Board",
"url": "https://3.basecampapi.com/195539477/buckets/2085958506/client/board.json",
- "app_url": "https://3.basecamp.com/195539477/buckets/2085958506/client/board.1069480059"
+ "app_url": "https://3.basecamp.com/195539477/buckets/2085958506/client/board"
},
"bucket": {
"id": 2085958506,
@@ -114,7 +114,7 @@ Get a client correspondence
"title": "The Clientside",
"type": "Client::Board",
"url": "https://3.basecampapi.com/195539477/buckets/2085958506/client/board.json",
- "app_url": "https://3.basecamp.com/195539477/buckets/2085958506/client/board.1069480059"
+ "app_url": "https://3.basecamp.com/195539477/buckets/2085958506/client/board"
},
"bucket": {
"id": 2085958506,
diff --git a/sections/documents.md b/sections/documents.md
index 22c3f67..ce79051 100644
--- a/sections/documents.md
+++ b/sections/documents.md
@@ -165,13 +165,13 @@ curl -s -H "Authorization: Bearer $ACCESS_TOKEN" https://3.basecampapi.com/$ACCO
Create a document
-----------------
-* `POST /vaults/2/documents.json` publishes a document under the vault with an ID of `2`.
+* `POST /vaults/2/documents.json` creates a document under the vault with an ID of `2`.
**Required parameters**: `title` as the title of the document, and `content` as the body of the document. See our [Rich text guide][rich] for what HTML tags are allowed.
_Optional parameters_:
-* `status`, set to `active` to publish immediately.
+* `status` — set to `active` to publish the document immediately. **When `status` is omitted the document is created as a draft** (it defaults to `drafted`); a draft is not posted and notifies no one until you publish it. See [Publishing a draft](#publishing-a-draft).
* `visible_to_clients` - top-level boolean. When the project has clients enabled, whether the document is visible to them. Defaults to `false` (team callers creating directly under the docked tool); a **client** caller always creates client-visible records. Applies only when creating directly in the tool's vault; items created inside a folder inherit the folder's visibility. See [Client visibility][client_visibility] to change it after creation.
This endpoint will return `201 Created` with the current JSON representation of the document if the creation was a success. See the [Get a document](#get-a-document) endpoint for more info on the payload.
@@ -218,6 +218,22 @@ curl -s -H "Authorization: Bearer $ACCESS_TOKEN" -H "Content-Type: application/j
https://3.basecampapi.com/$ACCOUNT_ID/documents/2.json
```
+Publishing a draft
+------------------
+
+Publish a [draft](drafts.md) document by updating it with `status` set to `active`. Unlike a message, a document update **replaces** the document rather than merging, so you must resend `title` and `content` alongside `status`:
+
+```json
+{
+ "title": "New Hire Info",
+ "content": "
Getting started
",
+ "status": "active"
+}
+```
+
+A status-only update fails with `400 Bad Request` (the `document` parameters are required), and omitting a field clears its value. Publishing posts the document and notifies its subscribers exactly once — the same publication side-effects as [publishing a message](messages.md#publishing-a-draft).
+
+
Legacy project-scoped routes
-----------------------------
diff --git a/sections/drafts.md b/sections/drafts.md
new file mode 100644
index 0000000..44d37dc
--- /dev/null
+++ b/sections/drafts.md
@@ -0,0 +1,64 @@
+Drafts
+======
+
+Endpoint for retrieving the current user's unpublished message, document, upload, and client approval/correspondence drafts across their active projects.
+
+A draft is a [message](messages.md), [document](documents.md), file [upload](uploads.md), or client [approval](client_approvals.md)/[correspondence](client_correspondences.md) that has been saved but not yet published. Messages and documents can be drafted and published through the API — see the [messages](messages.md#create-a-message) and [documents](documents.md#create-a-document) sections. Uploads have their own [create](uploads.md#create-an-upload) and [update](uploads.md#update-an-upload) endpoints, but no documented way to create or publish a *draft* upload; client approvals and correspondences are read-only over the API. Drafts of all of these types still appear here, however they were authored.
+
+Endpoints:
+
+- [Get my drafts](#get-my-drafts)
+
+
+Get my drafts
+-------------
+
+* `GET /my/drafts.json` will return a [paginated list][pagination] of the current user's drafts, most recently updated first, regardless of how they were created. Drafts whose project has been archived or trashed are excluded.
+
+Five kinds of drafts are returned:
+
+* [Messages](messages.md)
+* [Documents](documents.md)
+* [Uploads](uploads.md)
+* Client [approvals](client_approvals.md)
+* Client [correspondences](client_correspondences.md)
+
+**Not returned:** other resources can also be saved in a drafted state, but they aren't part of this list — notably [Google documents](google_documents.md) (which default to `drafted` when `status` is omitted), [cloud files](cloud_files.md), and [schedule entries](schedule_entries.md). This endpoint is not an exhaustive feed of everything you have unpublished.
+
+Each draft carries an `excerpt`: up to 300 characters of plain-text content, or an empty string when the draft has no body yet. `parent` is `null` for drafts filed directly under their bucket, and `scheduled_posting_at` is `null` unless the draft is scheduled to publish later.
+
+###### Example JSON Response
+
+```json
+[
+ {
+ "id": 1069480291,
+ "app_url": "https://3.basecamp.com/195539477/buckets/2085958504/messages/1069480291",
+ "title": "Launch announcement",
+ "type": "message",
+ "bucket": {
+ "id": 2085958504,
+ "name": "The Leto Laptop",
+ "app_url": "https://3.basecamp.com/195539477/buckets/2085958504"
+ },
+ "parent": {
+ "id": 1069479829,
+ "title": "Message Board",
+ "app_url": "https://3.basecamp.com/195539477/buckets/2085958504/message_boards/1069479829"
+ },
+ "excerpt": "Still polishing the wording before we post this.",
+ "created_at": "2026-07-22T22:11:07.611Z",
+ "updated_at": "2026-07-22T22:11:07.632Z",
+ "scheduled_posting_at": null
+ }
+]
+```
+
+
+###### Copy as cURL
+
+```shell
+curl -s -H "Authorization: Bearer $ACCESS_TOKEN" https://3.basecampapi.com/$ACCOUNT_ID/my/drafts.json
+```
+
+[pagination]: ../README.md#pagination
diff --git a/sections/messages.md b/sections/messages.md
index 80a4280..8b39c30 100644
--- a/sections/messages.md
+++ b/sections/messages.md
@@ -414,12 +414,13 @@ curl -s -H "Authorization: Bearer $ACCESS_TOKEN" https://3.basecampapi.com/$ACCO
Create a message
----------------
-* `POST /message_boards/3/messages.json` publishes a message under the message board with an ID of `3`.
+* `POST /message_boards/3/messages.json` creates a message under the message board with an ID of `3`.
-**Required parameters**: `subject` as the title of the message, and `status`, set to `active` to publish immediately.
+**Required parameters**: `subject` as the title of the message.
-_Optional parameters_:
+_Optional parameters_:
+* `status` — set to `active` to publish the message immediately. **When `status` is omitted the message is created as a draft** (it defaults to `drafted`); a draft is not posted and notifies no one until you publish it. See [Publishing a draft](#publishing-a-draft).
* `content` as the body of the message. See our [Rich text guide][4] for what HTML tags are allowed.
* `category_id` to set a type for the message. To get a list of all the message types for a project, see the [Get message types][5] endpoint.
* `subscriptions` an array of people IDs to be notified of the new message when it's posted and subscribed to it. If not present, all people on the project will be notified and subscribed.
@@ -470,6 +471,20 @@ curl -s -H "Authorization: Bearer $ACCESS_TOKEN" -H "Content-Type: application/j
https://3.basecampapi.com/$ACCOUNT_ID/messages/2.json
```
+Publishing a draft
+------------------
+
+Publish a [draft](drafts.md) message by updating it with `status` set to `active`:
+
+```json
+{ "status": "active" }
+```
+
+A message update **merges** the fields you send, so you don't need to resend `subject` or `content` — the draft's existing content is preserved.
+
+Publishing is what actually posts the message, and it happens exactly once: each subscriber who should hear about it receives a single notification (delivered shortly after, once notifications are dispatched). Creating or re-saving a draft notifies no one — only publishing does. (Documents publish the same way; see [Publishing a draft](documents.md#publishing-a-draft) in the documents section.)
+
+
Pin a message
----------------
diff --git a/sections/my_assignments.md b/sections/my_assignments.md
index b7708e7..2d62b71 100644
--- a/sections/my_assignments.md
+++ b/sections/my_assignments.md
@@ -1,17 +1,20 @@
My assignments
===============
-Endpoints for retrieving the current user's assignments, including active assignments split by priority, completed assignments, and due assignments filtered by time scope.
+Retrieve the current user's assignments — active ones split by priority, completed ones, and due ones filtered by time scope — and manage their priority order ("Up Next"). For how to actually change an assignment (assign someone, complete it, reschedule it), see [Act on assignments](#act-on-assignments), which routes you to the to-do, card, and card table step endpoints.
-Each assignment includes its content, type, bucket, parent, assignees, due dates, completion status, and any child assignments (e.g. card steps nested under their parent card).
+Each assignment includes its content, type, bucket, parent, assignees, due dates, completion status, and any child assignments (e.g. card table steps nested under their parent card).
-**Note:** These endpoints return the full result set and are not paginated.
+**Note:** the retrieval endpoints below — Get assignments, Get completed assignments, and Get due assignments — return the full result set and are not paginated.
Endpoints:
- [Get assignments](#get-assignments)
- [Get completed assignments](#get-completed-assignments)
- [Get due assignments](#get-due-assignments)
+- [Prioritize an assignment](#prioritize-an-assignment)
+- [Deprioritize an assignment](#deprioritize-an-assignment)
+- [Reorder Up Next](#reorder-up-next)
Get assignments
@@ -635,3 +638,99 @@ Providing an invalid `scope` returns `400 Bad Request` with an error listing the
```shell
curl -s -H "Authorization: Bearer $ACCESS_TOKEN" https://3.basecampapi.com/$ACCOUNT_ID/my/assignments/due.json?scope=overdue
```
+
+
+Act on assignments
+------------------
+
+Assignments are created and changed through the to-do, card, and card table step endpoints, not through `/my/assignments`. A card table step is returned nested under its parent card as a `children` entry, and it's acted on through its own step endpoints, not the parent card's:
+
+* **Assign or unassign** — pass `assignee_ids` when updating a [to-do](todos.md#update-a-to-do) or a [card](card_table_cards.md#update-a-card); include `notify: true` to notify the people you add. A card table step is assigned on the step itself via [Update a step](card_table_steps.md#update-a-step) (`PUT /card_tables/steps/:id.json`), not the parent card.
+* **Complete or reopen** — a to-do uses its [completion](todos.md#complete-a-to-do) endpoint (`POST`/`DELETE /todos/:id/completion.json`). A card table step uses [Change step completion status](card_table_steps.md#change-step-completion-status) — `PUT /card_tables/steps/:step_id/completions.json` with `completion: "on"` to complete or `"off"` to reopen. A card has no completion endpoint: it's completed by moving it into a done column and reopened by moving it out, via [Move a card](card_table_cards.md#move-a-card) (`POST /card_tables/cards/:id/moves.json` with the destination `column_id`).
+* **Reschedule** — update a [to-do](todos.md#update-a-to-do) with `due_on` and, optionally, `starts_on`. A [card](card_table_cards.md#update-a-card) is rescheduled with `due_on` only — cards have no `starts_on` — and a card table [step](card_table_steps.md#update-a-step) likewise takes `due_on`, on its own update endpoint.
+
+**Note:** these updates replace omitted fields. A to-do update and a card table step update both clear any field you don't send — including `assignee_ids`, `due_on`, and (for steps) `title` — so resend the current values you want to keep. A card update is the one that preserves omitted `title` and `content`, but even there `due_on` is cleared unless you resend it. In short: to change only assignees or only the due date, echo the record's other current fields in the same request.
+
+
+Prioritize an assignment
+------------------------
+
+* `POST /my/priorities.json` adds a recording to "Up Next" — the current user's ordered list of prioritized assignments, returned as `priorities` in [Get assignments](#get-assignments). Returns `204 No Content`.
+
+Identify the item by the **recording id** of the thing that carries the priority, not by the assignment id. [Get assignments](#get-assignments) normalizes a card table step under its parent card, so which id to send depends on what you're prioritizing:
+
+* **A to-do, or a card itself** — send the item's top-level `id`.
+* **A card table step that isn't prioritized yet** — send the step's own `id` from its parent card's `children`. Only already-prioritized top-level entries carry a `priority_recording_id`, so a step you haven't prioritized has just its own id.
+* **A step that is already prioritized** — the top-level entry's `id` is the *card's*, so send that entry's `priority_recording_id`, which points at the prioritized step. Use it to reorder or deprioritize.
+
+If more than one step on the same card is prioritized, [Get assignments](#get-assignments) lists that card once and reports a single `priority_recording_id`; the sibling step priorities aren't separately addressable through this list.
+
+**Required parameters**: `id` — the recording id to prioritize.
+
+###### Example JSON Request
+
+```json
+{
+ "id": 1069479801
+}
+```
+
+###### Copy as cURL
+
+```shell
+curl -s -H "Authorization: Bearer $ACCESS_TOKEN" \
+ -H "Content-Type: application/json" \
+ -d '{"id":1069479801}' \
+ https://3.basecampapi.com/$ACCOUNT_ID/my/priorities.json
+```
+
+
+Deprioritize an assignment
+--------------------------
+
+* `DELETE /my/priorities/1069479801.json` removes the recording with an ID of `1069479801` from Up Next. Returns `204 No Content`.
+
+###### Copy as cURL
+
+```shell
+curl -s -H "Authorization: Bearer $ACCESS_TOKEN" \
+ -X DELETE \
+ https://3.basecampapi.com/$ACCOUNT_ID/my/priorities/1069479801.json
+```
+
+
+Reorder Up Next
+---------------
+
+* `POST /my/priority_moves.json` moves an already-prioritized recording to a new spot in Up Next. Returns `204 No Content`.
+
+**Required parameters**:
+
+* `source_id` — the recording id to move, chosen the same way as when [prioritizing](#prioritize-an-assignment).
+* `position` — the 1-based position to move it to.
+
+###### Example JSON Request
+
+```json
+{
+ "source_id": 1069479801,
+ "position": 1
+}
+```
+
+###### Copy as cURL
+
+```shell
+curl -s -H "Authorization: Bearer $ACCESS_TOKEN" \
+ -H "Content-Type: application/json" \
+ -d '{"source_id":1069479801,"position":1}' \
+ https://3.basecampapi.com/$ACCOUNT_ID/my/priority_moves.json
+```
+
+Reordering validates `position` and, on failure, returns an error body of the form `{ "error": "..." }`:
+
+* Missing `position` → `400 Bad Request`, `{ "error": "Position is required." }`.
+* Non-integer `position` → `400 Bad Request`, `{ "error": "Position must be an integer." }`.
+* `position` outside the list → `422 Unprocessable Entity`, `{ "error": "Position must be between 1 and N." }`, where `N` is the number of prioritized assignments.
+* A recording that isn't prioritized → `422 Unprocessable Entity`, `{ "error": "Recording is not prioritized." }`.
+* A recording you can't access → `404 Not Found`.
diff --git a/sections/schedule_entries.md b/sections/schedule_entries.md
index db956fa..f860677 100644
--- a/sections/schedule_entries.md
+++ b/sections/schedule_entries.md
@@ -283,6 +283,7 @@ _Optional parameters_:
* `month_interval` - for `custom_month`: repeat every `2` to `12` months
* `recurs_until` - date (ISO 8601) the recurrence ends. Omit to recur indefinitely. Reflected as `recurrence_schedule.end_date` in the response.
* `visible_to_clients` - top-level boolean. When the project has clients enabled, whether the schedule entry is visible to them. Defaults to `false` (team callers creating directly under the docked tool); a **client** caller always creates client-visible records. See [Client visibility][client_visibility] to change it after creation.
+* `status` - when omitted, a schedule entry created over the API is published (`active`) immediately. Passing `status: "drafted"` creates it as a draft instead. Note that, unlike messages and documents, schedule-entry drafts are **not** listed by [Get my drafts](drafts.md#get-my-drafts).
The remaining `recurrence_schedule` attributes shown in [Get a schedule entry](#get-a-schedule-entry) (`hour`, `minute`, `start_date`, `duration`, `end_date`) are derived from `starts_at`, `ends_at`, and `recurs_until` — they're ignored on input. An invalid `recurrence_schedule` is discarded on create: the entry is created without recurring.