# REST API reference

Source: https://switchdesk.org/docs/ai/rest

> The SwitchDesk AI Access REST API: one route per tool at api.switchdesk.org/v1, auth header, GET vs POST, responses, errors, idempotency and limits.

## Base URL and auth {#auth}

```text
https://api.switchdesk.org/v1/<tool>
Authorization: Bearer sda_…
```

The key (`sda_` + 32 characters + an 8-character checksum) is created in Settings → AI Access and shown once. Send it only in the `Authorization` header, never in a URL. Sign-in tokens don’t work here: they belong to `https://mcp.switchdesk.org/mcp`. The full schema is [openapi.json](https://api.switchdesk.org/openapi.json).

## Requests {#requests}

- Every tool is `POST /v1/<tool>` with a JSON body of its arguments (see [the tool reference](/docs/ai/mcp#tools)).
- Reads without user-written text also accept `GET` with query parameters. Anything carrying text (`query`, titles, notes) must be a POST body, because URLs end up in logs.
- Browsers can’t call the API: a request with an `Origin` header is refused (`origin_not_allowed`).
- Writes take an optional `idempotency_key` (1–100 characters). Resending it within 24 hours returns the first answer.

```sh
curl -s https://api.switchdesk.org/v1/list_todos \
  -H "Authorization: Bearer $SWITCHDESK_TOKEN" -H "Content-Type: application/json" \
  -d '{"section":"today"}'

curl -s https://api.switchdesk.org/v1/add_todo \
  -H "Authorization: Bearer $SWITCHDESK_TOKEN" -H "Content-Type: application/json" \
  -d '{"title":"Call Ana","category":"Fernhill","due":"2026-10-14","idempotency_key":"call-ana-1"}'
```

## Responses {#responses}

| HTTP | Means |
|---|---|
| 200 | Done: `{"ok": true, …}` |
| 201 | Created (a new to-do, category or note) |
| 202 | Waiting for the person: `{"ok": true, "status": "pending_approval", "proposal_id": "…"}` |
| 4xx | `{"ok": false, "error", "message", "hint"}`: see below |

## Routes {#routes}

| Route | GET too | Permission |
|---|---|---|
| [`POST /v1/get_access`](/docs/ai/mcp#get_access) | yes | `none` |
| [`POST /v1/list_categories`](/docs/ai/mcp#list_categories) | yes | `categories.read` |
| [`POST /v1/list_todos`](/docs/ai/mcp#list_todos) | yes | `todos.read` |
| [`POST /v1/get_todo`](/docs/ai/mcp#get_todo) | yes | `todos.read` |
| [`POST /v1/add_todo`](/docs/ai/mcp#add_todo) |  | `todos.add_incoming / todos.add` |
| [`POST /v1/update_todo`](/docs/ai/mcp#update_todo) |  | `todos.edit` |
| [`POST /v1/set_todo_state`](/docs/ai/mcp#set_todo_state) |  | `todos.complete` |
| [`POST /v1/delete_todos`](/docs/ai/mcp#delete_todos) |  | `todos.delete` |
| [`POST /v1/add_todo_note`](/docs/ai/mcp#add_todo_note) |  | `todos.edit (+ team.write on a shared to-do)` |
| [`POST /v1/delete_todo_note`](/docs/ai/mcp#delete_todo_note) |  | `todos.edit (+ team.write on a shared to-do)` |
| [`POST /v1/propose_changes`](/docs/ai/mcp#propose_changes) |  | `approvals.ask` |
| [`POST /v1/ask_me`](/docs/ai/mcp#ask_me) |  | `approvals.ask` |
| [`POST /v1/get_proposal`](/docs/ai/mcp#get_proposal) | yes | `approvals.ask (or any write in Ask mode)` |
| [`POST /v1/list_proposals`](/docs/ai/mcp#list_proposals) | yes | `approvals.ask (or any write in Ask mode)` |
| [`POST /v1/create_category`](/docs/ai/mcp#create_category) |  | `categories.write` |
| [`POST /v1/update_category`](/docs/ai/mcp#update_category) |  | `categories.write (+ team.write when shared or used by a meeting automation)` |
| [`POST /v1/list_meetings`](/docs/ai/mcp#list_meetings) | yes | `meetings.read` |
| [`POST /v1/get_meeting`](/docs/ai/mcp#get_meeting) |  | `meetings.read (+ meetings.transcripts for the transcript)` |
| [`POST /v1/search_meetings`](/docs/ai/mcp#search_meetings) |  | `meetings.read` |
| [`POST /v1/list_notes`](/docs/ai/mcp#list_notes) | yes | `notes.read` |
| [`POST /v1/get_note`](/docs/ai/mcp#get_note) | yes | `notes.read` |
| [`POST /v1/search_notes`](/docs/ai/mcp#search_notes) |  | `notes.read` |
| [`POST /v1/create_note`](/docs/ai/mcp#create_note) |  | `notes.write` |
| [`POST /v1/append_to_note`](/docs/ai/mcp#append_to_note) |  | `notes.write` |
| [`POST /v1/update_note`](/docs/ai/mcp#update_note) |  | `notes.write` |
| [`POST /v1/delete_note`](/docs/ai/mcp#delete_note) |  | `notes.write` |
| [`POST /v1/get_recap`](/docs/ai/mcp#get_recap) | yes | `recaps.read` |
| [`POST /v1/search_brain`](/docs/ai/mcp#search_brain) |  | `brain.read` |
| [`POST /v1/get_brain_page`](/docs/ai/mcp#get_brain_page) | yes | `brain.read` |
| [`POST /v1/remember`](/docs/ai/mcp#remember) |  | `brain.write` |

## Errors {#errors}

| Error code | HTTP | CLI exit | Hint the API sends |
|---|---|---|---|
| `missing_key` | 401 | 2 | Send the key in the Authorization header: Bearer sda_… |
| `invalid_key` | 401 | 2 | The key is wrong or was deleted. Ask the person for a new key from SwitchDesk → Settings → AI Access. |
| `revoked` | 401 | 2 | The person revoked this connection. Ask them for a new key. |
| `expired` | 401 | 2 | This connection expired (90 days unused, or its end date). Ask the person for a new key. |
| `plan_required` | 403 | 2 | SwitchDesk Pro is needed for AI access. |
| `capability_off` | 403 | 2 | Not allowed. Turn it on in SwitchDesk → Settings → AI Access. |
| `needs_confirm` | 403 | 4 | The person needs to confirm Meetings & Notes access for this connection with the code SwitchDesk emails them (Settings → AI Access). |
| `copy_off` | 403 | 4 | The person needs to turn on "Share meetings & notes with your AIs" in SwitchDesk → Settings → AI Access on their Mac. |
| `transcripts_off` | 403 | 4 | The person needs to tick "Include full transcripts" in SwitchDesk → Settings → AI Access on their Mac. |
| `not_found` | 404 | 1 |  |
| `unknown_tool` | 404 | 1 |  |
| `paused` | 423 | 2 | The person paused this connection's changes. Reads still work; they can resume it in SwitchDesk → Settings → AI Access. |
| `rate_limited` | 429 | 5 | Too many requests. Wait retry_after seconds. |
| `too_many_pending` | 429 | 1 |  |
| `incoming_full` | 429 | 1 |  |
| `try_again` | 409 | 1 | The person's day just turned over (3 AM in their time zone). Send the same request again. |
| `duplicate_category` | 409 | 1 | A category with that name exists; use update_category with its id. |
| `no_mac` | 409 | 4 | The person's Mac hasn't connected yet. Ask them to open SwitchDesk on it. |
| `brain_off_for_ai` | 403 | 4 | The person needs to choose what connected AIs may see in SwitchDesk → Settings → Brain. |
| `brain_off` | 403 | 4 | The person's Brain is off. They can turn it on in SwitchDesk → Settings → Brain. |
| `not_available_yet` | 501 | 4 | Coming soon. |
| `series_stop_required` | 422 | 1 | This to-do repeats. Pass series: "stop" to delete it and end the series. |
| `ambiguous_category` | 422 | 1 | Several categories match; pass category_id from list_categories. |
| `unknown_category` | 422 | 1 | No category matched; call list_categories for the names. |
| `timezone_required` | 422 | 1 | SwitchDesk doesn't know the person's time zone yet: pass timezone (e.g. "America/New_York"). |
| `invalid_timezone` | 422 | 1 | Use an IANA time zone name like "America/New_York". |
| `invalid_repeat` | 422 | 1 | Send an RRULE like "FREQ=WEEKLY;BYDAY=MO" or a rule object (see the docs). |
| `sub_todo` | 422 | 1 | Sub-to-dos follow their parent. Move or date the parent, or send parent_id: null first. |
| `teammate_subtree` | 422 | 1 | A sub-to-do belongs to a teammate, so the whole block can't be moved or deleted. |
| `nothing_to_change` | 422 | 1 | Every field already has that value. |
| `reserved_name` | 422 | 1 | Today's Todo and This Week are built in. |
| `repeat_not_allowed` | 422 | 1 | Reopen the to-do (or wait for its review) before making it repeat. |
| `unknown_space` | 422 | 1 | Use a space name or id from get_access (brain.spaces), or leave space out. |
| `secret` | 422 | 2 | SwitchDesk never keeps passwords, keys, card, bank or ID numbers in the Brain. Suggest a password manager instead. |

Rate-limited answers (429) carry `retry_after` and a `Retry-After` header. Per-connection and account limits: [Limits](/docs/ai#limits).
