# Twos > Twos is a notes and to-do app. Its public API reads and writes a signed-in > person's own lists and things. This file is the whole contract in one page. Version: 1.0.0 Base URL: https://www.twosapp.com/api/v1 OpenAPI: https://www.twosapp.com/api/v1/openapi.json MCP server: https://www.twosapp.com/mcp (streamable HTTP, OAuth 2.1 with dynamic client registration) Docs for people: https://www.twosapp.com/developers Support: help@twosapp.com ## Authentication Send a personal API key on every request: `Authorization: Bearer twos_...` A person creates one at https://www.twosapp.com/settings/api-keys and it is shown once. Ask for the key. Never guess one, and never send a key to any other host. A key only ever reaches its own owner's data, so there is no user id to pass. Rate limit: 1000 requests per hour per key, reported in the `X-RateLimit-Limit`, `X-RateLimit-Remaining` and `X-RateLimit-Reset` headers. ## The object model Two objects. A LIST holds THINGS. A thing has `text` (markdown) and a `type`. `todo` is a checkbox, `none` is a plain line. The other types are `dash`, `number` and `bullet`. Writes also accept `note` as an alias for `none`, but a read always returns `none`, so that one field does not round-trip unchanged. A thing can also carry `url` (a hyperlink), `tags`, `tabs` (indent depth, 0 is top level), `favorited` (starred), `completed`, and `note` (a long-form markdown note behind an icon, which is a different thing from the `note` type above). Three rules that save round trips: 1. Every get, update, delete and reminder call takes an `id`. Call `GET /search` or `GET /lists` first to resolve a name to an id. Do not invent ids. 2. The day list is the core of the product. For "today", "yesterday", "tomorrow" or a date, call `GET /today`, which resolves or creates that day list. Do not scan `GET /lists` looking for a date-titled one. 3. Write many things with `POST /things/bulk` (up to 500) rather than looping over `POST /things`. Same for `PATCH /things/bulk` and `DELETE /things/bulk`. Every write is a real change to a real account. There is no sandbox and no test mode. Confirm destructive calls with whoever asked for them. ## Scopes - `read:lists`: Read your lists. - `write:lists`: Create, update, and delete lists. - `read:things`: Read the things inside your lists, and your tags. - `write:things`: Create, update, and delete things. - `search`: Search across your lists and things. A key missing the scope an endpoint needs gets a 403 naming the scope. ## Endpoints ### GET /api/v1/lists List lists. Scope: `read:lists`. Returns 200. Returns your lists (non-hidden, non-archived), most recently modified first, 50 per page. Parameters: - `page` (integer, query, optional): Zero-based page index. Example: ``` curl "https://www.twosapp.com/api/v1/lists?page=0" \ -H "Authorization: Bearer twos_YOUR_KEY" ``` ### POST /api/v1/lists Create a list. Scope: `write:lists`. Returns 201. Creates a new list. Fields you omit fall back to your account defaults (e.g. list sort). Pass `things` to create the list already filled, in one call (returns `created`/`skipped` counts alongside the list). Body: - `title` (string, required): The list name. - `emoji` (string, optional): An emoji shown next to the list. - `things` (object[], optional): Optional initial things (max 500), in order. Each item: { text, type?, url?, list_ref?, tags?, photos?, completed?, tabs?, created?, and formatting: bold?, italic?, underline?, header?, subheader?, quote?, code? }. Example: ``` curl -X POST "https://www.twosapp.com/api/v1/lists" \ -H "Authorization: Bearer twos_YOUR_KEY" \ -H "Content-Type: application/json" \ -d '{"title":"Groceries","emoji":"🛒","things":[{"text":"Oat milk","type":"todo"}]}' ``` ### POST /api/v1/lists/import Import a list from markdown. Scope: `write:lists`. Returns 201. Create a NEW list from a markdown document in one call. The title comes from `title`, or the document’s first heading (`# …`) when omitted; the rest becomes the list’s things (checkboxes → todos, `1.` → numbered, `-`/`*` → bullets, `##` → heading, `###` → subheading, indentation → nesting). Body: - `markdown` (string, required): The markdown document to import. - `title` (string, optional): The new list’s name. Omit to use the markdown’s first "# Heading". - `emoji` (string, optional): An emoji for the new list. Example: ``` curl -X POST "https://www.twosapp.com/api/v1/lists/import" \ -H "Authorization: Bearer twos_YOUR_KEY" \ -H "Content-Type: application/json" \ -d '{"title":"Trip plan","markdown":"# Trip plan\n- [ ] Book flights\n- [ ] Pack"}' ``` ### GET /api/v1/lists/{id} Get a list. Scope: `read:lists`. Returns 200. Returns a single list you own, its things (in the app’s display order, per the list’s `sort`), and any reminders on those things. Parameters: - `id` (string, path, required): The list id. - `max_text` (string, query, optional): Truncate each thing’s text to this many characters (… marks a cut). For cheap reads of long lists. Example: ``` curl "https://www.twosapp.com/api/v1/lists/64f0c2a1e4b0a1234567890a?max_text=120" \ -H "Authorization: Bearer twos_YOUR_KEY" ``` ### GET /api/v1/today Get a day list. Scope: `read:lists`. Returns 200. Returns the user’s date-titled day list for a date — resolved in the user’s timezone — creating it if it doesn’t exist yet (like opening that day in the app). Same response shape as Get a list. Use it for the daily-list loop without hunting for an id. Parameters: - `date` (string, query, optional): "today" (default), "yesterday", "tomorrow", or an ISO 8601 date (e.g. 2026-04-01). - `max_text` (string, query, optional): Truncate each thing’s text to this many characters. Example: ``` curl "https://www.twosapp.com/api/v1/today?date=today&max_text=120" \ -H "Authorization: Bearer twos_YOUR_KEY" ``` ### PATCH /api/v1/lists/{id} Update a list. Scope: `write:lists`. Returns 200. Updates a list. Only the fields you send are changed. Renaming cascades to the list’s things. Parameters: - `id` (string, path, required): The list id. Body: - `title` (string, optional): Rename the list. - `emoji` (string, optional): Change the emoji. - `favorited` (boolean, optional): Star or unstar the list. - `archived` (boolean, optional): Archive or unarchive the list. Example: ``` curl -X PATCH "https://www.twosapp.com/api/v1/lists/64f0c2a1e4b0a1234567890a" \ -H "Authorization: Bearer twos_YOUR_KEY" \ -H "Content-Type: application/json" \ -d '{"title":"Weekend groceries","emoji":"🥑"}' ``` ### DELETE /api/v1/lists/{id} Delete a list. Scope: `write:lists`. Returns 200. Permanently deletes a list and all of its things. Idempotent. Parameters: - `id` (string, path, required): The list id. Example: ``` curl -X DELETE "https://www.twosapp.com/api/v1/lists/64f0c2a1e4b0a1234567890a" \ -H "Authorization: Bearer twos_YOUR_KEY" ``` ### GET /api/v1/things List things. Scope: `read:things`. Returns 200. List your things newest-first across all lists (or one list). Built for automation polling: each thing has a stable `id` for dedup, and `since` + `sort` form an incremental cursor. Filter by list, completion, type, or tag. Parameters: - `list_id` (string, query, optional): Only things in this list. - `completed` (boolean, query, optional): Filter by completion (true/false). - `type` (string, query, optional): Filter by type: todo, note, dash, number, or bullet. - `tag` (string, query, optional): Only things with this tag. - `since` (string, query, optional): Only things newer than this ISO 8601 time (by the chosen sort). - `sort` (string, query, optional): created (default) or updated. - `page` (string, query, optional): Zero-based page (50 per page). Page until has_more is false to read everything. - `max_text` (string, query, optional): Truncate each thing’s text to this many characters (… marks a cut). Example: ``` curl "https://www.twosapp.com/api/v1/things?list_id=64f0c2a1e4b0a1234567890a&completed=false&type=todo&tag=errands&since=2026-06-01T00%3A00%3A00.000Z&sort=created&page=0&max_text=120" \ -H "Authorization: Bearer twos_YOUR_KEY" ``` ### POST /api/v1/things Create a thing. Scope: `write:things`. Returns 201. Creates a thing inside one of your lists. Target the list with `list_id` (from list_lists/search) or `list` (a list name, or "today"/"tomorrow"/an ISO date). Pick a `type` — `todo` (checkbox), `note`/`none` (plain), `dash`, `number`, or `bullet` — or omit it to use your default thing type. Add a `url` to make it a hyperlink, `tags` to categorize it, `list_ref` to link another list, or `created` to backdate it. To create many at once, send an `items` array to Bulk-create things instead. Body: - `list_id` (string, optional): The id of the list to add the thing to. Omit if you pass `list`. - `list` (string, optional): Alternative to list_id: a list name, or "today"/"yesterday"/"tomorrow"/an ISO date (the day list, created if needed). - `text` (string, required): The thing’s text, in markdown (**bold**, *italic*, ~~strike~~, `code`, [label](url)) — the same dialect reads return, so text round-trips. An unpaired marker stays literal, and a code:true thing is never parsed. - `type` ("todo" | "note" | "dash" | "number" | "bullet" | "none" | "photo", optional): Thing type: `todo` (checkbox), `note`/`none` (plain, no marker), `dash`, `number` (numbered), or `bullet`. Omit to use your default thing type. - `url` (string, optional): A hyperlink to attach to the thing. - `list_ref` (string, optional): The id of another list to reference (a navigable list link). Pass a list id, not a URL. - `tags` (string[], optional): Tag names (with or without a leading #). - `photos` (string[], optional): Hosted image URLs to attach (must be http(s); max 10). The API does not host images — upload them elsewhere and pass the URLs. - `completed` (boolean, optional): Whether the thing starts completed. - `created` (string, optional): ISO 8601 creation time. Use it to preserve original capture-date order when importing; omit to stamp now. - `tabs` (integer, optional): Indent level: 0 = top level (default); 1+ nests this thing under the preceding one. Create the parent first, then the indented children. - `bold` (boolean, optional): Bold the whole thing. - `italic` (boolean, optional): Italicize the whole thing. - `underline` (boolean, optional): Underline the whole thing. - `header` (boolean, optional): Make the thing a heading (H1). Mutually exclusive with `subheader`; also bolds it unless you pass `bold` explicitly. - `subheader` (boolean, optional): Make the thing a subheading (H2). Mutually exclusive with `header`; also bolds it unless you pass `bold` explicitly. - `quote` (boolean, optional): Style the thing as a blockquote. Mutually exclusive with `code`. - `code` (boolean, optional): Style the thing as a code block. Mutually exclusive with `quote`. - `note` (string, optional): A long-form note attached to the thing, in **markdown**. Shown behind a note icon in the app, never inline with `text`. Pass `""` to remove it. Reads return the same markdown, so a note round-trips unchanged. Example: ``` curl -X POST "https://www.twosapp.com/api/v1/things" \ -H "Authorization: Bearer twos_YOUR_KEY" \ -H "Content-Type: application/json" \ -d '{"list_id":"64f0c2a1e4b0a1234567890a","text":"Buy oat milk","type":"todo","tags":["errands"]}' ``` ### POST /api/v1/things/bulk Bulk-create things. Scope: `write:things`. Returns 201. Create many things in one call (one ordered insert) — use this instead of looping for imports or building a list. Send an `items` array; each item takes the same fields as Create a thing (`text`, optional `type`, `url`, `list_ref`, `tags`, `photos`, `completed`, `tabs`, `created`). Set a top-level `list_id` as the default target, or give each item its own. `skip_duplicates: true` skips items already present (by url, else text), making re-runs safe. Returns per-list counts, not the items. Max 500 per call. Body: - `items` (object[], required): The things to create (max 500). Each item: { text, list_id?, list?, type?, url?, list_ref?, tags?, photos?, completed?, tabs?, created?, and formatting: bold?, italic?, underline?, header?, subheader?, quote?, code? }. - `list_id` (string, optional): Default list for items that don’t set their own list_id. - `list` (string, optional): Alternative default target: a list name, or "today"/"tomorrow"/an ISO date. - `skip_duplicates` (boolean, optional): Skip items already in the target list (by url, else text). Example: ``` curl -X POST "https://www.twosapp.com/api/v1/things/bulk" \ -H "Authorization: Bearer twos_YOUR_KEY" \ -H "Content-Type: application/json" \ -d '{"list_id":"64f0c2a1e4b0a1234567890a","items":[{"text":"Oat milk","type":"todo"},{"text":"Sourdough","type":"todo"}]}' ``` ### PATCH /api/v1/things/bulk Bulk-update things. Scope: `write:things`. Returns 200. Apply one change set to many things at once — check them all off, move them to a list, star them. Send `ids` (the thing ids) and `set` (fields to apply to each, same shape as Update a thing). A move target (`list_id` or `list` name) is resolved once. Ids you no longer own are skipped, not errors. Max 500 ids per call. Body: - `ids` (string[], required): Ids of the things to change (max 500). - `set` (object, required): Fields to apply to every listed thing: { completed?, canceled?, favorited?, type?, tags?, tabs?, url?, list_ref?, list_id?, list?, and formatting: bold?, italic?, underline?, header?, subheader?, quote?, code? }. Example: ``` curl -X PATCH "https://www.twosapp.com/api/v1/things/bulk" \ -H "Authorization: Bearer twos_YOUR_KEY" \ -H "Content-Type: application/json" \ -d '{"ids":["64f0c2a1e4b0a1234567890b"],"set":{"completed":true}}' ``` ### DELETE /api/v1/things/bulk Bulk-delete things. Scope: `write:things`. Returns 200. Permanently delete many things at once by id. This cannot be undone. Returns how many were deleted (unknown ids are ignored). Max 500 ids per call. Body: - `ids` (string[], required): Ids of the things to delete (max 500). Example: ``` curl -X DELETE "https://www.twosapp.com/api/v1/things/bulk" \ -H "Authorization: Bearer twos_YOUR_KEY" \ -H "Content-Type: application/json" \ -d '{"ids":["64f0c2a1e4b0a1234567890b"]}' ``` ### POST /api/v1/things/markdown Append markdown to a list. Scope: `write:things`. Returns 201. Parse a markdown block into things and append them to a list in one call. Checkboxes (`- [ ]`/`- [x]`) become todos, `1.` numbered items, `-`/`*` bullets, `>` notes, `##` headings, `###` subheadings; leading indentation nests items. Target the list with `list_id` or `list` (name / "today"). `skip_duplicates: true` makes a re-run safe. Body: - `markdown` (string, required): The markdown to parse into things. - `list_id` (string, optional): The list to append to. - `list` (string, optional): Alternative to list_id: a list name, or "today"/"tomorrow"/an ISO date. - `skip_duplicates` (boolean, optional): Skip items already present (by url, else text). Example: ``` curl -X POST "https://www.twosapp.com/api/v1/things/markdown" \ -H "Authorization: Bearer twos_YOUR_KEY" \ -H "Content-Type: application/json" \ -d '{"list_id":"64f0c2a1e4b0a1234567890a","markdown":"- [ ] Oat milk\n- [ ] Sourdough"}' ``` ### GET /api/v1/things/{id} Get a thing. Scope: `read:things`. Returns 200. Returns a single thing you own, plus its reminder (or null). Parameters: - `id` (string, path, required): The thing id. Example: ``` curl "https://www.twosapp.com/api/v1/things/64f0c2a1e4b0a1234567890b" \ -H "Authorization: Bearer twos_YOUR_KEY" ``` ### PATCH /api/v1/things/{id} Update a thing. Scope: `write:things`. Returns 200. Updates a thing. Only the fields you send change. Set `completed`/`canceled` to complete or cancel a todo, `list_id` to move it to another list, or `list_ref` to reference another list. Content edits keep the thing’s position in its list — only moving it (new `list_id`) changes its order. Completing (or canceling) a thing with a repeating reminder works like the app: a completed copy stays where it was done, the reminder advances to the next occurrence, and the thing itself comes back open on that occurrence’s day. The response then includes `repeating_reminder` with the copy’s id and the next occurrence. Completing or canceling a thing with a one-off reminder removes the reminder, and the response includes `reminder_removed: true`. Parameters: - `id` (string, path, required): The thing id. Body: - `text` (string, optional): New text for the thing, in markdown (same dialect as reads, so a read-modify-write keeps its formatting). - `type` ("todo" | "note" | "dash" | "number" | "bullet" | "none" | "photo", optional): Change the thing type: `todo`, `note`/`none`, `dash`, `number`, or `bullet`. - `url` (string, optional): Set or clear the hyperlink. - `list_ref` (string, optional): Set a referenced list id (a navigable list link), or pass an empty string to clear it. - `tags` (string[], optional): Replace the thing’s tags. - `photos` (string[], optional): Replace the attached image URLs (http(s); max 10). Pass [] to remove all photos. - `completed` (boolean, optional): Mark complete or incomplete. - `canceled` (boolean, optional): Cancel or un-cancel the thing. - `favorited` (boolean, optional): Star or unstar the thing. - `tabs` (integer, optional): Indent level: 0 = top level; 1+ nests under the preceding thing. - `list_id` (string, optional): Move the thing to another list you own. - `list` (string, optional): Alternative to list_id for a move: a list name, or "today"/"tomorrow"/an ISO date. - `bold` (boolean, optional): Bold the whole thing. - `italic` (boolean, optional): Italicize the whole thing. - `underline` (boolean, optional): Underline the whole thing. - `header` (boolean, optional): Make the thing a heading (H1). Mutually exclusive with `subheader`; also bolds it unless you pass `bold` explicitly. - `subheader` (boolean, optional): Make the thing a subheading (H2). Mutually exclusive with `header`; also bolds it unless you pass `bold` explicitly. - `quote` (boolean, optional): Style the thing as a blockquote. Mutually exclusive with `code`. - `code` (boolean, optional): Style the thing as a code block. Mutually exclusive with `quote`. - `note` (string, optional): A long-form note attached to the thing, in **markdown**. Shown behind a note icon in the app, never inline with `text`. Pass `""` to remove it. Reads return the same markdown, so a note round-trips unchanged. Example: ``` curl -X PATCH "https://www.twosapp.com/api/v1/things/64f0c2a1e4b0a1234567890b" \ -H "Authorization: Bearer twos_YOUR_KEY" \ -H "Content-Type: application/json" \ -d '{"completed":true}' ``` ### DELETE /api/v1/things/{id} Delete a thing. Scope: `write:things`. Returns 200. Permanently deletes a thing. Idempotent. Parameters: - `id` (string, path, required): The thing id. Example: ``` curl -X DELETE "https://www.twosapp.com/api/v1/things/64f0c2a1e4b0a1234567890b" \ -H "Authorization: Bearer twos_YOUR_KEY" ``` ### PUT /api/v1/things/{id}/reminder Set a reminder. Scope: `write:things`. Returns 200. Set or replace the reminder on a thing. A thing has at most one reminder. Parameters: - `id` (string, path, required): The thing id. Body: - `at` (string, required): When to remind — ISO 8601. Include a timezone offset or Z for an exact time (e.g. 2026-06-03T17:00:00-04:00). A naive time (no offset) is interpreted in the user’s timezone. - `all_day` (boolean, optional): Treat it as an all-day reminder. - `duration_minutes` (integer, optional): How long the calendar block lasts, in minutes. 0 gives it no end time. Defaults to your "Default reminder duration" setting. Cannot be combined with end or all_day. - `end` (string, optional): When the block ends — ISO 8601, an alternative to duration_minutes. Must use the same form as at: both with a timezone offset, or both without. - `repeat` ("none" | "daily" | "weekly" | "monthly" | "yearly", optional): Recurrence. Defaults to "none". - `every` (integer, optional): Repeat every N of the chosen interval, so repeat "monthly" with every 3 is every 3 months. Defaults to 1. Cannot be combined with days. - `repeat_until` (string, optional): Stop repeating after this date — ISO 8601. Must be after at. - `days` (string[], optional): For weekly reminders, the weekdays it repeats on — names ("monday") or 0–6 (0=Sunday). Providing days makes it weekly. - `alert_minutes` (integer, optional): Notify this many minutes before. Defaults to your reminder settings. 0 means no advance notice. - `second_alert_minutes` (integer, optional): A second, independent notification this many minutes before — e.g. 1440 for a day before, alongside a 60-minute first alert. 0 means none. - `color` (string, optional): Hex color. Defaults to your default reminder color. Example: ``` curl -X PUT "https://www.twosapp.com/api/v1/things/64f0c2a1e4b0a1234567890b/reminder" \ -H "Authorization: Bearer twos_YOUR_KEY" \ -H "Content-Type: application/json" \ -d '{"at":"2026-06-03T17:00:00Z","duration_minutes":30,"repeat":"monthly","every":3,"alert_minutes":60,"second_alert_minutes":1440}' ``` ### DELETE /api/v1/things/{id}/reminder Remove a reminder. Scope: `write:things`. Returns 200. Remove the reminder from a thing. Idempotent. Parameters: - `id` (string, path, required): The thing id. Example: ``` curl -X DELETE "https://www.twosapp.com/api/v1/things/64f0c2a1e4b0a1234567890b/reminder" \ -H "Authorization: Bearer twos_YOUR_KEY" ``` ### GET /api/v1/reminders List reminders. Scope: `read:things`. Returns 200. Your upcoming reminders (future first), up to 50. Example: ``` curl "https://www.twosapp.com/api/v1/reminders" \ -H "Authorization: Bearer twos_YOUR_KEY" ``` ### GET /api/v1/search Search. Scope: `search`. Returns 200. Case-insensitive search across your list titles and thing text. Returns up to 50 lists + 50 things; `has_more` is true when results were capped (narrow the query). Not a complete enumeration — page `/things` to read a whole list. Parameters: - `query` (string, query, required): The search query (the legacy alias `q` also works). Example: ``` curl "https://www.twosapp.com/api/v1/search?query=milk" \ -H "Authorization: Bearer twos_YOUR_KEY" ``` ### GET /api/v1/fetch Fetch a document. Scope: `read:things`. Returns 200. Fetch one resource (a list or a thing) by id and return it as a single document — `{ id, title, text, url, metadata }`. For a list, `text` is its things rendered as plain text. Pairs with search (search returns ids, fetch retrieves full content). Primarily for AI deep-research clients. Parameters: - `id` (string, query, required): A list or thing id. Example: ``` curl "https://www.twosapp.com/api/v1/fetch?id=64f0c2a1e4b0a1234567890b" \ -H "Authorization: Bearer twos_YOUR_KEY" ``` ### GET /api/v1/tags List tags. Scope: `read:things`. Returns 200. The tags you use across your things, with usage counts. Example: ``` curl "https://www.twosapp.com/api/v1/tags" \ -H "Authorization: Bearer twos_YOUR_KEY" ``` ## Errors Every error is a standard status code with a `{ "error": "..." }` body. - 400 Bad Request: A required field or parameter is missing or malformed. - 401 Unauthorized: The API key is missing, invalid, or has been revoked. - 403 Forbidden: The key is valid but lacks the scope required for this endpoint. - 404 Not Found: The referenced list or thing does not exist or is not owned by you. - 429 Too Many Requests: The rate limit of 1000 requests per hour has been exceeded. ## MCP tools The MCP server exposes the same operations as named tools, with the same scopes. - `list_lists` (`read:lists`): List your lists. - `get_list` (`read:lists`): Get a list and its things (in the app’s display order). - `get_today` (`read:lists`): Get/create the day list for today, yesterday, tomorrow, or a date. - `create_list` (`write:lists`): Create a list (optionally filled with `things` in one call). - `import_markdown` (`write:lists`): Create a new list from a markdown document. - `update_list` (`write:lists`): Rename, star, or archive a list. - `delete_list` (`write:lists`): Delete a list and its things. - `list_things` (`read:things`): List things newest-first (filter by list, completion, tag). - `create_thing` (`write:things`): Create a todo or note (target a list by id or name). - `create_things` (`write:things`): Bulk-create many things in one call (returns per-list counts). - `append_markdown` (`write:things`): Append a markdown block to a list as things. - `get_thing` (`read:things`): Get a thing. - `update_thing` (`write:things`): Edit, complete, cancel, or move a thing. - `update_things` (`write:things`): Apply one change to many things (check off / move / star). - `delete_thing` (`write:things`): Delete a thing. - `delete_things` (`write:things`): Delete many things at once by id. - `set_reminder` (`write:things`): Set or replace a reminder on a thing, at a time or on arriving at or leaving a place. - `remove_reminder` (`write:things`): Remove a thing’s reminder. - `list_reminders` (`read:things`): List your reminders, optionally within a date range. - `list_places` (`read:things`): List your saved places (Home, Work, and so on). - `save_place` (`write:things`): Save or update a named place from an address. - `search` (`search`): Search your lists and things. - `fetch` (`read:things`): Fetch one list or thing by id as a document. - `list_tags` (`read:things`): List your tags with counts.