Developer Documentation
Taduu API & MCP server
Programmatic access to your Taduu lists, tasks, notes, and inbox. Same Personal Access Token works for the REST API and the MCP server — so Claude can add tasks to your account while you chat, on Desktop, Web, or Mobile.
Overview
Two ways to integrate with Taduu:
- REST API at
https://taduu.com/api/v1/*— what you'd expect: JSON in, JSON out, Bearer auth. - MCP server at
https://taduu.com/mcp— the same operations exposed as native tools for Claude (Desktop, web, mobile) and any other Model Context Protocol client.
Two paths for creating tasks: POST /api/v1/inbox for text-only quick-capture (brain dump path), and POST /api/v1/tasks for fully-defined tasks with list, priority, due date, and assignee.
The API is Pro-gated — token holders need an active Pro trial or paid Pro plan. Tokens stop working as soon as the trial expires.
Quickstart
Five steps from zero to your first task created via the API. If you're integrating someone else's Taduu account into your app, send them step 1 and do the rest with the token they hand you.
1. Get a token
- Sign in at taduu.com — Pro plan or active trial required.
- Open Settings → Integrations → New token
- Pick scopes — for most integrations:
tasks:read,tasks:write,inbox:write,lists:read,users:read. - Copy the
tdu_live_…token. It's shown ONCE — store it safely.
2. Verify the token
Always do this first. If it returns 200, you're authenticated.
curl
curl https://taduu.com/api/v1/me \
-H "Authorization: Bearer tdu_live_..."3. Find a list
Most task creation needs a target list. Grab one from the response's data array and save the id.
curl
curl https://taduu.com/api/v1/lists \
-H "Authorization: Bearer tdu_live_..."4. Create a task
Two paths. Pick whichever fits the use case:
Quick capture (no list, no structure)
Text lands in the user's Unsorted inbox. They'll define it later via the Define wizard.
POST /api/v1/inbox
curl -X POST https://taduu.com/api/v1/inbox \
-H "Authorization: Bearer tdu_live_..." \
-H "Content-Type: application/json" \
-d '{"text":"Buy groceries on the way home"}'Fully-defined (list + priority + due + assignee)
Bypasses Define. The task lands ready to act on.
POST /api/v1/tasks
curl -X POST https://taduu.com/api/v1/tasks \
-H "Authorization: Bearer tdu_live_..." \
-H "Content-Type: application/json" \
-d '{
"listId": "65f3...",
"name": "Submit Q3 report",
"priority": "High",
"dueDate": "2026-06-15T17:00:00Z",
"assignedTo": "sarah@example.com",
"notes": "Include the EBITDA breakdown"
}'priority is Low | Medium | High | Critical. dueDate is ISO 8601. For assignment-by-email, call /api/v1/users first to get valid emails.
5. Update, complete, delete
Mark complete
curl -X PATCH https://taduu.com/api/v1/tasks/TASK_ID \
-H "Authorization: Bearer tdu_live_..." \
-H "Content-Type: application/json" \
-d '{"status":"Complete"}'Change priority + reassign
curl -X PATCH https://taduu.com/api/v1/tasks/TASK_ID \
-H "Authorization: Bearer tdu_live_..." \
-H "Content-Type: application/json" \
-d '{"priority":"Critical","assignedTo":"mitch@example.com"}'Soft-delete (preserves history)
curl -X DELETE https://taduu.com/api/v1/tasks/TASK_ID \
-H "Authorization: Bearer tdu_live_..."End-to-end example: Node
Find a list by name, fall back to the first one, create a task due in 7 days.
Node (fetch)
const TOKEN = process.env.TADUU_TOKEN;
const auth = { Authorization: `Bearer ${TOKEN}` };
const json = { ...auth, "Content-Type": "application/json" };
// 1. Find a list
const lists = await fetch("https://taduu.com/api/v1/lists", { headers: auth })
.then((r) => r.json());
const target =
lists.data.find((l) => l.name === "Work") ?? lists.data[0];
// 2. Create a task
const res = await fetch("https://taduu.com/api/v1/tasks", {
method: "POST",
headers: json,
body: JSON.stringify({
listId: target.id,
name: "Auto-generated from MyApp",
priority: "Medium",
dueDate: new Date(Date.now() + 86400000 * 7).toISOString(),
}),
});
const task = await res.json();
console.log("Created", task.id, task.taskId);End-to-end example: Python
Python (requests)
import os, requests
BASE = "https://taduu.com/api/v1"
H = {"Authorization": f"Bearer {os.environ['TADUU_TOKEN']}"}
# 1. Find a list
lists = requests.get(f"{BASE}/lists", headers=H).json()["data"]
target = next((l for l in lists if l["name"] == "Work"), lists[0])
# 2. Create a task
task = requests.post(
f"{BASE}/tasks",
headers={**H, "Content-Type": "application/json"},
json={
"listId": target["id"],
"name": "Auto-generated from MyApp",
"priority": "Medium",
},
).json()
print("Created", task["id"], task["taskId"])Authentication
Create a Personal Access Token at Settings → Integrations. Tokens look like tdu_live_… and are shown only once at creation — copy and store them safely.
Send the token as a Bearer header on every request:
http
Authorization: Bearer tdu_live_yourTokenHereTokens can be scoped to a single workspace (locked) or all workspaces the user belongs to. Lock down the blast radius by giving each integration its own narrowly-scoped token.
Scopes
Each token carries a set of scopes. Endpoints declare which scope they need; missing the scope returns 403.
| Scope | Grants |
|---|---|
| me:read | Always granted. Verify token identity. |
| workspaces:read | List workspaces the token can reach. |
| users:read | List workspace members (for assignment). |
| lists:read | Read lists. |
| lists:write | Create / rename lists. |
| tasks:read | Read action items. |
| tasks:write | Create / update / delete action items. |
| notes:read | Read accessible notes. |
| notes:write | Create / update notes. |
| inbox:read | Read Unsorted inbox items (for triage). |
| inbox:write | Drop items into Unsorted; promote / delete them. |
| locations:read | Read the user's named Locations. |
| locations:write | Create / update / delete Locations. |
| time:read | Read time entries in the workspace. |
| time:write | Log / update / delete time entries. |
| webhooks:read | List webhook subscriptions. |
| webhooks:write | Create / update / delete webhook subscriptions. |
Errors
Failures return JSON in the shape { "error": { "code": "...", "message": "..." } } with a standard HTTP status code:
- 400 — invalid body / query params.
- 401 — missing or invalid Bearer token.
- 402 — Pro required. The token holder isn't on Pro (trial expired or never enrolled).
- 403 — token lacks the required scope.
- 404 — resource not found or not visible to this token. We don't differentiate between "doesn't exist" and "you can't see it" — no info leak.
- 409 — conflict (e.g. list name already taken in this workspace).
- 429 — rate-limited. See below.
- 500 — internal error. Retry with exponential backoff.
Rate limits
300 requests/minute per token, shared across REST and MCP. Every response includes:
x-ratelimit-limit— your current ceiling.x-ratelimit-remaining— requests left in this window.x-ratelimit-reset— Unix timestamp when the window resets.
Identity
/api/v1/mescope: me:readGet current token identity
Confirms the token is valid and reports who it belongs to + what scopes it carries + the workspace it can reach. Useful as a connectivity check.
curl
curl https://taduu.com/api/v1/me \
-H "Authorization: Bearer $TADUU_TOKEN"Node (fetch)
const res = await fetch("https://taduu.com/api/v1/me", {
headers: { Authorization: `Bearer ${process.env.TADUU_TOKEN}` },
});
const me = await res.json();
console.log(me);Python (requests)
import os, requests
res = requests.get(
"https://taduu.com/api/v1/me",
headers={"Authorization": f"Bearer {os.environ['TADUU_TOKEN']}"}
)
print(res.json())Workspaces
/api/v1/workspacesscope: workspaces:readList workspaces
Lists workspaces this token can access. Workspace-scoped tokens return exactly one; all-workspaces tokens return every workspace the user is a member of.
curl
curl https://taduu.com/api/v1/workspaces \
-H "Authorization: Bearer $TADUU_TOKEN"Node (fetch)
const res = await fetch("https://taduu.com/api/v1/workspaces", {
headers: { Authorization: `Bearer ${process.env.TADUU_TOKEN}` },
});
const { data } = await res.json();
for (const ws of data) console.log(ws.id, ws.name);Python (requests)
import os, requests
res = requests.get(
"https://taduu.com/api/v1/workspaces",
headers={"Authorization": f"Bearer {os.environ['TADUU_TOKEN']}"}
)
for ws in res.json()["data"]:
print(ws["id"], ws["name"])/api/v1/usersscope: users:readList workspace members
Lists workspace members so you can resolve an assignee by name → email. Pass the email back as the `assignedTo` argument when creating or updating tasks. Each member also carries `role` (workspace-level: owner / admin / member / guest) and `engagementType` (Crew enrollment: employee / volunteer / contractor, or null when not enrolled) so integrations like Limitless Finances can map a Taduu user → a person in their own ledger.
curl
curl "https://taduu.com/api/v1/users?workspaceId=$WORKSPACE_ID" \
-H "Authorization: Bearer $TADUU_TOKEN"Node (fetch)
const res = await fetch(
`https://taduu.com/api/v1/users?workspaceId=${workspaceId}`,
{ headers: { Authorization: `Bearer ${process.env.TADUU_TOKEN}` } },
);
const { data } = await res.json();Python (requests)
import os, requests
res = requests.get(
"https://taduu.com/api/v1/users",
params={"workspaceId": workspace_id},
headers={"Authorization": f"Bearer {os.environ['TADUU_TOKEN']}"}
)
print(res.json()["data"])Lists
/api/v1/listsscope: lists:readList accessible lists
Returns every list the token holder can see in the resolved workspace, honoring per-list roles and visibility.
curl
curl https://taduu.com/api/v1/lists \
-H "Authorization: Bearer $TADUU_TOKEN"Node (fetch)
const res = await fetch("https://taduu.com/api/v1/lists", {
headers: { Authorization: `Bearer ${process.env.TADUU_TOKEN}` },
});
const { data } = await res.json();Python (requests)
import os, requests
res = requests.get(
"https://taduu.com/api/v1/lists",
headers={"Authorization": f"Bearer {os.environ['TADUU_TOKEN']}"}
)
print(res.json()["data"])/api/v1/listsscope: lists:writeCreate a list
Creates a new list. The token holder becomes the owner. Returns 409 if a list with that name already exists in the workspace.
curl
curl -X POST https://taduu.com/api/v1/lists \
-H "Authorization: Bearer $TADUU_TOKEN" \
-H "Content-Type: application/json" \
-d '{"name":"Q4 launch"}'Node (fetch)
const res = await fetch("https://taduu.com/api/v1/lists", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.TADUU_TOKEN}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ name: "Q4 launch" }),
});
const list = await res.json();Python (requests)
import os, requests
res = requests.post(
"https://taduu.com/api/v1/lists",
headers={
"Authorization": f"Bearer {os.environ['TADUU_TOKEN']}",
"Content-Type": "application/json",
},
json={"name": "Q4 launch"},
)
print(res.json())Tasks
External system correlation & upsert
Tasks accept an optional externalRef string (≤300 chars) for integrations that pushed the task in from another system. Example values: limitless-finances:txn:abc123, zendesk:ticket:55041. Filter back by passing ?externalRef=limitless-finances:txn:abc123 to GET /api/v1/tasks.
POST behaves as an upsert when externalRef is set: if an OPEN task with the same (workspace, externalRef) already exists, the existing row is updated in place (200) instead of duplicated. Complete or Deleted rows are never reopened — a new task is created so history stays intact. Returns 201 on a fresh create.
Optional source field (≤60 chars) names the recording system, e.g. limitless-finances. Distinct from externalRef (which is the per-item id). Powers integration badges in the Taduu UI.
Field aliases: POST also accepts title for name, note for notes, and externalId for externalRef. Use whichever naming your client framework prefers.
If listId is omitted, the task lands in the workspace's Unsorted list (or the first accessible non-personal list if Unsorted isn't present).
/api/v1/tasksscope: tasks:readSearch tasks
Search action items. Filters are AND-combined. Pagination is cursor-based — pass the previous response's `nextCursor` as `cursor` to get the next page.
curl
curl "https://taduu.com/api/v1/tasks?completed=false&dueBefore=2026-06-30T00:00:00Z" \
-H "Authorization: Bearer $TADUU_TOKEN"Node (fetch)
const params = new URLSearchParams({
completed: "false",
dueBefore: "2026-06-30T00:00:00Z",
});
const res = await fetch(
`https://taduu.com/api/v1/tasks?${params}`,
{ headers: { Authorization: `Bearer ${process.env.TADUU_TOKEN}` } },
);
const { data, nextCursor } = await res.json();Python (requests)
import os, requests
res = requests.get(
"https://taduu.com/api/v1/tasks",
params={"completed": "false", "dueBefore": "2026-06-30T00:00:00Z"},
headers={"Authorization": f"Bearer {os.environ['TADUU_TOKEN']}"}
)
print(res.json()["data"])/api/v1/tasksscope: tasks:writeCreate a task
Creates a fully-defined task in a specific list. Use POST /api/v1/inbox instead if you don't know the target list yet.
completedAt to back-date as already complete (lands in Complete status, not Not Started).curl
curl -X POST https://taduu.com/api/v1/tasks \
-H "Authorization: Bearer $TADUU_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"listId": "65f3a7...",
"name": "Submit Q3 report",
"priority": "High",
"dueDate": "2026-06-15T17:00:00Z",
"assignedTo": "sarah@example.com"
}'Node (fetch)
const res = await fetch("https://taduu.com/api/v1/tasks", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.TADUU_TOKEN}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
listId: "65f3a7...",
name: "Submit Q3 report",
priority: "High",
dueDate: "2026-06-15T17:00:00Z",
assignedTo: "sarah@example.com",
}),
});Python (requests)
import os, requests
res = requests.post(
"https://taduu.com/api/v1/tasks",
headers={
"Authorization": f"Bearer {os.environ['TADUU_TOKEN']}",
"Content-Type": "application/json",
},
json={
"listId": "65f3a7...",
"name": "Submit Q3 report",
"priority": "High",
"dueDate": "2026-06-15T17:00:00Z",
"assignedTo": "sarah@example.com",
},
)/api/v1/tasks/{id}scope: tasks:writeUpdate a task
Partial update. Only the fields you pass are changed. Pass null on nullable fields to clear them.
curl
curl -X PATCH https://taduu.com/api/v1/tasks/65f3a7... \
-H "Authorization: Bearer $TADUU_TOKEN" \
-H "Content-Type: application/json" \
-d '{"status":"Complete"}'Node (fetch)
await fetch("https://taduu.com/api/v1/tasks/65f3a7...", {
method: "PATCH",
headers: {
Authorization: `Bearer ${process.env.TADUU_TOKEN}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ status: "Complete" }),
});Python (requests)
import os, requests
requests.patch(
f"https://taduu.com/api/v1/tasks/{task_id}",
headers={
"Authorization": f"Bearer {os.environ['TADUU_TOKEN']}",
"Content-Type": "application/json",
},
json={"status": "Complete"},
)/api/v1/tasks/{id}scope: tasks:writeDelete a task
Soft-delete — status flips to 'Deleted'. The task disappears from active views but is preserved for audit / history.
curl
curl -X DELETE https://taduu.com/api/v1/tasks/65f3a7... \
-H "Authorization: Bearer $TADUU_TOKEN"Node (fetch)
await fetch("https://taduu.com/api/v1/tasks/65f3a7...", {
method: "DELETE",
headers: { Authorization: `Bearer ${process.env.TADUU_TOKEN}` },
});Python (requests)
import os, requests
requests.delete(
f"https://taduu.com/api/v1/tasks/{task_id}",
headers={"Authorization": f"Bearer {os.environ['TADUU_TOKEN']}"}
)Inbox
/api/v1/inboxscope: inbox:writeQuick-capture into Unsorted
Drops a single thought into the user's Unsorted inbox. The brain-dump path — text only, no list, no structure. The user triages it later via the Define wizard.
curl
curl -X POST https://taduu.com/api/v1/inbox \
-H "Authorization: Bearer $TADUU_TOKEN" \
-H "Content-Type: application/json" \
-d '{"text":"Pick up dry cleaning Friday"}'Node (fetch)
await fetch("https://taduu.com/api/v1/inbox", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.TADUU_TOKEN}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
text: "Pick up dry cleaning Friday",
}),
});Python (requests)
import os, requests
requests.post(
"https://taduu.com/api/v1/inbox",
headers={
"Authorization": f"Bearer {os.environ['TADUU_TOKEN']}",
"Content-Type": "application/json",
},
json={"text": "Pick up dry cleaning Friday"},
)Notes
/api/v1/notesscope: notes:readList notes
Notes the token holder owns or has been explicitly shared on. Workspace-scoped tokens still see only their owner's notes.
curl
curl https://taduu.com/api/v1/notes \
-H "Authorization: Bearer $TADUU_TOKEN"Node (fetch)
const res = await fetch("https://taduu.com/api/v1/notes", {
headers: { Authorization: `Bearer ${process.env.TADUU_TOKEN}` },
});
const { data } = await res.json();Python (requests)
import os, requests
res = requests.get(
"https://taduu.com/api/v1/notes",
headers={"Authorization": f"Bearer {os.environ['TADUU_TOKEN']}"}
)
print(res.json()["data"])/api/v1/notesscope: notes:writeCreate a note
Creates a private note in the user's Personal workspace. Sharing is a separate flow.
curl
curl -X POST https://taduu.com/api/v1/notes \
-H "Authorization: Bearer $TADUU_TOKEN" \
-H "Content-Type: application/json" \
-d '{"title":"Trip ideas","body":"…"}'Node (fetch)
await fetch("https://taduu.com/api/v1/notes", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.TADUU_TOKEN}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ title: "Trip ideas", body: "..." }),
});Python (requests)
import os, requests
requests.post(
"https://taduu.com/api/v1/notes",
headers={
"Authorization": f"Bearer {os.environ['TADUU_TOKEN']}",
"Content-Type": "application/json",
},
json={"title": "Trip ideas", "body": "..."},
)Notes from sibling apps
When another Limitless app creates a note (e.g. InboxOps making a reply draft), tag it with three optional fields so the Taduu UI knows where it came from:
source— short tag (e.g.inboxops). Drives the “Send via X” button on the note.externalRef— stable id from your side. Lets you address the note via /by-external-ref/{ref} without storing Taduu's Mongo id.externalUrl— deep link back to the original artefact (e.g. the Gmail message the user is drafting a reply to). Rendered as “View original in X”.
POST is idempotent on (source, externalRef) — safe to retry. The send loop: your app POSTs the note → user edits in Taduu → user clicks Send → Taduu deep-links them to your compose page with the edited body → user sends → your app fires DELETE to make the note disappear.
/api/v1/notes/by-external-ref/{ref}scope: notes:readGet a note by externalRef
Reads back a note by the externalRef your app set at create time. URL-encode the ref. The source query param disambiguates two integrations sharing a ref string.
curl
curl https://taduu.com/api/v1/notes/by-external-ref/inboxops%3Amsg%3Aabc \
-G --data-urlencode "source=inboxops" \
-H "Authorization: Bearer $TADUU_TOKEN"Node (fetch)
const res = await fetch(
`https://taduu.com/api/v1/notes/by-external-ref/${encodeURIComponent(ref)}?source=inboxops`,
{ headers: { Authorization: `Bearer ${process.env.TADUU_TOKEN}` } }
);Python (requests)
import os, requests
from urllib.parse import quote
res = requests.get(
f"https://taduu.com/api/v1/notes/by-external-ref/{quote(ref, safe='')}",
params={"source": "inboxops"},
headers={"Authorization": f"Bearer {os.environ['TADUU_TOKEN']}"},
)/api/v1/notes/by-external-ref/{ref}scope: notes:writeDelete a note by externalRef
Removes a note once your app confirms the side-effect (the email actually sent, etc). The Taduu UI shows a 'Sent via X' badge from the moment the user clicks Send; this DELETE makes the note disappear.
curl
curl -X DELETE https://taduu.com/api/v1/notes/by-external-ref/inboxops%3Amsg%3Aabc \
-G --data-urlencode "source=inboxops" \
-H "Authorization: Bearer $TADUU_TOKEN"Node (fetch)
await fetch(
`https://taduu.com/api/v1/notes/by-external-ref/${encodeURIComponent(ref)}?source=inboxops`,
{ method: "DELETE", headers: { Authorization: `Bearer ${process.env.TADUU_TOKEN}` } }
);Python (requests)
import os, requests
from urllib.parse import quote
requests.delete(
f"https://taduu.com/api/v1/notes/by-external-ref/{quote(ref, safe='')}",
params={"source": "inboxops"},
headers={"Authorization": f"Bearer {os.environ['TADUU_TOKEN']}"},
)Locations
Locations are personal reference data — places you travel to or film at (studio, harbour, kid's school, a client's office). Each one carries an optional address pluslat/lng so the calendar and day-planner views can compute driving time between consecutive items in a day — including travel from wherever you are to your studio. Even the studio is a location.
When you send address without lat/lng, the server geocodes via Google Places and fills coordinates plus the Place ID automatically.
Fields
id— Mongo id (string)name— required, ≤200 charsaddress— full street address (optional, ≤500 chars)lat,lng— coordinates. Auto-filled from address when not supplied.placeId— Google Place ID. Set when address was resolved.vibe— free-text feel/look (e.g. “dramatic, premium, golden-hour”), ≤500 charstags— string array, lowercased + deduped server-side, max 30kind— free-string discriminator for what KIND of place this is. Suggested values:branch,website,studio,home,office. Integrations can extend with their own taxonomy. ≤40 chars.url— optional URL for digital “places” (a bank website, a Zoom room). ≤500 chars.notes— free-text, ≤5000 charscreatedAt,updatedAt— ISO timestamps
/api/v1/locationsscope: locations:readList locations
Returns every Location owned by the token holder, newest first.
curl
curl https://taduu.com/api/v1/locations \
-H "Authorization: Bearer $TADUU_TOKEN"Node (fetch)
const res = await fetch(
"https://taduu.com/api/v1/locations",
{ headers: { Authorization: `Bearer ${process.env.TADUU_TOKEN}` } },
);
const { data } = await res.json();Python (requests)
import os, requests
res = requests.get(
"https://taduu.com/api/v1/locations",
headers={"Authorization": f"Bearer {os.environ['TADUU_TOKEN']}"}
)
print(res.json()["data"])/api/v1/locationsscope: locations:writeCreate a location (with geocode)
Creates a Location. Only `name` is required. Passing `address` without lat/lng triggers a server-side geocode via Google Places — the response will include the resolved lat/lng/placeId.
lat and lng are supplied explicitly, the server trusts them and skips the geocode round-trip.curl
curl -X POST https://taduu.com/api/v1/locations \
-H "Authorization: Bearer $TADUU_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "Studio — black backdrop",
"address": "1234 Industrial Way, Port Alberni, BC",
"vibe": "dramatic, premium, low-key lighting",
"tags": ["indoor", "studio", "black-backdrop"]
}'Node (fetch)
const res = await fetch("https://taduu.com/api/v1/locations", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.TADUU_TOKEN}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
name: "Studio — black backdrop",
address: "1234 Industrial Way, Port Alberni, BC",
vibe: "dramatic, premium, low-key lighting",
tags: ["indoor", "studio", "black-backdrop"],
}),
});
const location = await res.json();
// location.lat, location.lng, location.placeId are auto-populatedPython (requests)
import os, requests
res = requests.post(
"https://taduu.com/api/v1/locations",
headers={
"Authorization": f"Bearer {os.environ['TADUU_TOKEN']}",
"Content-Type": "application/json",
},
json={
"name": "Studio — black backdrop",
"address": "1234 Industrial Way, Port Alberni, BC",
"vibe": "dramatic, premium, low-key lighting",
"tags": ["indoor", "studio", "black-backdrop"],
},
)
print(res.json()) # lat/lng/placeId filled in/api/v1/locations/{id}scope: locations:readFetch a location
Fetch a single Location by id. 404 when it doesn't belong to the token holder (no info leak).
curl
curl https://taduu.com/api/v1/locations/65f3a7... \
-H "Authorization: Bearer $TADUU_TOKEN"Node (fetch)
const res = await fetch(
`https://taduu.com/api/v1/locations/${id}`,
{ headers: { Authorization: `Bearer ${process.env.TADUU_TOKEN}` } },
);Python (requests)
import os, requests
requests.get(
f"https://taduu.com/api/v1/locations/{loc_id}",
headers={"Authorization": f"Bearer {os.environ['TADUU_TOKEN']}"}
)/api/v1/locations/{id}scope: locations:writeUpdate a location
Partial update. Only fields you send change. Sending `address` without lat/lng re-geocodes; explicit lat/lng skips the round-trip. Pass null on nullable fields to clear them.
curl
curl -X PATCH https://taduu.com/api/v1/locations/65f3a7... \
-H "Authorization: Bearer $TADUU_TOKEN" \
-H "Content-Type: application/json" \
-d '{"vibe":"warm + lived-in, hardwood floors"}'Node (fetch)
await fetch(`https://taduu.com/api/v1/locations/${id}`, {
method: "PATCH",
headers: {
Authorization: `Bearer ${process.env.TADUU_TOKEN}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ vibe: "warm + lived-in, hardwood floors" }),
});Python (requests)
import os, requests
requests.patch(
f"https://taduu.com/api/v1/locations/{loc_id}",
headers={
"Authorization": f"Bearer {os.environ['TADUU_TOKEN']}",
"Content-Type": "application/json",
},
json={"vibe": "warm + lived-in, hardwood floors"},
)/api/v1/locations/{id}scope: locations:writeDelete a location
Hard delete. Tasks that reference this location keep their locationId pointer (no cascade) — re-create the location with a fresh id and re-link if needed.
curl
curl -X DELETE https://taduu.com/api/v1/locations/65f3a7... \
-H "Authorization: Bearer $TADUU_TOKEN"Node (fetch)
await fetch(`https://taduu.com/api/v1/locations/${id}`, {
method: "DELETE",
headers: { Authorization: `Bearer ${process.env.TADUU_TOKEN}` },
});Python (requests)
import os, requests
requests.delete(
f"https://taduu.com/api/v1/locations/{loc_id}",
headers={"Authorization": f"Bearer {os.environ['TADUU_TOKEN']}"}
)Linking tasks to a Location
Pass locationId on POST /api/v1/tasks or PATCH /api/v1/tasks/{id} to pin an action item to a Location. The task response also returns locationId on every read. On PATCH, pass null to clear.
Time entries
Discrete time spans logged against a user, optionally tied to a specific task. Separate from the Crew “hours this week” rollup so integrations can push granular entries (start/stop timer, payroll shift, manual minutes) without touching human-edited totals. Hours pulled by external payroll tools come from this collection.
Fields
minutes— required, the source of truth. 1–1440.userId— the person the time is logged FOR. Defaults to the token holder. Pass another id to log on someone else's behalf (e.g. a manager recording crew hours).userEmail,loggedByUserEmail— resolved at response time from the user documents (read-only; not accepted in POST/PATCH). Integrations like Finances match onuserEmailprimary, fall back touserId(Mongo id, never changes) for defense in depth.taskId— optional. Pin the entry to an action item.startedAt,endedAt— optional ISO timestamps for timer-style recordings. Not auto-recomputed intominutes.workDate— YYYY-MM-DD bucket derived fromstartedAt(or createdAt when no startedAt). Used by the cheapfromWorkDate/toWorkDatefilters.source— free-string recording system name, e.g.limitless-finances. Lets you filter “show me only entries Finances pushed.”externalRef— correlation id from the source system, e.g.limitless-finances:shift:abc123.note— free text, ≤2000 chars.
/api/v1/timescope: time:readList time entries
Lists time entries in the workspace, sorted newest-first. Use fromWorkDate / toWorkDate (YYYY-MM-DD) for cheap weekly rollups, or from / to (ISO) for tighter timestamp bounds.
curl
curl "https://taduu.com/api/v1/time?userId=$USER_ID&fromWorkDate=2026-06-01&toWorkDate=2026-06-07" \
-H "Authorization: Bearer $TADUU_TOKEN"Node (fetch)
const params = new URLSearchParams({
userId,
fromWorkDate: "2026-06-01",
toWorkDate: "2026-06-07",
});
const res = await fetch(`https://taduu.com/api/v1/time?${params}`, {
headers: { Authorization: `Bearer ${process.env.TADUU_TOKEN}` },
});
const { data } = await res.json();
const totalMinutes = data.reduce((sum, e) => sum + e.minutes, 0);Python (requests)
import os, requests
res = requests.get(
"https://taduu.com/api/v1/time",
params={
"userId": user_id,
"fromWorkDate": "2026-06-01",
"toWorkDate": "2026-06-07",
},
headers={"Authorization": f"Bearer {os.environ['TADUU_TOKEN']}"},
)
entries = res.json()["data"]
total_minutes = sum(e["minutes"] for e in entries)/api/v1/timescope: time:writeLog a time entry
Logs a new time entry. Defaults the logged-FOR userId to the token holder. Pass another userId to log time on someone else's behalf. taskId is optional. minutes is required (1–1440).
curl
curl -X POST https://taduu.com/api/v1/time \
-H "Authorization: Bearer $TADUU_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"minutes": 90,
"taskId": "65f3a7...",
"note": "Client call + writeup",
"source": "limitless-finances",
"externalRef": "limitless-finances:shift:abc123"
}'Node (fetch)
await fetch("https://taduu.com/api/v1/time", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.TADUU_TOKEN}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
minutes: 90,
taskId,
note: "Client call + writeup",
source: "limitless-finances",
externalRef: "limitless-finances:shift:abc123",
}),
});Python (requests)
import os, requests
requests.post(
"https://taduu.com/api/v1/time",
headers={
"Authorization": f"Bearer {os.environ['TADUU_TOKEN']}",
"Content-Type": "application/json",
},
json={
"minutes": 90,
"taskId": task_id,
"note": "Client call + writeup",
"source": "limitless-finances",
"externalRef": "limitless-finances:shift:abc123",
},
)/api/v1/time/{id}scope: time:writeUpdate a time entry
Partial update. Pass only changed fields. Pass null on nullable fields to clear.
curl
curl -X PATCH https://taduu.com/api/v1/time/65f3a7... \
-H "Authorization: Bearer $TADUU_TOKEN" \
-H "Content-Type: application/json" \
-d '{"minutes": 105}'Node (fetch)
await fetch(`https://taduu.com/api/v1/time/${id}`, {
method: "PATCH",
headers: {
Authorization: `Bearer ${process.env.TADUU_TOKEN}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ minutes: 105 }),
});Python (requests)
import os, requests
requests.patch(
f"https://taduu.com/api/v1/time/{entry_id}",
headers={
"Authorization": f"Bearer {os.environ['TADUU_TOKEN']}",
"Content-Type": "application/json",
},
json={"minutes": 105},
)/api/v1/time/{id}scope: time:writeDelete a time entry
Hard delete. Time entries aren't soft-deleted — integrations typically re-push authoritative state on their cadence, so keeping deleted rows would confuse the next sync.
curl
curl -X DELETE https://taduu.com/api/v1/time/65f3a7... \
-H "Authorization: Bearer $TADUU_TOKEN"Node (fetch)
await fetch(`https://taduu.com/api/v1/time/${id}`, {
method: "DELETE",
headers: { Authorization: `Bearer ${process.env.TADUU_TOKEN}` },
});Python (requests)
import os, requests
requests.delete(
f"https://taduu.com/api/v1/time/{entry_id}",
headers={"Authorization": f"Bearer {os.environ['TADUU_TOKEN']}"},
)Webhooks
Subscribe to events in a workspace and get JSON POSTed to your endpoint when they fire. Deliveries are signed with the subscription's shared secret using HMAC-SHA256 in the X-Taduu-Signature header (format: sha256=<hex>). Failed deliveries retry with exponential backoff (up to 6 attempts over ~24h) before being marked abandoned.
Event types
task.created— fires when a new task is created via any channel (web, mobile, API, MCP, inbox triage).task.updated— any PATCH to an existing task (name, due date, priority, etc.). Does NOT fire for status-only changes — those fire astask.completedortask.reopened.task.completed— status moved to Complete.task.reopened— status moved from Complete back to anything else.task.deleted— status moved to Deleted.time.created,time.updated,time.deleted— lifecycle events for time entries.*— wildcard. Subscribe to all events. Convenient for development; prefer explicit lists in production.
Wire format
Each subscription has a format field that controls the on-wire shape. Default is taduu-v1 (enveloped); pick a different preset on create or PATCH only when the consumer requires it.
taduu-v1 (default) — enveloped JSON, signature header X-Taduu-Signature: sha256=<hex>:
{
"id": "evt_65f3a7...", // unique delivery id
"event": "task.completed",
"workspaceId": "65f3a7...",
"subscriptionId": "65f3a7...",
"occurredAt": "2026-06-05T19:42:00Z",
"deliveryAttempt": 1,
"data": { ...full Task or TimeEntry... }
}limitless-finances — flat payload matching the Limitless Finances contract, signature header X-Taduu-Signature: <hex> (bare, no sha256= prefix):
// task.completed / task.reopened / task.deleted
{
"type": "task.completed",
"externalId": "665f1a:uncategorized",
"taskId": "65f3a7..."
}
// time.created / time.updated / time.deleted
{
"externalId": "65f3a7...",
"userEmail": "jane@example.com",
"minutes": 90,
"date": "2026-06-04T00:00:00.000Z",
"note": "Sorted Q2 receipts",
"deleted": false
}Notes for limitless-finances: task events without externalRef are dropped at enqueue time (Finances can't match without an externalId). date is midnight UTC of the entry's workDate. deleted is true only for time.deleted events. Pick this format only for the Finances integration; everything else should use taduu-v1.
Verifying the signature
import crypto from "node:crypto";
export function verifyTaduuSignature(
rawBody: string,
signatureHeader: string,
secret: string,
): boolean {
const expected =
"sha256=" +
crypto.createHmac("sha256", secret).update(rawBody).digest("hex");
// Timing-safe comparison.
const a = Buffer.from(signatureHeader);
const b = Buffer.from(expected);
return a.length === b.length && crypto.timingSafeEqual(a, b);
}Compute over the RAW request body, before any JSON parsing. If your framework auto-parses, capture the raw bytes first (Next.js: await req.text()).
/api/v1/webhooksscope: webhooks:readList webhook subscriptions
Lists registered subscriptions in the resolved workspace. The shared secret is NEVER included after create — store it from the create response.
curl
curl https://taduu.com/api/v1/webhooks \
-H "Authorization: Bearer $TADUU_TOKEN"Node (fetch)
const res = await fetch("https://taduu.com/api/v1/webhooks", {
headers: { Authorization: `Bearer ${process.env.TADUU_TOKEN}` },
});
const { data } = await res.json();Python (requests)
import os, requests
res = requests.get(
"https://taduu.com/api/v1/webhooks",
headers={"Authorization": f"Bearer {os.environ['TADUU_TOKEN']}"},
)/api/v1/webhooksscope: webhooks:writeCreate a webhook subscription
Creates a subscription. The response includes a generated `secret` ONCE — store it immediately; later reads do NOT include the secret.
curl
curl -X POST https://taduu.com/api/v1/webhooks \
-H "Authorization: Bearer $TADUU_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"url": "https://finances.limitlessautomation.ca/webhooks/taduu",
"events": ["task.completed", "task.reopened", "time.created"],
"description": "Finances payroll + completion sync"
}'Node (fetch)
const res = await fetch("https://taduu.com/api/v1/webhooks", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.TADUU_TOKEN}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
url: "https://finances.limitlessautomation.ca/webhooks/taduu",
events: ["task.completed", "task.reopened", "time.created"],
description: "Finances payroll + completion sync",
}),
});
const { id, secret } = await res.json();
// Store `secret` in your env / secret manager — you won't see it again.Python (requests)
import os, requests
res = requests.post(
"https://taduu.com/api/v1/webhooks",
headers={
"Authorization": f"Bearer {os.environ['TADUU_TOKEN']}",
"Content-Type": "application/json",
},
json={
"url": "https://finances.limitlessautomation.ca/webhooks/taduu",
"events": ["task.completed", "task.reopened", "time.created"],
"description": "Finances payroll + completion sync",
},
)
secret = res.json()["secret"] # Store now — subsequent reads omit this./api/v1/webhooks/{id}scope: webhooks:writeUpdate a subscription (and optionally rotate secret)
Partial update. Pass `rotateSecret: true` to generate a new shared secret — the response will include it ONCE.
curl
curl -X PATCH https://taduu.com/api/v1/webhooks/65f3a7... \
-H "Authorization: Bearer $TADUU_TOKEN" \
-H "Content-Type: application/json" \
-d '{"rotateSecret": true}'Node (fetch)
const res = await fetch(`https://taduu.com/api/v1/webhooks/${id}`, {
method: "PATCH",
headers: {
Authorization: `Bearer ${process.env.TADUU_TOKEN}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ rotateSecret: true }),
});
const { secret } = await res.json();Python (requests)
import os, requests
res = requests.patch(
f"https://taduu.com/api/v1/webhooks/{sub_id}",
headers={"Authorization": f"Bearer {os.environ['TADUU_TOKEN']}"},
json={"rotateSecret": True},
)/api/v1/webhooks/{id}scope: webhooks:writeDelete a subscription
Hard delete. Pending deliveries for this subscription are dropped.
curl
curl -X DELETE https://taduu.com/api/v1/webhooks/65f3a7... \
-H "Authorization: Bearer $TADUU_TOKEN"Node (fetch)
await fetch(`https://taduu.com/api/v1/webhooks/${id}`, {
method: "DELETE",
headers: { Authorization: `Bearer ${process.env.TADUU_TOKEN}` },
});Python (requests)
import os, requests
requests.delete(
f"https://taduu.com/api/v1/webhooks/{sub_id}",
headers={"Authorization": f"Bearer {os.environ['TADUU_TOKEN']}"},
)MCP server (Claude Desktop, Web, Mobile)
The MCP server lives at https://taduu.com/mcp and speaks Model Context Protocol over HTTP. Same Bearer auth as REST. Once connected, Claude calls Taduu tools natively (with the usual permission prompts) — add tasks, list what's due, capture brain-dumps.
Claude Desktop
Open Claude Desktop → Settings → Developer → Edit Config. Add this block to claude_desktop_config.json and restart:
claude_desktop_config.json
{
"mcpServers": {
"taduu": {
"command": "npx",
"args": [
"-y",
"mcp-remote",
"https://taduu.com/mcp"
]
}
}
}claude.ai (web) and Claude Mobile (iOS / Android)
On claude.ai/settings/connectors click Add custom connector. URL is https://taduu.com/mcp, auth is Bearer token, paste your Taduu PAT. Connectors added on the web sync automatically to Claude Mobile — nothing extra to install on the phone.
Available tools
taduu_me, taduu_quick_capture, taduu_create_task, taduu_list_tasks, taduu_update_task, taduu_complete_task, taduu_delete_task, taduu_list_workspaces, taduu_list_lists, taduu_list_users, taduu_create_list, taduu_create_note, taduu_list_locations, taduu_create_location, taduu_update_location, taduu_delete_location, taduu_log_time, taduu_list_time, taduu_delete_time.
Common pitfalls
- "My token stopped working overnight."
- Pro trial expired. The API is Pro-gated, so tokens stop authenticating as soon as the token holder drops off Pro. Check status at Settings → Billing.
- "Claude says it can't find my tasks."
- If your token covers all workspaces, Claude has to disambiguate. Either tell it the workspace by name in the prompt, or create a workspace-scoped token for the integration so the ambiguity can't happen.
- "I want to see what an integration actually did."
- Open Settings → Integrations and click Activity on the token row. Last 50 calls with endpoint + status + relative time.
- "I rotated my token but Claude Desktop still works with the old one."
- Fully quit Claude Desktop and reopen. It holds the config in memory until restart. On macOS that's Cmd+Q, not just closing the window.
- "Claude Desktop says ‘Some MCP servers could not be loaded’ and skips taduu."
- Your Claude Desktop version's config validator only accepts stdio-style MCP servers (command + args), not direct
type: "http"blocks. Use themcp-remoteshim format shown above — works on every Claude Desktop version. Requires Node.js locally sonpxcan fetch and run the shim. - "I'm getting 404 on a list/task I know exists."
- Taduu returns the same 404 for "doesn't exist" and "you can't see it" — no info leak between those two cases. Check the token's scopes + workspace, and verify the user who owns the token has at least Viewer access to the list.
- "Hitting 429 sooner than I expected."
- The 300 req/min limit is per token. If your integration is bursty, batch reads where you can, and respect
x-ratelimit-reseton the 429 response.