Developers

Your tasks, from wherever you want: a script, an automation or an AI agent. With a token you create and you revoke.

Get started

Three steps and you're in. No app registration, no client keys, no OAuth: a token and that's it.

  1. 1Create a token in Settings → Quick capture, ticking the permissions you need.
  2. 2Keep it: it's shown once and can't be read again.
  3. 3Check that it works, without creating anything:
curl https://mykairo.app/api/capture -H "Authorization: Bearer kairo_…"

{ "ok": true, "name": "Ana" }

Tokens and permissions

The token is stored hashed; Kairo doesn't know it in the clear and can't show it again. Revoke it from the same screen and it stops working immediately. It can expire.

Permissions

Chosen when you create the token, and they can't be widened afterwards. A token on a fridge sticker doesn't need to read your whole account: give it the weakest one that does the job.

captureCreate tasks from outside (Siri, shortcuts, email).
tasks:readRead and search tasks.
tasks:writeCreate, change, complete and delete tasks.
lists:readRead lists, sections and labels.
lists:writeCreate and change lists.

How to talk to it

JSON all the way, over HTTPS. Authentication goes in the header (“X-Kairo-Token” is accepted too).

Authorization: Bearer kairo_…
Content-Type: application/json

https://mykairo.app/api/v1/…

Dates

ISO 8601 in UTC. An all-day date is midnight in YOUR timezone, not UTC: send “2026-03-03T00:00:00Z” from Madrid and you'll see March 3rd, but underneath they aren't the same thing. The safest route is to send the sentence in `text` and let Kairo read it with your clock.

Identifiers

Opaque strings. Don't assume their shape.

What you can't reach

Someone else's task answers 404, exactly like one that doesn't exist. That's deliberate: identifiers don't tell you what's out there.

Idempotency

You can supply the `id` when creating. Repeat the request with the same one and nothing is duplicated: you get back the one that was already there.

A task

This is what the API returns. It's the same object on every route.

FieldType
idstringIdentifier. You can supply it on create so a retry doesn't duplicate.
contentstringThe title.
descriptionstring | nullThe notes. Line breaks are kept.
priority1 – 41 is highest; 4 means “no priority”.
completedboolean
completedAtstring | nullISO 8601.
dueDatestring | nullWhen it's due. ISO 8601, in UTC.
dueAllDaybooleanWhether it's all-day: then `dueDate` is midnight in YOUR timezone.
deadlinestring | nullThe deadline, separate from when you start.
recurrencestring | null“every:week:1:1” = every Monday. “every:month:1”, “every:year:1”.
somedaybooleanParked, with no date.
esEventobooleanA birthday or an expiry date: it isn't completed and rolls over by itself.
projectId / projectNamestringThe list where YOU see it (on a shared task, each person their own).
sectionId / sectionNamestring | nullThe section inside that list.
parentIdstring | nullWhich task it hangs from.
labelsarray`{ id, name, color }`.
assignee / creatorobject | null`{ id, name, image }`.
childCount / commentCountnumberHow many sub-tasks and comments it has.
createdAtstringISO 8601.

Routes

Eleven, and no more. What isn't here doesn't exist yet.

GET/api/v1/metasks:read

Who you are: name, email, timezone and language.

GET/api/v1/taskstasks:read

Your tasks, with the app's views and filters.

POST/api/v1/taskstasks:write

Create, from a whole sentence or from explicit fields.

GET/api/v1/tasks/{id}tasks:read

The detail, with sub-tasks and comments.

PATCH/api/v1/tasks/{id}tasks:write

Change title, date, priority, list, labels…

DELETE/api/v1/tasks/{id}tasks:write

To the bin, where it can be recovered.

POST/api/v1/tasks/{id}/completetasks:write

Complete, or reopen with “completed: false”.

POST/api/v1/tasks/{id}/commentstasks:write

Comment, notifying whoever has it.

GET/api/v1/listslists:read

Your lists, with their sections.

POST/api/v1/listslists:write

Create a list.

GET/api/v1/labelslists:read

Your labels.

When creating, “text” goes through the same parser as the app: it picks up the date, the time, the list (#list), the labels (@label), the priority (p1…p4) and the repetition. Explicit fields win over whatever came out of the sentence. With no list, it lands wherever your Quick capture says.

curl -X POST https://mykairo.app/api/v1/tasks \
  -H "Authorization: Bearer kairo_…" \
  -H "Content-Type: application/json" \
  -d '{"text": "Call Marta tomorrow at 5 #Work p1 @calls"}'

{
  "task": {
    "id": "cmue69128…",
    "content": "Call Marta",
    "dueDate": "2026-09-24T15:00:00.000Z",
    "dueAllDay": false,
    "priority": 1,
    "projectName": "Work",
    "labels": [{ "id": "cm…", "name": "calls", "color": "charcoal" }]
  }
}

Views and filters

The same ones as the app's screens: what comes back here is what you'd see there, no more and no less.

view=todayToday's and anything overdue, like the Today screen.
view=upcoming&days=7The next few days, with anything overdue first.
view=overdueOnly what's past its date.
view=inboxThe inbox.
view=anytimeNo date, from lists that count as Anytime.
view=somedayWhatever is parked.
projectId=…One list. Ids come from /api/v1/lists.
sectionId=…One section.
labelId=…One label.
q=textoText search.
completed=trueCompleted ones, instead of open ones.

Pages

“limit” (50 by default, 200 at most) and “cursor”, the id of the last task served. When there's nothing left, “nextCursor” comes back null. If the cursor no longer appears in that view — completed, moved — it answers 400 instead of quietly returning the first page again: staying silent would leave an agent going round for ever.

curl "https://mykairo.app/api/v1/tasks?view=today&limit=50" \
  -H "Authorization: Bearer kairo_…"

{ "tasks": [ … ], "nextCursor": "cmue69128…" }

Errors and limits

120 requests per minute, per token — not per IP: an automation server's address is shared by many. Past that, a 429 with “Retry-After” in seconds. Errors always take the same shape, so you can tell them apart without reading the text:

sin_tokenYou didn't send one.401
token_invalidoThat token doesn't exist.401
token_caducadoIt existed and has expired.401
sin_permisoThe token lacks the permission that route asks for.403
no_encontradoIt doesn't exist, or you can't reach it.404
cursor_invalidoThe cursor no longer works for that view.400
datos_invalidosThe body doesn't match; comes with “issues”.422
demasiadas_peticionesYou went past the per-minute limit.429
{ "error": "Token no válido", "code": "token_invalido" }

AI agents (MCP)

Kairo speaks MCP, the protocol a model uses to call tools. Point your client at it — Claude, or anything else that speaks MCP — and the agent can look at your day, create tasks and close them. Within the token's permissions: a read-only one looks and doesn't touch.

In your client's configuration

{
  "mcpServers": {
    "kairo": {
      "type": "http",
      "url": "https://mykairo.app/api/mcp",
      "headers": { "Authorization": "Bearer kairo_…" }
    }
  }
}

Tools

list_tasksview, projectId, q, completed, limit

What's in Today, Upcoming, a list or a search.

create_tasktext, projectId

Create from a whole sentence.

complete_taskid, completed

Complete or reopen.

get_taskid

The detail: description, sub-tasks, comments.

list_lists—

The lists and their sections.

How it behaves

  • It returns just enough to decide — id, title, when, priority and list: an agent that pulls fifty whole tasks eats its own context window.
  • A tool failure comes back inside the result, not as a protocol error: that way the model can read it, explain it to you and try another way.
  • When creating, give it the whole sentence. The parser is the app's own, so “tomorrow at five” and “every Monday” already work without the agent doing date maths.

An agent with a write token can create and complete your tasks. Give it the weakest token that does the job, and revoke it when it's no longer needed.

Capturing from outside

The shortest path for things that only need writing down: one sentence. It's what Siri, iOS Shortcuts, NFC stickers and email capture use. The step-by-step recipes live in the app, under Settings → Quick capture.

curl -X POST https://mykairo.app/api/capture \
  -H "Authorization: Bearer kairo_…" \
  -H "Content-Type: application/json" \
  -d '{"text": "Buy batteries #Home"}'

It answers with the task it created and with “spoken”, a sentence ready for Siri to read out loud.

Calendar

Your dated tasks in iCalendar format, to subscribe from Google, Apple or Outlook. The address comes from Settings → Calendar: it carries its own key, it's read-only and needs no token.

Recipes

Three things you can wire up in a minute.

Note something from the terminal, without opening anything

kairo() {
  curl -s -X POST https://mykairo.app/api/v1/tasks \
    -H "Authorization: Bearer $KAIRO_TOKEN" \
    -H "Content-Type: application/json" \
    -d "{\"text\": \"$*\"}" > /dev/null
}

kairo Review the contract on Friday p2 #Work

What you've got today, in one line

curl -s "https://mykairo.app/api/v1/tasks?view=today" \
  -H "Authorization: Bearer $KAIRO_TOKEN" \
  | jq -r '.tasks[] | "• \(.content)"'

Close everything in a list

curl -s "https://mykairo.app/api/v1/tasks?projectId=$LISTA" \
  -H "Authorization: Bearer $KAIRO_TOKEN" | jq -r '.tasks[].id' |
while read id; do
  curl -s -X POST "https://mykairo.app/api/v1/tasks/$id/complete" \
    -H "Authorization: Bearer $KAIRO_TOKEN" > /dev/null
done

Versions

“/api/v1” is stable: the fields that exist today will keep existing, and keep meaning the same. If something has to change shape it'll be “/api/v2”, and v1 will keep working. Additions — new fields, new routes — can show up without notice, so don't assume an object has exactly these keys and no others.

Missing something?

Write to us and tell us what you're building. Whatever gets asked for most is what gets built next. info@mykairo.app