Todo.work API

Everything the Todo.work app does is available over HTTP: tasks, projects, leads, clients, comments, chat, kanban boards, files and time tracking. The same API is also exposed as an MCP server, so an AI agent can use it directly without any glue code.

48 endpoints Base URL https://api.todo.work/api_integrations MCP https://api.todo.work/api_integrations/mcp Get your API key →

Before you start

Create a key under Settings → API keys in the app. A key acts with the permissions of the user it is bound to — it can never do more than that user can.

  • The samples below show YOUR_API_KEY and YOUR_API_SECRET. Replace them with your own, and keep them out of anything you commit or paste.
  • For AI agents: point any MCP-capable client at https://api.todo.work/api_integrations/mcp and authenticate with Authorization: Bearer <api_key>:<api_secret>. The tools are grouped by module and take an { action, params } pair whose action names match the REST paths below.
  • Success bodies are { "response": <payload> }. Check the HTTP status rather than the presence of a response key — the 401 body carries one too, empty.

Authentication

Every request authenticates with a key/secret pair sent as HTTP headers. A key acts with the permissions of the user it is bound to.

Required headers
  • X-API-KEY: YOUR_API_KEY
  • X-API-SECRET: YOUR_API_SECRET
  • Content-Type: application/json
Optional headers
  • User-Token: <token> — override acting user (admin / legacy keys only)
  • User-Timezone: Asia/Jerusalem — defaults to Asia/Jerusalem
  • Aliases: you may send Api-Token / Api-Secret instead of X-API-KEY / X-API-SECRET.
  • A key bound to a user (acts_as_user_id) runs strictly as that user — it can do nothing the user cannot. Admin / legacy keys may override the acting user with a User-Token header.
  • A disabled key (status = 0) is rejected with 401.
  • Response shapes: success is { "response": <payload> }. A few endpoints (/, /get_users, /get_project_senior_members, /add_lead) wrap twice, so their data sits at response.response.
  • Do not treat the presence of a "response" key as success — the 401 body contains it too, empty. Check the HTTP status first: 401 unauthorized, 403 forbidden, 400 endpoint error (its body is double-wrapped), 500 internal error, 404 an unknown path (which answers HTML, not JSON).

GET / Health check

Confirms the key authenticates and a platform resolved. Returns "API is working".

Parameters

No parameters — send the authentication headers only.

Permission

Any valid key

Request
// Health check — verifies your credentials
const response = await fetch('https://api.todo.work/api_integrations/', {
  method: 'GET',
  headers: {
    'Content-Type': 'application/json',
    'X-API-KEY': 'YOUR_API_KEY',
    'X-API-SECRET': 'YOUR_API_SECRET'
  }
});

const data = await response.json();
console.log(data.response);
Response
{
  "response": {
    "status": 200,
    "message": "API is working",
    "response": []
  }
}

Projects

List, read and create/update projects. Data is scoped to the key’s platform.

GET /projects/results List projects

Paginated, filtered list of projects with calculated metrics (logged hours, open tickets, departments, last update).

Parameters
NameTypeDescription
id
optional · query
int A single project id. Overrides is_archive.
client_id
optional · query
int Filter by client.
is_archive
optional · query
bool true = archived/deleted, false = active (default). Accepts true/false, 1/0 and the strings "true"/"false" over a query string.
Permission

Any key

Notes
  • The archived flag on each row is is_deleted, not is_archive — there is no is_archive field in the response.
  • hours is a formatted "HH:MM" string, not a number.
  • Rows also carry the project permission columns (perms_*), notes, folder_id, and the joined client columns cln_name / cln_phone / cln_email. Only the commonly used fields are shown below.
Request
// List active projects for a client
const response = await fetch('https://api.todo.work/api_integrations/projects/results?client_id=5&is_archive=false', {
  method: 'GET',
  headers: {
    'Content-Type': 'application/json',
    'X-API-KEY': 'YOUR_API_KEY',
    'X-API-SECRET': 'YOUR_API_SECRET'
  }
});

const data = await response.json();
console.log(data.response);
Response
{
  "response": [
    {
      "id": 12,
      "name": "Website Redesign",
      "type": "hourly",
      "client_id": 5,
      "color": "#59ce8f",
      "hours": "42:30",
      "open_tickets": 7,
      "is_deleted": 0,
      "last_update": "2026-06-26 14:11:00",
      "departments": [1, 3],
      "cln_name": "Acme Inc.",
      "notes": "",
      "folder_id": null
    }
  ]
}

GET /projects/get Get a project

A single project by id, enriched with its documents and full department objects.

Parameters
NameTypeDescription
id
required · query
int Project id (must be > 0).
Permission

Any key

Request
// Get project 123
const response = await fetch('https://api.todo.work/api_integrations/projects/get?id=123', {
  method: 'GET',
  headers: {
    'Content-Type': 'application/json',
    'X-API-KEY': 'YOUR_API_KEY',
    'X-API-SECRET': 'YOUR_API_SECRET'
  }
});

const data = await response.json();
console.log(data.response);
Response
{
  "response": {
    "id": 123,
    "name": "Website Redesign",
    "type": "hourly",
    "departments": [ { "id": 1, "name": "Design" } ],
    "docs": [],
    "docs_token": "…"
  }
}

POST /projects/save Create or update a project

Omit id to create, include it to update. Can auto-create a client and (on create) opens a first ticket.

Parameters
NameTypeDescription
id
optional · body
int Include to update; omit to create.
name
required · body
string Project name.
type
required · body
enum "hourly" (time-tracked) or "fixed" (fixed price).
client_id
optional · body
int | "add" Existing client id, or "add" to create a new client (with client_name / client_email).
price_hour
optional · body
decimal Hourly rate (type=hourly only).
price
optional · body
decimal Fixed price (type=fixed only).
target_monthly_hours
optional · body
decimal Monthly hours target (type=hourly only).
departments
optional · body
array Array of { id } objects or ids.
date_deadline
optional · body
string Any parseable date; stored as Y-m-d.
open_first_ticket
optional · body
bool Open a starter ticket on create (default true).
Permission

Any key (needs tasks.manage)

Notes
  • The id comes back as a number in both directions — create and update. (It used to be a string on create.)
Request
// Create a new hourly project
const response = await fetch('https://api.todo.work/api_integrations/projects/save', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'X-API-KEY': 'YOUR_API_KEY',
    'X-API-SECRET': 'YOUR_API_SECRET'
  },
  body: JSON.stringify({
    name: 'Website Redesign',
    type: 'hourly',
    client_id: 5,
    price_hour: '50.00',
    target_monthly_hours: '160',
    departments: [{ id: 1 }]
  })
});

const data = await response.json();
console.log(data.response);
Response
// On success the new (or updated) project id:
{ "response": 87 }

Tickets

The core task object. Create, read, update, run timers and manage handlers. Results are limited to projects the acting key can access.

  • scheduled_date is read/written in the caller’s timezone (User-Timezone header) and stored as UTC.
  • Update changes ONE field per call using the field / value pattern.
  • Dates: prefer ISO — "2026-10-01 09:00" or "2026-10-02T09:00:00". Slash format is read as DD/MM/YYYY, so 03/10/2026 is 3 October. A value that cannot be parsed is rejected; it never clears the stored date.
  • handlers_uid comes back as a pipe-delimited string ("|7|12|"), not an array. Send handlers (array of { id }) to change it.

POST /tickets/results List tickets

Paginated ticket list with rich filtering by project, handler, status, custom fields, date range and full-text search.

Parameters
NameTypeDescription
filter_by
optional · body
enum pending | finished | my | all | search.
project_id
optional · body
int | array | "all" One id, a list, or "all" accessible projects.
handler
optional · body
int | array | "none" Filter by handler; "none" = unassigned.
q
optional · body
string Search text (min 3 chars, with filter_by=search).
page
optional · body
int 1-indexed page (default 1).
limit
optional · body
int Per page (default 100).
order
optional · body
string date_updated (default) | date_created | scheduled_date | handler.
order_dir
optional · body
enum ASC | DESC (default DESC).
Permission

Any key

Notes
  • The response always carries a projects array alongside tickets.
  • project_totals, total_results and next_token are returned ONLY when project_id is sent. Without it the response is just { tickets, projects } — so paginate by sending project_id.
Request
// Pending tickets in projects 1 and 2 handled by user 5
const response = await fetch('https://api.todo.work/api_integrations/tickets/results', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'X-API-KEY': 'YOUR_API_KEY',
    'X-API-SECRET': 'YOUR_API_SECRET'
  },
  body: JSON.stringify({
    filter_by: 'pending',
    project_id: [1, 2],
    handler: 5,
    page: 1,
    limit: 50,
    order: 'date_updated',
    order_dir: 'DESC'
  })
});

const data = await response.json();
console.log(data.response);
Response
{
  "response": {
    "tickets": [
      {
        "id": 123,
        "subject": "Fix login bug",
        "project_id": 5,
        "handlers": [ { "id": 7, "display_name": "Dana" } ],
        "handlers_uid": "|7|",
        "is_completed": 0,
        "scheduled_date": "2026-07-15 14:30:00",
        "duration": 90
      }
    ],
    "projects": [ { "id": 5, "name": "Website Redesign" } ],
    "total_results": 1,
    "next_token": "…"
  }
}

POST /tickets/get Get a ticket

A single ticket with comments, subtasks, handlers, custom fields and a share url.

Parameters
NameTypeDescription
id
required · body
int Ticket id.
Permission

Any key (must have access to the ticket)

Request
// Get ticket 456
const response = await fetch('https://api.todo.work/api_integrations/tickets/get', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'X-API-KEY': 'YOUR_API_KEY',
    'X-API-SECRET': 'YOUR_API_SECRET'
  },
  body: JSON.stringify({ id: 456 })
});

const data = await response.json();
console.log(data.response);
Response
{
  "response": {
    "id": 456,
    "subject": "Fix login bug",
    "project_id": 5,
    "handlers": [ { "id": 7, "display_name": "Dana" } ],
    "is_completed": 0,
    "custom_filters": { "42": "101" },
    "comments": [],
    "subtasks_list": [],
    "share_url": "https://…"
  }
}

POST /tickets/add Create a ticket

Creates a task (or lead). Handlers are auto-enrolled into the project.

Parameters
NameTypeDescription
subject
required · body
string Ticket title.
project_id
optional · body
int Target project (auto-selects first if omitted).
scheduled_date
optional · body
string Datetime in your timezone.
handlers_uid
optional · body
array[int] Handler user ids.
duration_limit
optional · body
int Time budget in minutes.
list_id
optional · body
int Kanban list to place it on.
type
optional · body
enum task (default) | lead.
Permission

Any key

Notes
  • The response also carries ticket_id alongside ticket.id — ticket_id is a string, ticket.id is a number. Prefer ticket.id.
Request
// Create a ticket in project 123
const response = await fetch('https://api.todo.work/api_integrations/tickets/add', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'X-API-KEY': 'YOUR_API_KEY',
    'X-API-SECRET': 'YOUR_API_SECRET'
  },
  body: JSON.stringify({
    subject: 'Fix login bug',
    project_id: 123,
    handlers_uid: [7, 12],
    scheduled_date: '2026-07-15 14:30',
    duration_limit: 240
  })
});

const data = await response.json();
console.log(data.response);
Response
{
  "response": {
    "status": true,
    "ticket_id": "988",
    "ticket": { "id": 988, "subject": "Fix login bug", "project_id": 123 }
  }
}

POST /tickets/update Update a ticket field

Updates one field per call. Side effects fire automatically (timers stop on completion, handlers enroll into the project, reminders schedule).

Parameters
NameTypeDescription
id
required · body
int Ticket id.
field
required · body
string subject | scheduled_date | handlers | is_completed | project_id | duration_limit | custom_field.
value
required · body
mixed Format depends on field (see notes). handlers = array of { id }. Must be present — omitting the key is an error, not an empty value.
custom_field_id
optional · body
int Required when field = "custom_field" (see Custom Fields).
Permission

Any key (edit access to the ticket’s project)

Notes
  • is_completed accepts true/false, 1/0 and the strings "1"/"0"/"true"/"false". Anything else is rejected with a message rather than being coerced.
  • A bare { "response": false } means one thing only: nothing changed, because the value sent already equals the stored one. Every rejection answers { "status": false, "error": "…" } instead, so an idempotent re-run is distinguishable from a refusal.
  • Sending an id that does not exist answers { "status": false, "error": "Ticket not found" }.
Request
// Set the handlers of ticket 456
const response = await fetch('https://api.todo.work/api_integrations/tickets/update', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'X-API-KEY': 'YOUR_API_KEY',
    'X-API-SECRET': 'YOUR_API_SECRET'
  },
  body: JSON.stringify({
    id: 456,
    field: 'handlers',
    value: [{ id: 7 }, { id: 12 }]
  })
});

const data = await response.json();
console.log(data.response);
Response
{
  "response": {
    "status": "success",
    "ticket": { "id": 456, "handlers": [ { "id": 7 }, { "id": 12 } ] }
  }
}

POST /tickets/create_subtask Create a subtask

Creates a subtask under a parent ticket; it inherits the parent’s project.

Parameters
NameTypeDescription
parent_id
required · body
int Parent ticket id.
subject
required · body
string Subtask title.
handlers_uid
optional · body
array[int] Handler user ids.
Permission

Any key (access to the parent ticket)

Request
// Add a subtask to ticket 456
const response = await fetch('https://api.todo.work/api_integrations/tickets/create_subtask', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'X-API-KEY': 'YOUR_API_KEY',
    'X-API-SECRET': 'YOUR_API_SECRET'
  },
  body: JSON.stringify({
    parent_id: 456,
    subject: 'Test the login page',
    handlers_uid: [8]
  })
});

const data = await response.json();
console.log(data.response);
Response
{
  "response": { "status": true, "ticket": { "id": 991, "parent_id": 456 } }
}

POST /tickets/add_handler Add a handler

Adds a single handler without replacing the others (idempotent).

Parameters
NameTypeDescription
tid
required · body
int Ticket id.
handler_id
required · body
int User id to add.
Permission

Any key (edit access)

Request
// Add handler 7 to ticket 456
const response = await fetch('https://api.todo.work/api_integrations/tickets/add_handler', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'X-API-KEY': 'YOUR_API_KEY',
    'X-API-SECRET': 'YOUR_API_SECRET'
  },
  body: JSON.stringify({ tid: 456, handler_id: 7 })
});

const data = await response.json();
console.log(data.response);
Response
{
  "response": {
    "status": "success",
    "ticket": { "id": 456, "handlers_uid": "|7|", "handlers": [ { "id": 7, "display_name": "Dana" } ] }
  }
}

POST /tickets/remove_handler Remove a handler

Removes a single handler (idempotent).

Parameters
NameTypeDescription
tid
required · body
int Ticket id.
handler_id
required · body
int User id to remove.
Permission

Any key (edit access)

Request
// Remove handler 7 from ticket 456
const response = await fetch('https://api.todo.work/api_integrations/tickets/remove_handler', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'X-API-KEY': 'YOUR_API_KEY',
    'X-API-SECRET': 'YOUR_API_SECRET'
  },
  body: JSON.stringify({ tid: 456, handler_id: 7 })
});

const data = await response.json();
console.log(data.response);
Response
{
  "response": {
    "status": "success",
    "ticket": { "id": 456, "handlers_uid": "||", "handlers": [] }
  }
}

POST /tickets/start_handling Start a timer

Starts a duration timer on a ticket. If a time threshold is exceeded the call returns "confirm" — re-send with confirmed: true to force it.

Parameters
NameTypeDescription
tid
required · body
int Ticket id.
handler_uid
optional · body
int Defaults to the acting user.
confirmed
optional · body
bool true to skip the threshold confirmation.
Permission

Any key (acting user must be a team member)

Request
// Start the timer on ticket 456
const response = await fetch('https://api.todo.work/api_integrations/tickets/start_handling', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'X-API-KEY': 'YOUR_API_KEY',
    'X-API-SECRET': 'YOUR_API_SECRET'
  },
  body: JSON.stringify({ tid: 456, confirmed: false })
});

const data = await response.json();
console.log(data.response);
Response
// true on start, or the string "confirm" when a threshold needs confirmation:
{ "response": true }

POST /tickets/stop_handling Stop a timer

Stops the active timer and records the elapsed minutes.

Parameters
NameTypeDescription
tid
required · body
int Ticket id.
Permission

Any key (acting user must be a team member)

Request
// Stop the timer on ticket 456
const response = await fetch('https://api.todo.work/api_integrations/tickets/stop_handling', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'X-API-KEY': 'YOUR_API_KEY',
    'X-API-SECRET': 'YOUR_API_SECRET'
  },
  body: JSON.stringify({ tid: 456 })
});

const data = await response.json();
console.log(data.response);
Response
{ "response": true }

POST /tickets/report_duration Report worked time

Records a completed block of work directly, without a start/stop clock — useful for billing or reported work.

Parameters
NameTypeDescription
tid
required · body
int Ticket id.
duration
required · body
int Minutes worked (> 0).
handler_uid
optional · body
int Defaults to the acting user.
date
optional · body
string Work datetime in your timezone (defaults to now).
Permission

Any key (acting user must be a team member)

Request
// Log 2 hours on ticket 456
const response = await fetch('https://api.todo.work/api_integrations/tickets/report_duration', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'X-API-KEY': 'YOUR_API_KEY',
    'X-API-SECRET': 'YOUR_API_SECRET'
  },
  body: JSON.stringify({
    tid: 456,
    duration: 120,
    handler_uid: 7,
    date: '2026-06-27 14:00'
  })
});

const data = await response.json();
console.log(data.response);
Response
{ "response": true }

Leads (webhook)

One request creates a lead, fills its contact fields and posts the incoming text as its first note — built to be dropped straight into a form / lead-ad / Zapier webhook field.

  • Field names are matched loosely and case-insensitively: name / full_name / first_name+last_name, phone / mobile / tel, message / note / body / description …
  • Select fields (source, status) accept the option NAME or its id. A value matching no option is reported in "warnings" instead of failing the call.
  • Anything the mapping did not consume (utm_*, landing page, extra form questions) is appended to the note, so a webhook never loses data. Send append_unmapped: false to drop it instead.
  • This endpoint also accepts its credentials in the body (api_key + api_secret) or as Authorization: Bearer key:secret — for senders that cannot set custom headers. Prefer the headers when you can.
  • If the key is bound to a non-admin user, send project_id (or handlers_uid) so that user can read the lead — otherwise the lead is created but the note is refused, and you get a warning saying so.

POST /add_lead Create a lead + note

Creates the lead (default lead status, automations fire), fills phone / email / contact name / company / source / status, then posts content as the first comment. Replaces the old tickets/add → filters/get_all → add_filter_to_ticket → comments/add_comment chain.

Parameters
NameTypeDescription
content
optional · body
string The note. Plain text is fine — line breaks are kept. Aliases: message, note, body, description…
subject
optional · body
string Board title. Defaults to the contact name / company / phone / email.
contact_name
optional · body
string Person’s name. Aliases: name, full_name, first_name + last_name.
phone
optional · body
string Aliases: mobile, tel, telephone, whatsapp…
email
optional · body
string Aliases: mail, email_address.
company
optional · body
string Aliases: company_name, organization, business.
source
optional · body
string Lead source — option name or id.
status
optional · body
string Lead status — option name or id. Defaults to the workspace default.
handlers_uid
optional · body
array[int] Assign to these users. Accepts [7], [{id:7}] or "7,12".
project_id
optional · body
int Optional — leads normally live outside projects.
scheduled_date
optional · body
string Follow-up datetime in your timezone.
fields
optional · body
object Any other custom field: { "<system_key | id | name>": value }.
append_unmapped
optional · body
bool Append unrecognised payload keys to the note. Default true.
Permission

Any key with leads.manage (admins always)

Notes
  • Requires the workspace to have lead statuses configured. If it does not, the call is refused with a message — open the Leads screen in the TODO app once to create the defaults, then retry.
Request
// Website form → lead + note, in one call
const response = await fetch('https://api.todo.work/api_integrations/add_lead', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'X-API-KEY': 'YOUR_API_KEY',
    'X-API-SECRET': 'YOUR_API_SECRET'
  },
  body: JSON.stringify({
    name: 'Israel Israeli',
    phone: '050-1234567',
    email: 'israel@example.com',
    company: 'Acme Ltd.',
    source: 'טופס אתר',
    message: 'Interested in the large package.\nBudget around 5,000.',
    utm_campaign: 'summer-2026'
  })
});

const data = await response.json();
console.log(data.response);
Response
{
  "response": {
    "status": 200,
    "message": "Lead created",
    "response": {
      "lead_id": 76924,
      "subject": "Israel Israeli",
      "comment_id": 92933,
      "fields_set": {
        "lead_contact_name": "Israel Israeli",
        "lead_phone": "050-1234567",
        "lead_email": "israel@example.com",
        "lead_company": "Acme Ltd.",
        "lead_source": "549"
      },
      "warnings": [],
      "lead": { "id": 76924, "type": "lead", "subject": "Israel Israeli" }
    }
  }
}

Custom Fields

Read your platform’s custom field definitions, then set their values on tickets through the ticket update endpoint.

  • Read current values from a ticket via /tickets/get (the custom_filters map).
  • Always look up the field id and type from /filters/get_all before writing.

GET /filters/get_all List custom field definitions

Every custom field on the platform with its id, type and (for select fields) its option values.

Parameters
NameTypeDescription
target
optional · query
enum task (default) | lead | expense. Note: expense currently returns 0 fields.
project_id
optional · query
int Restrict to fields available on this project.
Permission

Any key

Notes
  • Select fields return their options twice: values as full objects, and selectValues as { key, val } pairs. They carry the same options in two shapes.
  • On user_approval fields the selectValues key is a state string such as "idle", not an option id.
Request
// List custom field definitions
const response = await fetch('https://api.todo.work/api_integrations/filters/get_all', {
  method: 'GET',
  headers: {
    'Content-Type': 'application/json',
    'X-API-KEY': 'YOUR_API_KEY',
    'X-API-SECRET': 'YOUR_API_SECRET'
  }
});

const data = await response.json();
console.log(data.response);
Response
{
  "response": [
    {
      "id": 42,
      "name": "Status",
      "type": "select",
      "values": [
        { "id": 101, "name": "Open" },
        { "id": 102, "name": "In progress" },
        { "id": 103, "name": "Done" }
      ],
      "selectValues": [
        { "key": "101", "val": "Open" },
        { "key": "102", "val": "In progress" }
      ]
    },
    { "id": 43, "name": "Due date", "type": "date" },
    { "id": 44, "name": "Notes", "type": "free_text" }
  ]
}

POST /tickets/update Set a custom field on a ticket

Validated write: stores the value, logs it, and fires automation. The value format depends on the field type.

Parameters
NameTypeDescription
id
required · body
int Ticket id.
field
required · body
string Must be the literal "custom_field".
custom_field_id
required · body
int The field id from /filters/get_all.
value
required · body
mixed Per type (see below). null clears the value.
Permission

Any key (edit access to the ticket)

Request
// Each field type takes its own value format.
// Use field: 'custom_field' and custom_field_id to target the field.

// SELECT — value is the option id
await fetch('https://api.todo.work/api_integrations/tickets/update', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'X-API-KEY': 'YOUR_API_KEY',
    'X-API-SECRET': 'YOUR_API_SECRET'
  },
  body: JSON.stringify({ id: 456, field: 'custom_field', custom_field_id: 42, value: 101 })
});

// DATE — value is 'YYYY-MM-DD'
await fetch('https://api.todo.work/api_integrations/tickets/update', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'X-API-KEY': 'YOUR_API_KEY',
    'X-API-SECRET': 'YOUR_API_SECRET'
  },
  body: JSON.stringify({ id: 456, field: 'custom_field', custom_field_id: 43, value: '2026-03-15' })
});

// TEXT / FREE_TEXT — value is any string
await fetch('https://api.todo.work/api_integrations/tickets/update', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'X-API-KEY': 'YOUR_API_KEY',
    'X-API-SECRET': 'YOUR_API_SECRET'
  },
  body: JSON.stringify({ id: 456, field: 'custom_field', custom_field_id: 44, value: 'Any text' })
});

// USERS_LIST — value is an array of user ids
await fetch('https://api.todo.work/api_integrations/tickets/update', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'X-API-KEY': 'YOUR_API_KEY',
    'X-API-SECRET': 'YOUR_API_SECRET'
  },
  body: JSON.stringify({ id: 456, field: 'custom_field', custom_field_id: 45, value: [1, 2, 3] })
});

// USER_APPROVAL — value is 'idle' | 'pending' | 'approved' | 'rejected'
await fetch('https://api.todo.work/api_integrations/tickets/update', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'X-API-KEY': 'YOUR_API_KEY',
    'X-API-SECRET': 'YOUR_API_SECRET'
  },
  body: JSON.stringify({ id: 456, field: 'custom_field', custom_field_id: 46, value: 'approved' })
});

// SWITCH — value is '1' (on) or '0' (off)
await fetch('https://api.todo.work/api_integrations/tickets/update', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'X-API-KEY': 'YOUR_API_KEY',
    'X-API-SECRET': 'YOUR_API_SECRET'
  },
  body: JSON.stringify({ id: 456, field: 'custom_field', custom_field_id: 47, value: '1' })
});

console.log('Custom field examples — copy and adapt as needed');
Response
{
  "response": {
    "status": "success",
    "ticket": { "id": 456, "custom_filters": { "42": "101" } }
  }
}

Clients

Read the clients and projects the key can access.

GET /get_clients List clients

Flat array of the clients this key can see. Despite the endpoint name it returns clients only — there is no companion projects array and no full_name field. Use /projects/results for projects.

Parameters

No parameters — send the authentication headers only.

Permission

Any key (respects the acting user’s access)

Request
// List clients
const response = await fetch('https://api.todo.work/api_integrations/get_clients', {
  method: 'GET',
  headers: {
    'Content-Type': 'application/json',
    'X-API-KEY': 'YOUR_API_KEY',
    'X-API-SECRET': 'YOUR_API_SECRET'
  }
});

const data = await response.json();
console.log(data.response);
Response
{
  "response": [
    { "id": 5, "name": "Acme Inc.", "phone": "03-1234567", "email": "hi@acme.com" }
  ]
}

Comments

Add or edit comments on tickets and expenses, with @mentions and replies.

POST /comments/add_comment Add or edit a comment

Creates a comment (or edits one when comment_id is sent). Supports @mentions, nested replies and file attachments.

Parameters
NameTypeDescription
tid
required · body
int Ticket (or expense) id.
content
required · body
string HTML text (a safe tag subset is allowed).
object_type
optional · body
enum ticket (default) | expense.
mentions
optional · body
string Comma-separated user ids, e.g. "5,12".
reply_to
optional · body
int Parent comment id for a reply.
comment_id
optional · body
int Send to edit an existing comment.
Permission

Any key (access to the ticket/expense; edits are author or admin only)

Request
// Comment on ticket 456 and mention two users
const response = await fetch('https://api.todo.work/api_integrations/comments/add_comment', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'X-API-KEY': 'YOUR_API_KEY',
    'X-API-SECRET': 'YOUR_API_SECRET'
  },
  body: JSON.stringify({
    tid: 456,
    object_type: 'ticket',
    content: '<p>Fixed the alignment.</p>',
    mentions: '5,12',
    reply_to: 0
  })
});

const data = await response.json();
console.log(data.response);
Response
{ "response": { "success": true, "comment_id": 4567 } }

Users & Team

List platform users and find the senior members of a project.

  • A non-admin key only sees users at its own permission level and below — it can never obtain an admin’s token.
  • The returned token is scoped to this integrations API and expires after 48 hours. It is not an app session token and will not authenticate anywhere else — re-fetch it when it expires rather than storing it long-term.

GET /get_users List users

All active platform users. Each carries a freshly-minted token, scoped to this API and valid for 48 hours — use it as a User-Token header on later requests (if the key permits).

Parameters

No parameters — send the authentication headers only.

Permission

Any key (results filtered by the key’s permission level)

Request
// List platform users
const response = await fetch('https://api.todo.work/api_integrations/get_users', {
  method: 'GET',
  headers: {
    'Content-Type': 'application/json',
    'X-API-KEY': 'YOUR_API_KEY',
    'X-API-SECRET': 'YOUR_API_SECRET'
  }
});

const data = await response.json();
console.log(data.response);
Response
{
  "response": {
    "status": 200,
    "message": "Users fetched successfully",
    "response": [
      { "id": 7, "display_name": "Dana", "type": "user", "token": "…" }
    ]
  }
}

GET /get_project_senior_members Get project senior members

The highest-permission team members on a project (up to 5), for @-mentioning senior staff. Falls back to platform admins.

Parameters
NameTypeDescription
project_id
required · query
int Project id.
exclude_user_id
optional · query
int A user to leave out (e.g. yourself).
Permission

Any key

Request
// Senior members of project 123
const response = await fetch('https://api.todo.work/api_integrations/get_project_senior_members?project_id=123&exclude_user_id=7', {
  method: 'GET',
  headers: {
    'Content-Type': 'application/json',
    'X-API-KEY': 'YOUR_API_KEY',
    'X-API-SECRET': 'YOUR_API_SECRET'
  }
});

const data = await response.json();
console.log(data.response);
Response
{
  "response": {
    "status": 200,
    "message": "Senior members fetched successfully",
    "response": [
      { "id": 3, "display_name": "Denis", "permission": 1, "pic_parsed": "https://…" }
    ]
  }
}

Files & Briefs

Attach a brief (text + reference/material files) to a project, and add users to it.

  • These endpoints use multipart/form-data — do not set Content-Type, let the browser set the boundary.

POST /add_files Add a brief & files to a project

Creates a project brief: a description plus image rows for each reference and material file.

Parameters
NameTypeDescription
project_id
required · formdata
int Target project.
description
required · formdata
string Brief text.
reference_files[]
optional · formdata
file[] Reference images.
material_files[]
optional · formdata
file[] Working-material files.
Permission

Any key (scoped to the platform)

Request
// Add a brief to a project (multipart/form-data — no Content-Type header)
const formData = new FormData();
formData.append('project_id', 123);
formData.append('description', 'Brief description');
// formData.append('reference_files[]', file1);
// formData.append('material_files[]', file2);

const response = await fetch('https://api.todo.work/api_integrations/add_files', {
  method: 'POST',
  headers: {
    'X-API-KEY': 'YOUR_API_KEY',
    'X-API-SECRET': 'YOUR_API_SECRET'
  },
  body: formData
});

const data = await response.json();
console.log(data.response);
Response
{ "response": true }

POST /attach_users Attach a user to a project

Prefixes the project name with its id and (optionally) attaches a user as a viewer.

Parameters
NameTypeDescription
project_id
required · body
int Target project.
user_id
optional · body
int User to attach (omit to only rename).
rename
optional · body
bool Default true. Send false to attach the user without touching the project name.
Permission

Any key (scoped to the platform)

Notes
  • The rename writes the project name as "<id> - <name>". It is applied at most once — calling repeatedly no longer stacks the prefix.
  • Send rename: false if you only want to attach the user.
Request
// Attach user 7 to project 123
const response = await fetch('https://api.todo.work/api_integrations/attach_users', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'X-API-KEY': 'YOUR_API_KEY',
    'X-API-SECRET': 'YOUR_API_SECRET'
  },
  body: JSON.stringify({ project_id: 123, user_id: 7 })
});

const data = await response.json();
console.log(data.response);
Response
{ "response": true }

Chat

Conversations, group management and messaging. Access is scoped to the conversations you participate in.

GET /chat/get_conversations List conversations

The acting user’s conversations, pinned-first then most recent, with participants and last message.

Parameters
NameTypeDescription
limit
optional · query
int Default 50.
offset
optional · query
int Default 0.
Permission

Any key

Request
// List conversations
const response = await fetch('https://api.todo.work/api_integrations/chat/get_conversations?limit=50&offset=0', {
  method: 'GET',
  headers: {
    'Content-Type': 'application/json',
    'X-API-KEY': 'YOUR_API_KEY',
    'X-API-SECRET': 'YOUR_API_SECRET'
  }
});

const data = await response.json();
console.log(data.response);
Response
{
  "response": {
    "status": true,
    "conversations": [
      { "id": 5, "type": "private", "name": "Dana", "unread_count": 2, "last_message": { } }
    ],
    "has_more": false
  }
}

GET /chat/get_messages Get messages

Messages in a conversation, in chronological order, paginated with a before_id cursor.

Parameters
NameTypeDescription
conversation_id
required · query
int Conversation id.
before_id
optional · query
int Cursor: fetch messages older than this id.
limit
optional · query
int Default 50.
Permission

Any key (participant)

Request
// Get the latest messages of conversation 5
const response = await fetch('https://api.todo.work/api_integrations/chat/get_messages?conversation_id=5&limit=50', {
  method: 'GET',
  headers: {
    'Content-Type': 'application/json',
    'X-API-KEY': 'YOUR_API_KEY',
    'X-API-SECRET': 'YOUR_API_SECRET'
  }
});

const data = await response.json();
console.log(data.response);
Response
{
  "response": {
    "status": true,
    "messages": [
      { "id": 900, "content": "Hi!", "message_type": "text", "sender": { "id": 7, "display_name": "Dana" } }
    ],
    "has_more": true
  }
}

GET /chat/get_visible_users List who you can message

Users the acting user can start a 1:1 chat with, with an optional name search.

Parameters
NameTypeDescription
search
optional · query
string Case-insensitive match on display name.
limit
optional · query
int Default 50.
Permission

Any key

Request
// List users you can chat with
const response = await fetch('https://api.todo.work/api_integrations/chat/get_visible_users?limit=50', {
  method: 'GET',
  headers: {
    'Content-Type': 'application/json',
    'X-API-KEY': 'YOUR_API_KEY',
    'X-API-SECRET': 'YOUR_API_SECRET'
  }
});

const data = await response.json();
console.log(data.response);
Response
{
  "response": {
    "status": true,
    "users": [ { "id": 7, "display_name": "Dana", "type": "user" } ]
  }
}

POST /chat/get_or_create_private Open a 1:1 conversation

Gets the existing private conversation with a user, or creates it.

Parameters
NameTypeDescription
user_id
required · body
int The other user (must be visible to you).
Permission

Any key

Request
// Open a chat with user 7
const response = await fetch('https://api.todo.work/api_integrations/chat/get_or_create_private', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'X-API-KEY': 'YOUR_API_KEY',
    'X-API-SECRET': 'YOUR_API_SECRET'
  },
  body: JSON.stringify({ user_id: 7 })
});

const data = await response.json();
console.log(data.response);
Response
{
  "response": { "status": true, "conversation": { "id": 5, "type": "private" }, "created": false }
}

POST /chat/create_group Create a group

Creates a group; you become its admin. Unreachable participants are skipped.

Parameters
NameTypeDescription
name
required · body
string Group name.
participants
optional · body
array[int] User ids to add.
image
optional · body
string Storage id of a group image.
Permission

Any key

Request
// Create a group with three members
const response = await fetch('https://api.todo.work/api_integrations/chat/create_group', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'X-API-KEY': 'YOUR_API_KEY',
    'X-API-SECRET': 'YOUR_API_SECRET'
  },
  body: JSON.stringify({
    name: 'New project',
    participants: [5, 12, 18]
  })
});

const data = await response.json();
console.log(data.response);
Response
{
  "response": {
    "status": true,
    "conversation": { "id": 200, "type": "group", "name": "New project", "my_role": "admin" }
  }
}

POST /chat/send_message Send a message

Sends a text message. Supports @[Name](userId) and #[Title](ticketId) mentions; URLs auto-expand into link/ticket cards.

Parameters
NameTypeDescription
conversation_id
required · body
int Target conversation.
content
required · body
string Message text (mention syntax supported).
reply_to_id
optional · body
int Message being replied to.
temp_id
optional · body
string Client id echoed back for optimistic UI.
Permission

Any key (participant)

Request
// Send a message with a mention
const response = await fetch('https://api.todo.work/api_integrations/chat/send_message', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'X-API-KEY': 'YOUR_API_KEY',
    'X-API-SECRET': 'YOUR_API_SECRET'
  },
  body: JSON.stringify({
    conversation_id: 5,
    content: 'Hi @[Dana](7), see #[Ticket](456)',
    temp_id: 'client_123'
  })
});

const data = await response.json();
console.log(data.response);
Response
{
  "response": {
    "status": true,
    "message": { "id": 901, "content": "Hi @Dana, see #Ticket", "message_type": "text" }
  }
}

POST /chat/mark_read Mark a conversation read

Marks every message in a conversation as read and resets the unread count.

Parameters
NameTypeDescription
conversation_id
required · body
int Conversation id.
Permission

Any key (participant)

Request
// Mark conversation 5 read
const response = await fetch('https://api.todo.work/api_integrations/chat/mark_read', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'X-API-KEY': 'YOUR_API_KEY',
    'X-API-SECRET': 'YOUR_API_SECRET'
  },
  body: JSON.stringify({ conversation_id: 5 })
});

const data = await response.json();
console.log(data.response);
Response
{ "response": { "status": true } }

Kanban

Build and maintain boards: lists (columns) and the ticket cards on them.

POST /kanban/boards List boards

Boards owned by or shared with the acting user, most recently updated first.

Parameters
NameTypeDescription
id
optional · body
int A specific board id.
limit
optional · body
int Default 50.
page
optional · body
int 0-indexed; page 0 includes the total count.
Permission

Any key

Request
// List my boards
const response = await fetch('https://api.todo.work/api_integrations/kanban/boards', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'X-API-KEY': 'YOUR_API_KEY',
    'X-API-SECRET': 'YOUR_API_SECRET'
  },
  body: JSON.stringify({ limit: 10, page: 0 })
});

const data = await response.json();
console.log(data.response);
Response
{
  "response": {
    "items": [ { "id": 42, "name": "Daily — Dana", "color": "#59ce8f", "users": [] } ],
    "num": 1
  }
}

POST /kanban/lists List columns

The lists (columns) of a board, in order.

Parameters
NameTypeDescription
kanban_id
required · body
int Board id.
limit
optional · body
int Default 50.
page
optional · body
int 0-indexed.
Permission

Any key (board access)

Request
// List the columns of board 42
const response = await fetch('https://api.todo.work/api_integrations/kanban/lists', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'X-API-KEY': 'YOUR_API_KEY',
    'X-API-SECRET': 'YOUR_API_SECRET'
  },
  body: JSON.stringify({ kanban_id: 42, page: 0 })
});

const data = await response.json();
console.log(data.response);
Response
{
  "response": {
    "items": [ { "id": 101, "kanban_id": 42, "name": "To do", "ord": 0, "tickets": [] } ],
    "num": 1
  }
}

POST /kanban/get_tickets Get board cards

Every active ticket card on a board, with its list and order.

Parameters
NameTypeDescription
kanban_id
required · body
int Board id.
Permission

Any key (board access)

Request
// Get the cards on board 42
const response = await fetch('https://api.todo.work/api_integrations/kanban/get_tickets', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'X-API-KEY': 'YOUR_API_KEY',
    'X-API-SECRET': 'YOUR_API_SECRET'
  },
  body: JSON.stringify({ kanban_id: 42 })
});

const data = await response.json();
console.log(data.response);
Response
{ "response": [ { "id": 789, "list_id": 101, "ord": 0, "project_id": 5 } ] }

POST /kanban/add_board Create a board

Creates a board (you are added as a participant); optionally shares it with another user.

Parameters
NameTypeDescription
name
required · body
string Board name.
share_user_id
optional · body
int Share the board with this user.
Permission

Any key

Request
// Create a shared board
const response = await fetch('https://api.todo.work/api_integrations/kanban/add_board', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'X-API-KEY': 'YOUR_API_KEY',
    'X-API-SECRET': 'YOUR_API_SECRET'
  },
  body: JSON.stringify({ name: 'Daily — Dana', share_user_id: 7 })
});

const data = await response.json();
console.log(data.response);
Response
{ "response": { "board": { "id": 43, "name": "Daily — Dana" } } }

POST /kanban/add_list Add or update a column

Creates a list (column), or updates it when id is sent.

Parameters
NameTypeDescription
kanban_id
required · body
int Board id.
name
required · body
string List name.
id
optional · body
int Send to update an existing list.
project_id
optional · body
int Link the column to a project (auto-populated).
Permission

Any key (board access)

Request
// Add a column to board 42
const response = await fetch('https://api.todo.work/api_integrations/kanban/add_list', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'X-API-KEY': 'YOUR_API_KEY',
    'X-API-SECRET': 'YOUR_API_SECRET'
  },
  body: JSON.stringify({ kanban_id: 42, name: 'June 27 — Thursday' })
});

const data = await response.json();
console.log(data.response);
Response
{ "response": { "list": { "id": 102, "kanban_id": 42, "name": "June 27 — Thursday" } } }

POST /kanban/move_ticket Place a card

Places or moves a ticket card onto a list.

Parameters
NameTypeDescription
ticket_id
required · body
int Ticket id.
list_id
required · body
int Target list id.
Permission

Any key (board access)

Request
// Move a card onto a list
const response = await fetch('https://api.todo.work/api_integrations/kanban/move_ticket', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'X-API-KEY': 'YOUR_API_KEY',
    'X-API-SECRET': 'YOUR_API_SECRET'
  },
  body: JSON.stringify({ ticket_id: 789, list_id: 102 })
});

const data = await response.json();
console.log(data.response);
Response
{ "response": true }

POST /kanban/remove_ticket Remove a card

Removes a ticket card from a list (the ticket itself is untouched).

Parameters
NameTypeDescription
ticket_id
required · body
int Ticket id.
list_id
required · body
int List id.
Permission

Any key (board access)

Request
// Remove a card from a list
const response = await fetch('https://api.todo.work/api_integrations/kanban/remove_ticket', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'X-API-KEY': 'YOUR_API_KEY',
    'X-API-SECRET': 'YOUR_API_SECRET'
  },
  body: JSON.stringify({ ticket_id: 789, list_id: 102 })
});

const data = await response.json();
console.log(data.response);
Response
{ "response": true }

POST /kanban/remove_list Delete a column

Deletes a list (column) and its card placements.

Parameters
NameTypeDescription
id
required · body
int List id.
Permission

Any key (board access)

Request
// Delete a column
const response = await fetch('https://api.todo.work/api_integrations/kanban/remove_list', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'X-API-KEY': 'YOUR_API_KEY',
    'X-API-SECRET': 'YOUR_API_SECRET'
  },
  body: JSON.stringify({ id: 102 })
});

const data = await response.json();
console.log(data.response);
Response
{ "response": true }

POST /kanban/remove_board Delete a board

Deletes a board with all its lists and card placements.

Parameters
NameTypeDescription
id
required · body
int Board id.
Permission

Any key (board ownership)

Request
// Delete a board
const response = await fetch('https://api.todo.work/api_integrations/kanban/remove_board', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'X-API-KEY': 'YOUR_API_KEY',
    'X-API-SECRET': 'YOUR_API_SECRET'
  },
  body: JSON.stringify({ id: 42 })
});

const data = await response.json();
console.log(data.response);
Response
{ "response": true }

POST /kanban/find_shared_board Find a shared board admin only

Finds a board shared between two specific users.

Parameters
NameTypeDescription
user_a
required · body
int First user id.
user_b
required · body
int Second user id.
Permission

Admin key only

Request
// Find the board shared between two users
const response = await fetch('https://api.todo.work/api_integrations/kanban/find_shared_board', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'X-API-KEY': 'YOUR_API_KEY',
    'X-API-SECRET': 'YOUR_API_SECRET'
  },
  body: JSON.stringify({ user_a: 5, user_b: 12 })
});

const data = await response.json();
console.log(data.response);
Response
{ "response": { "id": 42, "name": "Daily — Dana", "users": [] } }
// or { "response": false } when none exists

Team Insights

Read-only presence, time-tracking, call transcripts and daily-target progress. Built for an AI office-manager integration.

  • The /tracking endpoints read platform-wide presence and require an admin key.

GET /tracking/get_active_shifts Who is clocked in admin only

Everyone with an open shift right now.

Parameters

No parameters — send the authentication headers only.

Permission

Admin key only

Request
// Who is currently clocked in
const response = await fetch('https://api.todo.work/api_integrations/tracking/get_active_shifts', {
  method: 'GET',
  headers: {
    'Content-Type': 'application/json',
    'X-API-KEY': 'YOUR_API_KEY',
    'X-API-SECRET': 'YOUR_API_SECRET'
  }
});

const data = await response.json();
console.log(data.response);
Response
{
  "response": [ { "user_id": 7, "date_start": "2026-06-27T08:02:00Z", "absence_type": null } ]
}

GET /tracking/get_active_handlings Who is on a timer admin only

Everyone with an open task-timer right now.

Parameters

No parameters — send the authentication headers only.

Permission

Admin key only

Request
// Who is currently working a ticket
const response = await fetch('https://api.todo.work/api_integrations/tracking/get_active_handlings', {
  method: 'GET',
  headers: {
    'Content-Type': 'application/json',
    'X-API-KEY': 'YOUR_API_KEY',
    'X-API-SECRET': 'YOUR_API_SECRET'
  }
});

const data = await response.json();
console.log(data.response);
Response
{
  "response": [ { "handler_uid": 7, "tid": 123, "due_date": "2026-06-27T10:15:00Z" } ]
}

GET /tracking/get_today_handlings Today’s logged work admin only

Task-timers that closed today.

Parameters
NameTypeDescription
date
optional · query
string Y-m-d (defaults to today).
Permission

Admin key only

Request
// Work logged today
const response = await fetch('https://api.todo.work/api_integrations/tracking/get_today_handlings?date=2026-06-27', {
  method: 'GET',
  headers: {
    'Content-Type': 'application/json',
    'X-API-KEY': 'YOUR_API_KEY',
    'X-API-SECRET': 'YOUR_API_SECRET'
  }
});

const data = await response.json();
console.log(data.response);
Response
{
  "response": [ { "handler_uid": 7, "tid": 123, "duration": 45, "ended_at": "2026-06-27T11:00:00Z" } ]
}

GET /voice/get_calls_for_user Get call transcripts

Completed voice-call transcripts a user took part in.

Parameters
NameTypeDescription
user_id
required · query
int Whose calls to fetch.
from_date
optional · query
string Inclusive lower bound (Y-m-d).
limit
optional · query
int Default 100, max 200.
Permission

Any key

Request
// Recent call transcripts for user 7
const response = await fetch('https://api.todo.work/api_integrations/voice/get_calls_for_user?user_id=7&from_date=2026-06-01&limit=50', {
  method: 'GET',
  headers: {
    'Content-Type': 'application/json',
    'X-API-KEY': 'YOUR_API_KEY',
    'X-API-SECRET': 'YOUR_API_SECRET'
  }
});

const data = await response.json();
console.log(data.response);
Response
{
  "response": {
    "calls": [
      {
        "call_id": "call_abc",
        "initiator_id": 7,
        "participants": [7, 12],
        "started_at": "2026-06-27T09:00:00Z",
        "duration_seconds": 83,
        "summary": "…",
        "full_text": "…"
      }
    ]
  }
}

GET /panels/daily_target_for_user Daily target progress

One user’s daily-target progress — the same numbers as the dashboard target widget.

Parameters
NameTypeDescription
user_id
required · query
int The user.
date
optional · query
string Y-m-d (defaults to today).
Permission

Any key

Notes
  • user_id is genuinely required — without it the endpoint answers a bare false.
  • The response carries about 26 fields; the common ones are shown below. It also includes target / target_type, which are aliases of daily_target / daily_target_type.
Request
// Daily target progress for user 7
const response = await fetch('https://api.todo.work/api_integrations/panels/daily_target_for_user?user_id=7&date=2026-06-27', {
  method: 'GET',
  headers: {
    'Content-Type': 'application/json',
    'X-API-KEY': 'YOUR_API_KEY',
    'X-API-SECRET': 'YOUR_API_SECRET'
  }
});

const data = await response.json();
console.log(data.response);
Response
{
  "response": {
    "user_id": 7,
    "display_name": "Dana",
    "date": "2026-06-27",
    "daily_target": 8,
    "daily_target_type": "hours",
    "perc": 62.5,
    "ttl_hours_parsed": "05:00"
  }
}

Media Gallery

List, upload and delete project media (images, video and files).

GET /media_gallery/results List media

Media for a project and its tickets, paginated.

Parameters
NameTypeDescription
project_id
required · query
int Project to scope media to.
limit
optional · query
int Default 50.
page
optional · query
int 0-indexed; page 0 includes the total count.
q
optional · query
string Free-text filter on the file name and the owning ticket subject.
Permission

Any key

Notes
  • project_id is required — calling without it answers 400.
  • Deleting requires POST; a GET to /media_gallery/delete is refused with 405.
Request
// List media in project 123
const response = await fetch('https://api.todo.work/api_integrations/media_gallery/results?project_id=123&limit=10&page=0', {
  method: 'GET',
  headers: {
    'Content-Type': 'application/json',
    'X-API-KEY': 'YOUR_API_KEY',
    'X-API-SECRET': 'YOUR_API_SECRET'
  }
});

const data = await response.json();
console.log(data.response);
Response
{
  "response": {
    "num": 1,
    "items": [
      {
        "id": 5,
        "name": "logo.png",
        "url": "https://…",
        "type": "image",
        "project_id": 123,
        "ticket_id": false,
        "subject": false,
        "date_created": false
      }
    ]
  }
}

POST /media_gallery/upload Upload media

Uploads media files to a project (multipart/form-data).

Parameters
NameTypeDescription
project_id
required · formdata
int Target project.
files
required · formdata
file[] The files to upload.
Permission

Any key (needs project access)

Request
// Upload media (multipart/form-data — no Content-Type header)
const formData = new FormData();
formData.append('project_id', 123);
// formData.append('files[]', file1);

const response = await fetch('https://api.todo.work/api_integrations/media_gallery/upload', {
  method: 'POST',
  headers: {
    'X-API-KEY': 'YOUR_API_KEY',
    'X-API-SECRET': 'YOUR_API_SECRET'
  },
  body: formData
});

const data = await response.json();
console.log(data.response);
Response
{ "response": true }

GET /media_gallery/delete Delete media

Deletes a single media file by its storage id.

Parameters
NameTypeDescription
media_id
required · query
int The storage id to delete.
Permission

Any key (needs project access)

Request
// Delete a media file
const response = await fetch('https://api.todo.work/api_integrations/media_gallery/delete?media_id=5', {
  method: 'GET',
  headers: {
    'Content-Type': 'application/json',
    'X-API-KEY': 'YOUR_API_KEY',
    'X-API-SECRET': 'YOUR_API_SECRET'
  }
});

const data = await response.json();
console.log(data.response);
Response
{ "response": { "success": true } }
// or { "response": { "success": false, "message": "Media item not found" } }