API reference
Two surfaces, with different promises.
/api/v1/… is public. Third parties hold these paths in config files
we cannot edit, so they are versioned and will not change shape beneath
anyone. Everything below documents this surface.
/api/… is internal. It exists for the tidee web and mobile clients,
changes whenever they do, and is not documented here. Personal access
tokens are refused there by design: those routes assert no scopes, so a
token minted for capture alone would otherwise reach every one of them.
/e2e/… is test-only and gated on TIDEE_LIFE_API_AUTH_UUID.
Base URL: https://app.tidee.com
Authenticating
Two kinds of credential reach the public API, both as
Authorization: Bearer ….
Personal access tokens
For your own scripts and for MCP clients you configure by hand. Create one in tidee under Application Settings → Integrations. It is shown once and stored only as a hash — if you lose it, revoke it and make another.
Authorization: Bearer tdl_…
OAuth 2.1
For applications other people will connect. The user approves the application on a consent screen and never handles a secret; they can disconnect it at any time, which stops it working immediately.
Discovery is automatic — an unauthenticated call to /mcp returns a 401
whose WWW-Authenticate header points at
/.well-known/oauth-protected-resource, which names the authorization
server. From there:
| Endpoint | Purpose |
|---|---|
GET /.well-known/oauth-authorization-server | RFC 8414 metadata |
POST /oauth/register | Dynamic client registration (RFC 7591) |
GET /oauth/authorize | Authorization, PKCE required |
POST /oauth/token | authorization_code and refresh_token |
POST /oauth/revoke | RFC 7009 revocation |
Registration is open — no arrangement with us is needed. What that means in practice:
- PKCE with
S256is required.plainis refused and not advertised. - Redirect URIs are matched exactly against what you registered.
httpsanywhere,httponly to loopback, or a private-use scheme containing a dot. - Authorization codes are single-use and short-lived. Presenting one twice revokes the grant, on the assumption that two parties hold it.
- Refresh tokens rotate, and presenting a rotated one revokes the grant for the same reason.
- Access tokens last an hour; refresh tokens last 60 days from last use.
- Clients are public — there is no client secret, because a desktop or CLI client cannot keep one.
Scopes
| Scope | Allows |
|---|---|
capture | POST /api/v1/capture |
items:read | GET /api/v1/boxes, GET /api/v1/items |
items:write | POST /api/v1/items, PATCH /api/v1/items/:id |
Every endpoint asserts the scope it needs. Ask for the least you can work with — a user reading a consent screen is more likely to approve a short list, and a leaked credential does less.
Endpoints
POST /api/v1/capture
Free text in, items out. The one to reach for when you have a sentence rather than a decided title: tidee works out the separate items, which box each belongs in, and any dates.
{ "text": "book the MOT and call Sam about Friday", "parentId": null }
| Field | Meaning | |
|---|---|---|
text | required | What the user said, in their own words. |
parentId | optional | Box id to file everything in. Omit to let tidee choose. |
mode | optional | plan (default) or literal. |
{
"created": [{ "itemShareId": "…", "title": "book the MOT" }],
"createdCount": 2,
"usedFallback": false
}
usedFallback: true means the text could not be interpreted and was
stored verbatim as a single item. Nothing is ever lost — worth relaying,
because the result will not look like what was asked for.
It rewords as it files. “get some milk tomorrow” becomes an item
called “milk” with a date. That is wanted when someone is thinking
aloud, and wrong when they have chosen their words — quote the title, or
use POST /api/v1/items, which stores it exactly.
POST /api/v1/items
One item, title used exactly as given.
{
"title": "Get some milk",
"parentId": null,
"targetDate": "2026-09-10T09:00:00"
}
targetDate is when the user should ACT, as local time with no
timezone; the server resolves it against their profile timezone. Not the
date of an event the task is merely about — “book a table for the 14th”
needs doing before the 14th, so leave it out. Defaults to 09:00 when no
time is given.
GET /api/v1/boxes
{ "boxes": [{ "id": "…", "name": "Work" }], "workspaceRootId": "…" }
GET /api/v1/items
Query: q (search titles), parentId (one box), limit (default 50,
capped at 200).
{
"items": [
{
"id": "…",
"title": "book the MOT",
"itemType": "todoText",
"completed": false,
"parentId": "…",
"targetDate": null,
"createdDate": "2026-09-01T09:00:00.000Z"
}
]
}
PATCH /api/v1/items/:itemId
{ "completed": true }
Accepts completed, title, or both. An id belonging to someone else
simply does not resolve.
Errors
One shape, because the caller is a program:
{ "error": { "code": "error.free.tier.capture.limit", "message": "…" } }
Branch on code, not on prose. The ones worth handling:
| Status | code | Meaning |
|---|---|---|
| 400 | error.api.v1.text.required | Missing or empty input. |
| 401 | error.api.token.invalid | Token revoked, expired, or wrong. |
| 402 | error.free.tier.capture.limit | Free tier cap reached. The item was NOT created; the user must upgrade. |
| 403 | error.api.token.scope.missing | Valid credential, wrong scope. |
| 429 | rate_limited | 60 requests per minute, per token. |
Limits
- 60 requests per minute per token on the public API, 120 on
/mcp. Keyed on the token rather than the account, so one integration cannot exhaust another’s budget. - Free accounts have a lifetime cap of 50 items. Captures through this API count against it exactly as captures in the app do — there is no separate allowance, and a 402 means the write did not happen.
MCP
https://app.tidee.com/mcp speaks MCP over streamable HTTP, with the
same five operations as tools. It is stateless: no session id, one
request one answer. Authenticate with either credential above.
There is also a local server for clients that prefer stdio — see
packages/tidee-life-mcp/README.md.