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.
- 1Create a token in Settings → Quick capture, ticking the permissions you need.
- 2Keep it: it's shown once and can't be read again.
- 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.
| Field | Type | |
|---|---|---|
id | string | Identifier. You can supply it on create so a retry doesn't duplicate. |
content | string | The title. |
description | string | null | The notes. Line breaks are kept. |
priority | 1 – 4 | 1 is highest; 4 means “no priority”. |
completed | boolean | |
completedAt | string | null | ISO 8601. |
dueDate | string | null | When it's due. ISO 8601, in UTC. |
dueAllDay | boolean | Whether it's all-day: then `dueDate` is midnight in YOUR timezone. |
deadline | string | null | The deadline, separate from when you start. |
recurrence | string | null | “every:week:1:1” = every Monday. “every:month:1”, “every:year:1”. |
someday | boolean | Parked, with no date. |
esEvento | boolean | A birthday or an expiry date: it isn't completed and rolls over by itself. |
projectId / projectName | string | The list where YOU see it (on a shared task, each person their own). |
sectionId / sectionName | string | null | The section inside that list. |
parentId | string | null | Which task it hangs from. |
labels | array | `{ id, name, color }`. |
assignee / creator | object | null | `{ id, name, image }`. |
childCount / commentCount | number | How many sub-tasks and comments it has. |
createdAt | string | ISO 8601. |
Routes
Eleven, and no more. What isn't here doesn't exist yet.
GET/api/v1/metasks:readWho you are: name, email, timezone and language.
GET/api/v1/taskstasks:readYour tasks, with the app's views and filters.
POST/api/v1/taskstasks:writeCreate, from a whole sentence or from explicit fields.
GET/api/v1/tasks/{id}tasks:readThe detail, with sub-tasks and comments.
PATCH/api/v1/tasks/{id}tasks:writeChange title, date, priority, list, labels…
DELETE/api/v1/tasks/{id}tasks:writeTo the bin, where it can be recovered.
POST/api/v1/tasks/{id}/completetasks:writeComplete, or reopen with “completed: false”.
POST/api/v1/tasks/{id}/commentstasks:writeComment, notifying whoever has it.
GET/api/v1/listslists:readYour lists, with their sections.
POST/api/v1/listslists:writeCreate a list.
GET/api/v1/labelslists:readYour 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.401token_invalidoThat token doesn't exist.401token_caducadoIt existed and has expired.401sin_permisoThe token lacks the permission that route asks for.403no_encontradoIt doesn't exist, or you can't reach it.404cursor_invalidoThe cursor no longer works for that view.400datos_invalidosThe body doesn't match; comes with “issues”.422demasiadas_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, limitWhat's in Today, Upcoming, a list or a search.
create_tasktext, projectIdCreate from a whole sentence.
complete_taskid, completedComplete or reopen.
get_taskidThe 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 #WorkWhat 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
doneVersions
“/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