Taduu
OpenAPI specGet started

Contents

OverviewQuickstartAuthenticationScopesErrorsRate limitsIdentityWorkspacesListsTasksInboxNotesLocationsTime entriesWebhooksMCP server (Claude)Common pitfalls

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

  1. Sign in at taduu.com — Pro plan or active trial required.
  2. Open Settings → Integrations → New token
  3. Pick scopes — for most integrations: tasks:read, tasks:write, inbox:write, lists:read, users:read.
  4. 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_yourTokenHere

Tokens 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.

ScopeGrants
me:readAlways granted. Verify token identity.
workspaces:readList workspaces the token can reach.
users:readList workspace members (for assignment).
lists:readRead lists.
lists:writeCreate / rename lists.
tasks:readRead action items.
tasks:writeCreate / update / delete action items.
notes:readRead accessible notes.
notes:writeCreate / update notes.
inbox:readRead Unsorted inbox items (for triage).
inbox:writeDrop items into Unsorted; promote / delete them.
locations:readRead the user's named Locations.
locations:writeCreate / update / delete Locations.
time:readRead time entries in the workspace.
time:writeLog / update / delete time entries.
webhooks:readList webhook subscriptions.
webhooks:writeCreate / 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

GET/api/v1/mescope: me:read

Get 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

GET/api/v1/workspacesscope: workspaces:read

List 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"])
GET/api/v1/usersscope: users:read

List 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

GET/api/v1/listsscope: lists:read

List 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"])
POST/api/v1/listsscope: lists:write

Create 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).

GET/api/v1/tasksscope: tasks:read

Search 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"])
POST/api/v1/tasksscope: tasks:write

Create 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.

Pass 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",
    },
)
PATCH/api/v1/tasks/{id}scope: tasks:write

Update 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"},
)
DELETE/api/v1/tasks/{id}scope: tasks:write

Delete 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

POST/api/v1/inboxscope: inbox:write

Quick-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

GET/api/v1/notesscope: notes:read

List 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"])
POST/api/v1/notesscope: notes:write

Create 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.

GET/api/v1/notes/by-external-ref/{ref}scope: notes:read

Get 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']}"},
)
DELETE/api/v1/notes/by-external-ref/{ref}scope: notes:write

Delete 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 chars
  • address — 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 chars
  • tags — string array, lowercased + deduped server-side, max 30
  • kind — 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 chars
  • createdAt, updatedAt — ISO timestamps
GET/api/v1/locationsscope: locations:read

List 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"])
POST/api/v1/locationsscope: locations:write

Create 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.

If 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-populated

Python (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
GET/api/v1/locations/{id}scope: locations:read

Fetch 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']}"}
)
PATCH/api/v1/locations/{id}scope: locations:write

Update 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"},
)
DELETE/api/v1/locations/{id}scope: locations:write

Delete 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 on userEmail primary, fall back to userId (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 into minutes.
  • workDate — YYYY-MM-DD bucket derived from startedAt (or createdAt when no startedAt). Used by the cheap fromWorkDate/toWorkDate filters.
  • 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.
GET/api/v1/timescope: time:read

List 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)
POST/api/v1/timescope: time:write

Log 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",
    },
)
PATCH/api/v1/time/{id}scope: time:write

Update 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},
)
DELETE/api/v1/time/{id}scope: time:write

Delete 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 as task.completed or task.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()).

GET/api/v1/webhooksscope: webhooks:read

List 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']}"},
)
POST/api/v1/webhooksscope: webhooks:write

Create 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.
PATCH/api/v1/webhooks/{id}scope: webhooks:write

Update 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},
)
DELETE/api/v1/webhooks/{id}scope: webhooks:write

Delete 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 the mcp-remote shim format shown above — works on every Claude Desktop version. Requires Node.js locally so npx can 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-reset on the 429 response.

Ready to build?

Create your first token in Settings → Integrations and you'll be calling the API in 30 seconds.

Start using Taduu
Made with TaduuTry it free →