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:

EndpointPurpose
GET /.well-known/oauth-authorization-serverRFC 8414 metadata
POST /oauth/registerDynamic client registration (RFC 7591)
GET /oauth/authorizeAuthorization, PKCE required
POST /oauth/tokenauthorization_code and refresh_token
POST /oauth/revokeRFC 7009 revocation

Registration is open — no arrangement with us is needed. What that means in practice:

  • PKCE with S256 is required. plain is refused and not advertised.
  • Redirect URIs are matched exactly against what you registered. https anywhere, http only 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

ScopeAllows
capturePOST /api/v1/capture
items:readGET /api/v1/boxes, GET /api/v1/items
items:writePOST /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 }
FieldMeaning
textrequiredWhat the user said, in their own words.
parentIdoptionalBox id to file everything in. Omit to let tidee choose.
modeoptionalplan (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:

StatuscodeMeaning
400error.api.v1.text.requiredMissing or empty input.
401error.api.token.invalidToken revoked, expired, or wrong.
402error.free.tier.capture.limitFree tier cap reached. The item was NOT created; the user must upgrade.
403error.api.token.scope.missingValid credential, wrong scope.
429rate_limited60 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.