CHEATSHEET

API Reference

Programmatic access to your cheats, Tasks, Brain, Pennies, Projects, Hours and Dues, for external tools acting on your behalf (e.g. a local Claude Code project). Every account can generate a key from the User page's API Access section. Free accounts get read-only access; Pro unlocks write access (create/update/delete) plus a much higher daily limit. See Billing for pricing.

This page covers the REST API itself. If you just want to point Claude Code, Claude Desktop, or another MCP client at Cheatsheet, see Connect an MCP Client instead.

On this page:

Authentication

Send your key as a bearer token on every request:

Authorization: Bearer csk_live_...

Keys are shown to you only once, at creation, and can be revoked at any time from the User page. Each key is scoped to either read or read + write, and a write-scoped key can also do everything a read-scoped key can.

Sections

A key can also be limited to particular sections of the app: Cheats, Tasks, Brain, Pennies, Projects, Hours and Dues, ticked when you create the key on the User page. Scopes decide what a key can do; sections decide what it can reach, and the two apply independently. A key left with everything ticked reaches everything, which is also how every key created before this existed behaves.

This is worth using. A key wired into an unrelated project to look things up in your cheats has no reason to be able to read your Pennies log or your Brain, and until you untick those it can. An endpoint outside a key's sections returns 403 with a message naming the section. A connected MCP client is also offered only the tools for the sections its key covers, so a narrower key means a shorter tool list.

Keys issued through the sign-in flow for an MCP client (rather than created by hand on the User page) always reach every section, because that flow gives the client no way to ask for part of the app. Revoke the key if you want to take that access back.

Client identification (optional)

If you're building your own integration, you can optionally send an X-Cheatsheet-Client header identifying it — a short name of your choosing (letters, numbers, -, _, up to 40 characters), e.g.:

X-Cheatsheet-Client: my-cool-plugin

This is purely so the User page's API usage summary can show a per-client breakdown of a key's daily requests, and so "last used" for a key shows which client it was. It's entirely optional — a request that omits it works exactly the same, just grouped under "unknown" (or, for a direct "type": "http" MCP Connector entry with no header set, "mcp-connector-direct"). Applies the same way to the MCP Connector's headers as it does to the REST endpoints' request headers.

Base URL

https://cheats.aarontrotter.com

The endpoints below all live under /mcp/, e.g. /mcp/search — that's just this API's URL prefix, not the MCP protocol. An actual MCP client instead connects at the bare path /mcp (no trailing segment) under this same base URL, and doesn't call the endpoints below directly — see Connect an MCP Client.

Limits

  • Daily quota: shared across all of an account's keys, resets at midnight UTC, exceeding it returns 429. Free: 50 requests/day, read-only. Pro: 500 requests/day fair use, full read + write.
  • Write burst limit: the write endpoints are additionally capped at 60 requests per 5 minutes.
  • Write endpoints (add/update/delete, plus Pennies' fill/void and Projects' move) require the calling account to be Pro; a Free account's key returns 403 on those, but can still use every read endpoint.

Errors

Errors are returned as { "error": "message" } with a matching HTTP status: 400 bad request, 401 missing/invalid key, 403 forbidden (wrong scope, outside the key's sections, not Pro, not the owner), 404 not found, 429 rate limited, 500 server error.

Endpoints

GET /mcp/search

Search your visible cheats — public cheats plus your own (and any linked account's) private ones.

Scope: read

ParameterTypeRequiredDescription
qstringNoSearch text; omitted means the most recently created cheats
typestringNoRestrict results to one type/category name
limitnumberNoResults per page — default 8, max 25
pagenumberNo0-indexed page number, default 0 — use with hasMore to fetch further pages

Returns { results: [{ id, title, type, snippet, private, stars, points }], total, page, hasMore }. snippet is the cheat body truncated to 400 characters. total is the number of cheats matching the search across all pages; hasMore is true when a further page exists — request it with page + 1.

GET /mcp/search?q=docker&limit=5&page=1
Authorization: Bearer csk_live_...

GET /mcp/getCheat

Fetch the full body of one cheat.

Scope: read

ParameterInTypeRequiredDescription
idquery stringstringYesThe cheat id, as returned by /mcp/search

Returns { id, title, type, body: { text, highlights, links }, markup, private, stars, points, ownStars, ownPoints }. markup is the same body as tagged text (see Cheat body markup), which can be edited and sent straight back as body.text to /mcp/updateCheat without losing any formatting. stars and points are cumulative across the cheat's whole revision history, so a later revision starts with every vote cast before it already counted. ownStars and ownPoints are the votes cast on this revision alone, which is the figure to compare when you want to know which version people actually preferred.

404 if the cheat doesn't exist; 403 if it's private and you're not its owner (or a linked account).

GET /mcp/getCheat?id=abc123
Authorization: Bearer csk_live_...

GET /mcp/getRevisions

List the full append-only revision history for a cheat's lineage — every past and current version, oldest first.

Scope: read

ParameterInTypeRequiredDescription
idquery stringstringYesId of any revision in the lineage — not just the original

Returns { originalId, revisions: [{ id, title, type, snippet, private, ceased, createdDateMs, stars, points, ownStars, ownPoints }] }. Exactly one entry has ceased: false — that's the current version; the others are prior edits kept for history. Rank revisions by ownPoints rather than points: the latter is cumulative down the list, so sorting by it would mostly just tell you which revision is newest.

404 if the cheat doesn't exist; 403 if it's private and you're not its owner (or a linked account).

GET /mcp/getRevisions?id=abc123
Authorization: Bearer csk_live_...

Cheat body markup

A cheat's notes, variables and links are coloured decorations in the editor, stored as character offsets. Rather than counting offsets, /mcp/addCheat and /mcp/updateCheat read tags out of body.text and turn them into the same decorations:

WriteBecomes
[note]Add a user[/note]A note, for headings and short explanations
[var]UserName[/var]A variable, for a placeholder the reader substitutes
[link=https://example.com]Docs[/link]A link labelled "Docs"; an href with no scheme gets https://
https://example.comA link to itself, with no tag needed
value //short commentA note over the // comment to the end of the line, with no tag needed

A tag without its partner is left in the text as written. A link may overlap a note or a variable.

"body": { "text": "[note]Add a user[/note]\nuseradd -m [var]UserName[/var] //creates the home dir" }

POST /mcp/addCheat

Create a new cheat. Starts with 0 stars and 0 points regardless of what's sent.

Scope: write

FieldTypeRequiredDescription
titlestringYesMax 40 characters
typeNamestringYesMax 15 characters, case-insensitive; created automatically if it doesn't already exist
body.textstringYesMax 100 lines, 600 characters per line; whole record capped at 10 KB. Read as markup unless body.highlights or body.links is also sent
body.highlights, body.linksarrayNoRaw editor offsets, [{ from, to, className }] and [{ from, to, href }], as /mcp/getCheat returns them. Sending either turns markup off, and body.text is stored exactly as sent
privatebooleanYesMust be sent explicitly — there is no default

Returns { id } — the new cheat's id.

POST /mcp/addCheat
Authorization: Bearer csk_live_...
Content-Type: application/json

{
  "title": "Restart nginx",
  "typeName": "linux",
  "body": { "text": "sudo systemctl restart nginx" },
  "private": true
}

POST /mcp/updateCheat

Revise a cheat you own. This is append-only, same as editing a cheat in the browser: the old revision is marked ceased and a new one is created and linked to it, nothing is overwritten in place.

Scope: write

FieldTypeRequiredDescription
idstringYesId of the cheat to revise
titlestringYesMax 40 characters
typeNamestringYesMax 15 characters, case-insensitive; created automatically if it doesn't already exist
body.textstringYesMax 100 lines, 600 characters per line; whole record capped at 10 KB. Read as markup unless body.highlights or body.links is also sent
body.highlights, body.linksarrayNoRaw editor offsets, [{ from, to, className }] and [{ from, to, href }], as /mcp/getCheat returns them. Sending either turns markup off, and body.text is stored exactly as sent
privatebooleanYesMust be sent explicitly — there is no default

Returns { id } — the new revision's id (different from the id you sent in). 404 if the original doesn't exist; 403 if you don't own it.

POST /mcp/updateCheat
Authorization: Bearer csk_live_...
Content-Type: application/json

{
  "id": "abc123",
  "title": "Restart nginx",
  "typeName": "linux",
  "body": { "text": "sudo systemctl restart nginx --now" },
  "private": true
}

POST /mcp/deleteCheat

Delete a cheat you own. Not a hard delete — same as the browser's delete button, the cheat is marked ceased and stops appearing in search/getCheat, but the record and its revision history aren't erased.

Scope: write

FieldTypeRequiredDescription
idstringYesId of the cheat to delete

Returns { success: true }. 404 if the cheat doesn't exist; 403 if you don't own it.

POST /mcp/deleteCheat
Authorization: Bearer csk_live_...
Content-Type: application/json

{
  "id": "abc123"
}

Tasks

Tasks are unencrypted, private-only notes that live outside cheat search entirely, in a separate collection with its own endpoints below. Unlike cheats they have no type taxonomy (just a fixed category) and no private field (always private), and no revision history is ever exposed, even though an edit is append-only under the hood. A task is either permanent or auto-expires and is then automatically deleted (soft-ceased).

These endpoints cover your Tasks page only, meaning the note and list categories. Your Brain is a separate section with its own endpoints below, and nothing here reaches it: an id belonging to a Brain entry is rejected with a 404 rather than quietly working. /mcp/getGuides is the one exception, and it is read-only.

GET /mcp/getGuides

Fetch your brief and rules entries as one formatted block of text, ready to hand straight to an assistant. This is the same text the hosted MCP connector sends automatically when a client connects, exposed here so a tool built on the REST API can fetch it too. Takes no parameters.

Scope: read

Returns { guides }, a single Markdown string with a heading per category and one bullet per entry. guides is an empty string if you have no brief or rules entries.

GET /mcp/getGuides
Authorization: Bearer csk_live_...

GET /mcp/searchTasks

Search your own tasks. Not Algolia-backed like cheat search, just a plain title/text/category substring match over your tasks, since a personal task list is small.

Scope: read

ParameterTypeRequiredDescription
qstringNoSearch text matched against title/text/category; omitted means the most recently created tasks
categorystringNonote or list, to search just that one. Omitted means both
pagenumberNo1-indexed page number, default 1

Returns { tasks: [{ id, title, category, text, highlights, links, checkedLines, createdDateMs, expiresAtMs }], totalPages }. expiresAtMs is null for a permanent task; category is note or list. 400 if category isn't one of those two.

GET /mcp/searchTasks?q=groceries
Authorization: Bearer csk_live_...

GET /mcp/getTask

Fetch one task.

Scope: read

ParameterInTypeRequiredDescription
idquery stringstringYesThe task id, as returned by /mcp/searchTasks

Returns { id, title, category, text, highlights, links, checkedLines, createdDateMs, expiresAtMs }. 404 if the task doesn't exist or is a Brain entry; 403 if you don't own it.

GET /mcp/getTask?id=abc123
Authorization: Bearer csk_live_...

POST /mcp/addTask

Create a new task.

Scope: write

FieldTypeRequiredDescription
titlestringYesMax 40 characters
categorystringNonote or list; defaults to note. A Brain category here is a 400; use /mcp/addBrain instead
textstringYesMax 100 lines, 600 characters per line; whole record capped at 10 KB
durationstringNoOne of permanent, 1h, 1d, 1w; defaults to permanent

Returns { id }, the new task's id.

POST /mcp/addTask
Authorization: Bearer csk_live_...
Content-Type: application/json

{
  "title": "Ship the release",
  "category": "list",
  "text": "1. Tag the branch\n2. Deploy\n3. Announce",
  "duration": "1d"
}

POST /mcp/updateTask

Revise a task you own. Append-only, same as /mcp/updateCheat (the old revision is ceased and a new one created), but there is no endpoint to view that history for Tasks.

Scope: write

FieldTypeRequiredDescription
idstringYesId of the task to revise
titlestringYesMax 40 characters
categorystringNonote or list. Omitted keeps the task's current category
textstringYesMax 100 lines, 600 characters per line; whole record capped at 10 KB
durationstringNoOne of permanent, 1h, 1d, 1w; defaults to permanent

Returns { id }, the new revision's id. 404 if the original doesn't exist or is a Brain entry; 403 if you don't own it.

POST /mcp/updateTask
Authorization: Bearer csk_live_...
Content-Type: application/json

{
  "id": "abc123",
  "title": "Ship the release",
  "category": "list",
  "text": "1. Tag the branch\n2. Deploy\n3. Announce\n4. Watch dashboards",
  "duration": "1d"
}

POST /mcp/deleteTask

Delete a task you own. Not a hard delete, same soft-cease as /mcp/deleteCheat.

Scope: write

FieldTypeRequiredDescription
idstringYesId of the task to delete

Returns { success: true }. 404 if the task doesn't exist or is a Brain entry; 403 if you don't own it.

POST /mcp/deleteTask
Authorization: Bearer csk_live_...
Content-Type: application/json

{
  "id": "abc123"
}

Brain

Your Brain is where an assistant's working context lives: brief entries are orienting context you write for it, rules are constraints it must follow, and memory is what it records for itself between sessions. Your brief and rules are also handed to any MCP client automatically when it connects, so it reads your guidance before doing anything.

Brain and Tasks are separate sections here, not two filters over one list. The five endpoints below reach your Brain and nothing else, exactly as the five above reach your Tasks and nothing else. An id addressed through the wrong section is rejected with a 404, so deleting a memory can never quietly delete a shopping list instead, and an assistant tidying up your notes can never touch the rules you wrote for it.

GET /mcp/searchBrain

Search your Brain entries. Same plain substring match over title/text/category as /mcp/searchTasks, restricted to your brief, rules and memory entries.

Scope: read

ParameterTypeRequiredDescription
qstringNoSearch text matched against title/text/category; omitted means the most recently created entries
categorystringNoOne of brief, rules, memory, to search just that one. Omitted means all three
pagenumberNo1-indexed page number, default 1

Returns the same shape as /mcp/searchTasks: { tasks: [{ id, title, category, text, highlights, links, createdDateMs, expiresAtMs }], totalPages }. 400 if category isn't one of the three.

GET /mcp/searchBrain?category=memory
Authorization: Bearer csk_live_...

GET /mcp/getBrain

Fetch one Brain entry.

Scope: read

ParameterInTypeRequiredDescription
idquery stringstringYesThe entry id, as returned by /mcp/searchBrain

Returns { id, title, category, text, highlights, links, createdDateMs, expiresAtMs }. 404 if it doesn't exist or is a task rather than a Brain entry; 403 if you don't own it.

GET /mcp/getBrain?id=abc123
Authorization: Bearer csk_live_...

POST /mcp/addBrain

Record a new Brain entry.

Scope: write

FieldTypeRequiredDescription
titlestringYesMax 40 characters
categorystringNoOne of brief, rules, memory; defaults to memory, the category an assistant writes for itself
textstringYesMax 100 lines, 600 characters per line; whole record capped at 10 KB
durationstringNoOne of permanent, 1h, 1d, 1w; defaults to permanent

Returns { id }, the new entry's id.

POST /mcp/addBrain
Authorization: Bearer csk_live_...
Content-Type: application/json

{
  "title": "Deploy target",
  "category": "memory",
  "text": "This project deploys to europe-west3, not us-central1."
}

POST /mcp/updateBrain

Revise a Brain entry you own. Append-only, same as /mcp/updateTask, and with no history endpoint either.

Scope: write

FieldTypeRequiredDescription
idstringYesId of the entry to revise
titlestringYesMax 40 characters
categorystringNoOne of brief, rules, memory. Omitted keeps the entry's current category, so an edit never moves it out of your Brain
textstringYesMax 100 lines, 600 characters per line; whole record capped at 10 KB
durationstringNoOne of permanent, 1h, 1d, 1w; defaults to permanent

Returns { id }, the new revision's id. 404 if the entry doesn't exist or is a task; 403 if you don't own it.

POST /mcp/updateBrain
Authorization: Bearer csk_live_...
Content-Type: application/json

{
  "id": "abc123",
  "title": "Deploy target",
  "text": "This project deploys to europe-west3. Region is set in functions/index.js."
}

POST /mcp/deleteBrain

Delete a Brain entry you own. Not a hard delete, same soft-cease as /mcp/deleteTask.

Scope: write

FieldTypeRequiredDescription
idstringYesId of the entry to delete

Returns { success: true }. 404 if the entry doesn't exist or is a task; 403 if you don't own it.

POST /mcp/deleteBrain
Authorization: Bearer csk_live_...
Content-Type: application/json

{
  "id": "abc123"
}

Pennies

Pennies is your own private log of investment orders: crypto, stocks, ETFs, bonds, commodities and CFDs, with a portfolio summary built from them. Like Tasks, it is private-only, never appears in cheat search, and has no revision history. Besides logging an order, you can correct one, void it and, for a limit order, confirm it filled.

These endpoints were deliberately left out of this API while Pennies existed only in the browser, on the grounds that a financial log should not be reachable by a key at all. They are here now on the same terms as everything else: a read-only key can read your orders but cannot change them, so issuing read-only keys is still how you keep this section untouchable by an integration.

GET /mcp/searchPennies

List your own logged orders, newest first. Not a text search, since an order has no body to match on, just the same type and asset filters the Orders tab uses.

Scope: read

ParameterTypeRequiredDescription
itemTypestringNoOne of crypto, stock, etf, bond, commodity, cfd, other
assetNamestringNoRestrict to one asset, for example BTC. Applied within itemType when both are given
pagenumberNo1-indexed page number, default 1

Returns { pennies: [{ id, side, itemType, orderKind, status, assetName, platform, currency, quantity, pricePerUnit, rewardQuantity, fees, tax, date, notes, walletId, createdDateMs, filledDateMs }], totalPages, filters }. side is one of buy, sell, stake, unstake, reward; the last three are crypto only.

GET /mcp/searchPennies?itemType=crypto&assetName=BTC
Authorization: Bearer csk_live_...

GET /mcp/getPennySummary

Your portfolio, derived from those orders. One holding per asset, with cost basis on the moving-average method, so each sell is measured against the average cost at the moment it happened. If you have set a base currency on your user page, every figure also comes back converted into it, with historical costs converted at the rate on each order's own trade date.

Scope: read

ParameterTypeRequiredDescription
taxYearnumberNoRestrict to one tax year, named by the calendar year it starts in. Boundaries follow your country, so a UK tax year runs 6 April to 5 April. Holdings come back as they stood at that year end, while realized profit, staking income and fees cover only that year

Returns { holdings: [{ itemType, assetName, currency, currencies, remainingQty, stakedQty, rewardQty, avgCostPerUnit, costBasisRemaining, realizedPL, feesPaid, netCashFlow, ledger }], baseCurrency, taxYears, taxYear, reconciliation }. reconciliation pairs each asset's logged quantity against a balance you have recorded by hand, as { itemType, assetName, loggedQty, actualQty, driftQty, updatedAtMs }, and is null when a taxYear is applied. A holding traded in more than one currency reports currency: null and returns only the base-currency figures. A negative remainingQty means orders are missing from the log.

GET /mcp/getPennySummary?taxYear=2025
Authorization: Bearer csk_live_...

POST /mcp/addPenny

Log an order.

Scope: write

FieldTypeRequiredDescription
sidestringYesbuy or sell for a trade. stake, unstake and reward are crypto only
itemTypestringYesThe instrument type
assetNamestringYesTicker or short name, max 20 chars
currencystringYesISO 4217 code the order settled in
quantitynumberYesUnits, greater than 0
pricePerUnitnumberFor tradesPrice per unit. Optional and normally 0 for stake and unstake, except an unstake sent with alsoSell, where it is the sell price. On a reward it is the optional value at receipt, reported as staking income and never treated as cost
orderKindstringNomarket (default) or limit. A limit order starts pending and stays out of the summary until confirmed
rewardQuantitynumberNoUnstake only: extra units the staking product paid out
platformstringNoMax 30 chars
walletIdstringNoCrypto only, max 100 chars. Dropped for other types
fees / taxnumberNoTotals for this order, default 0
datenumberNoTrade date in milliseconds. Backdate it to the real date, which is what the currency conversion uses
notesstringNoMax 200 chars
alsoStakebooleanNoCrypto market buy only. Also logs a stake of the same units, for platforms that buy the coin as you stake it. Fees and tax go on the buy
alsoSellbooleanNounstake only. Also logs a market sell of everything the unstake released (quantity plus rewardQuantity) at pricePerUnit. Fees and tax go on the sell

Returns { id }, or { id, stakeId } / { id, sellId } when alsoStake / alsoSell wrote a pair. Both orders of a pair are saved together or not at all. The Free-tier order cap does not apply here, since write access already requires Pro.

POST /mcp/addPenny
Authorization: Bearer csk_live_...
Content-Type: application/json

{
  "side": "buy",
  "itemType": "crypto",
  "assetName": "BTC",
  "currency": "GBP",
  "quantity": 0.05,
  "pricePerUnit": 48000,
  "fees": 1.5,
  "platform": "Revolut",
  "date": 1767225600000
}

POST /mcp/updatePenny

Correct an order you own. Send only the fields that change: anything you leave out keeps the value logged. Any field /mcp/addPenny takes can be changed, apart from alsoStake and alsoSell. The edit is saved as a new revision, so the order comes back with a new id, though no revision history is exposed. A limit order already confirmed filled stays filled.

Scope: write

FieldTypeRequiredDescription
idstringYesId of the order to correct
Any /mcp/addPenny fieldNoThe new value. Send an empty string to clear platform, walletId or notes

Returns the revised order, in the same shape as an entry in /mcp/searchPennies, with its new id. 404 if the order doesn't exist; 403 if you don't own it; 409 if it has already been edited or voided, in which case search again for the current version.

POST /mcp/updatePenny
Authorization: Bearer csk_live_...
Content-Type: application/json

{
  "id": "abc123",
  "fees": 2.25
}

POST /mcp/fillPenny

Confirm that a pending limit order executed, which is what lets it count toward your portfolio. Anything you omit keeps the value originally logged.

Scope: write

FieldTypeRequiredDescription
idstringYesId of the pending order
pricePerUnitnumberNoThe actual fill price, if it differed from the target
fees / taxnumberNoActual amounts, if they differed
datenumberNoActual execution date in milliseconds

Returns { success: true }. 400 if the order is not pending; 404 if it doesn't exist; 403 if you don't own it.

POST /mcp/fillPenny
Authorization: Bearer csk_live_...
Content-Type: application/json

{
  "id": "abc123",
  "pricePerUnit": 47850
}

POST /mcp/voidPenny

Void an order you own, or cancel one still pending. Not a hard delete, same soft-cease as /mcp/deleteCheat, but it is the only way an order is ever removed and it cannot be undone from the API.

Scope: write

FieldTypeRequiredDescription
idstringYesId of the order to void

Returns { success: true }. 404 if the order doesn't exist; 403 if you don't own it.

POST /mcp/voidPenny
Authorization: Bearer csk_live_...
Content-Type: application/json

{
  "id": "abc123"
}

Projects

Projects is your Kanban board: projects are the boards, and each card belongs to one of them and carries the status that decides its column. Like Tasks it never appears in cheat search and has no revision history, even though editing a card is append-only underneath. There is no done column: a finished card is deleted and leaves the board.

Unlike every other section here, a project can be shared with other people, so these endpoints are not limited to boards you created. Boards shared with you come back from /mcp/searchProjects alongside your own, each with a role saying what you may do on it: owner, editor (add, edit, move and delete cards, including ones other people added) or viewer (read-only, so the write endpoints return 403).

Four things the browser can do that these endpoints deliberately can't. Renaming a project has no integration asking for it. Deleting a whole project deletes every card on it at once, which is too much reach for a call that can be made by mistake, so cards are deleted one at a time here. Card sharing hands a card to people outside your account, which belongs on the page where you can see the allow-list. And inviting someone to a project emails a person a link to a whole board, which is not something an API key should be able to do on your behalf, so invites and members are managed on the Projects page only.

GET /mcp/searchProjects

List your projects. Takes no parameters.

Scope: read

Returns { projects: [{ id, name, createdDateMs, role, ownerEmail, memberCount }] }, name-sorted, covering your own boards and any shared with you. role is owner, editor or viewer. The id is what the card endpoints below take as projectID.

GET /mcp/searchProjects
Authorization: Bearer csk_live_...

POST /mcp/addProject

Create a new project to put cards on.

Scope: write

FieldTypeRequiredDescription
namestringYesMax 30 characters

Returns { id, name, createdDateMs }.

POST /mcp/addProject
Authorization: Bearer csk_live_...
Content-Type: application/json

{
  "name": "Website rebuild"
}

GET /mcp/searchProjectTasks

List the cards on your board. Returns the whole board at once rather than a page of it, since a board is meant to be read whole, so narrow it when you only want part.

Scope: read

ParameterTypeRequiredDescription
qstringNoSearch text matched against title/text; omitted means every card
projectIdstringNoRestrict to one project's cards; omitted means every project you can see, shared boards included

Returns { tasks: [{ id, title, projectID, status, type, text, highlights, links, createdDateMs }] }. status is one of open, in-progress, in-review. type is one of feature, bug, improvement, chore, docs, research, or null for a card with no type.

GET /mcp/searchProjectTasks?projectId=proj123
Authorization: Bearer csk_live_...

GET /mcp/getProjectTask

Fetch one card.

Scope: read

ParameterInTypeRequiredDescription
idquery stringstringYesThe card id, as returned by /mcp/searchProjectTasks

Returns { id, title, projectID, status, text, highlights, links, createdDateMs }. 404 if the card doesn't exist; 403 if you have no access to the board it sits on.

GET /mcp/getProjectTask?id=abc123
Authorization: Bearer csk_live_...

POST /mcp/addProjectTask

Add a card to one of your projects.

Scope: write

FieldTypeRequiredDescription
titlestringYesMax 40 characters
projectIDstringYesId of the project to add it to, from /mcp/searchProjects
textstringYesMax 100 lines, 600 characters per line; whole record capped at 10 KB
statusstringNoOne of open, in-progress, in-review; defaults to open
typestringNoOne of feature, bug, improvement, chore, docs, research. Omitted leaves the card with no type

Returns { id }, the new card's id. 404/403/400 if projectID isn't a live project you own or have editor access to.

POST /mcp/addProjectTask
Authorization: Bearer csk_live_...
Content-Type: application/json

{
  "title": "Rewrite the pricing page",
  "projectID": "proj123",
  "text": "Copy is stale and the annual price is wrong.",
  "status": "open",
  "type": "bug"
}

POST /mcp/updateProjectTask

Revise a card on a board you can write to, which on a shared board includes cards other people added. Append-only, same as /mcp/updateTask: the old card is ceased and the returned id is a new one. Use /mcp/moveProjectTask instead when all you want is to change the column, since that keeps the card's id.

Scope: write

FieldTypeRequiredDescription
idstringYesId of the card to revise
titlestringYesMax 40 characters
projectIDstringYesProject the card belongs to. A different one moves the card to another board
textstringYesMax 100 lines, 600 characters per line; whole record capped at 10 KB
statusstringNoOne of open, in-progress, in-review. Omitted keeps the card in its current column
typestring or nullNoOne of feature, bug, improvement, chore, docs, research. Omitted keeps the card's current type; null clears it

Returns { id }, the new revision's id. 404 if the card doesn't exist; 403 if you only have view access to its board; 409 if it moved to another board mid-request.

POST /mcp/updateProjectTask
Authorization: Bearer csk_live_...
Content-Type: application/json

{
  "id": "abc123",
  "title": "Rewrite the pricing page",
  "projectID": "proj123",
  "text": "Copy is stale and the annual price is wrong. Draft is in the shared doc."
}

POST /mcp/moveProjectTask

Move a card to another column. A direct update rather than a revision, so the card keeps its id, exactly like dragging it on the board.

Scope: write

FieldTypeRequiredDescription
idstringYesId of the card to move
statusstringYesOne of open, in-progress, in-review

Returns { id, status }. 400 for an unknown status; 404 if the card doesn't exist; 403 if you only have view access to its board.

POST /mcp/moveProjectTask
Authorization: Bearer csk_live_...
Content-Type: application/json

{
  "id": "abc123",
  "status": "in-progress"
}

POST /mcp/deleteProjectTask

Delete a card on a board you can write to, which is also how a finished card leaves the board. Not a hard delete, same soft-cease as /mcp/deleteCheat. On a shared board it disappears for everyone on it.

Scope: write

FieldTypeRequiredDescription
idstringYesId of the card to delete

Returns { success: true }. 404 if the card doesn't exist; 403 if you only have view access to its board.

POST /mcp/deleteProjectTask
Authorization: Bearer csk_live_...
Content-Type: application/json

{
  "id": "abc123"
}

Hours

Hours is your own private log of time worked and what it earned. A project carries an hourly rate and, optionally, the company the work is billed to; an entry is one block of time on one project, on one day. Like Pennies it is private-only, never appears in cheat search, and has no visible revision history. These are a separate list from the boards under Projects.

Every entry keeps the rate it was logged at, so a project's rate only ever prices future work, and correcting an entry's description never reprices it. You can add a project here, but changing its rate, archiving it and deleting it are done on the Hours page: a rate change reprices everything you log from then on, and deleting a project takes every hour on it with it.

GET /mcp/searchHourProjects

List the projects you log time against, sorted by company then name.

Scope: read

ParameterTypeRequiredDescription
includeArchivedstringNo1 to include archived projects. Their hours count toward every total either way

Returns { projects: [{ id, name, company, hourlyRate, currency, archived, createdDateMs }] }.

GET /mcp/searchHourProjects
Authorization: Bearer csk_live_...

POST /mcp/addHourProject

Create a project to log time against.

Scope: write

FieldTypeRequiredDescription
namestringYesMax 40 chars
companystringNoWho the work is billed to, max 40 chars
hourlyRatenumberYesDefault rate, 0 to 100000, in currency
currencystringYesISO 4217 code

Returns the new project, in the same shape as an entry in /mcp/searchHourProjects. The Free-tier project cap does not apply here, since write access already requires Pro.

POST /mcp/addHourProject
Authorization: Bearer csk_live_...
Content-Type: application/json

{
  "name": "Website rebuild",
  "company": "Acme Ltd",
  "hourlyRate": 60,
  "currency": "GBP"
}

GET /mcp/searchHourEntries

List your logged time, newest first, 30 to a page. Entries on archived projects are included.

Scope: read

ParameterTypeRequiredDescription
projectIdstringNoRestrict to one project
companystringNoRestrict to one company, matched exactly
pagenumberNo1-indexed page number, default 1

Returns { entries: [{ id, projectID, projectName, company, date, minutes, description, hourlyRate, currency, amount, rateOverridden, createdDateMs }], totalPages, filters, projects }. date is UTC midnight of the day worked, in milliseconds; amount is what the entry earned. filters reports the filter actually applied and the companies and projects available to filter on.

GET /mcp/searchHourEntries?company=Acme%20Ltd
Authorization: Bearer csk_live_...

GET /mcp/getHoursSummary

Time worked and money earned today, this week, this month, this year and all time, plus a breakdown of up to 24 rows. Weeks run Monday to Sunday, and every boundary is UTC. If you have set a base currency on your user page, each tally is also converted into it at the rate on the day the work was done.

Scope: read

ParameterTypeRequiredDescription
granularitystringNoWhat each breakdown row covers: day (default), week, month or year
projectIdstringNoRestrict to one project
companystringNoRestrict to one company, matched exactly

Returns { baseCurrency, granularity, filters, projects, tallies: { day, week, month, year, all }, breakdown, truncated }, where each tally is { minutes, hours, amounts: [{ currency, amount }], baseAmount, entryCount }. baseAmount is null rather than partial whenever any entry in that tally could not be converted.

GET /mcp/getHoursSummary?granularity=week
Authorization: Bearer csk_live_...

POST /mcp/addHourEntry

Log a block of time on one project. Priced at the project's current rate unless you give one, and it keeps that rate if the project's rate changes later.

Scope: write

FieldTypeRequiredDescription
projectIDstringYesId of the project, from /mcp/searchHourProjects
datestring or numberNoThe day worked, as YYYY-MM-DD or milliseconds. Stored as UTC midnight of that day. Defaults to today (UTC)
hours / minutesnumberYesWhole hours plus minutes. Together a multiple of 15 minutes, from 15 minutes to 24 hours
descriptionstringYesWhat was done, max 200 chars
hourlyRatenumberNoA rate for this entry alone, in the project's currency

Returns the new entry, in the same shape as one in /mcp/searchHourEntries. The Free-tier entry cap does not apply here, since write access already requires Pro.

POST /mcp/addHourEntry
Authorization: Bearer csk_live_...
Content-Type: application/json

{
  "projectID": "abc123",
  "date": "2026-10-02",
  "hours": 2,
  "minutes": 30,
  "description": "Checkout page layout"
}

POST /mcp/updateHourEntry

Correct an entry you own. Send only the fields that change: anything you leave out keeps what was logged. hours and minutes restate the whole duration together, so hours alone means exactly that many hours. The entry keeps the rate it was logged at unless you send hourlyRate or move it to another project, which prices it at that project's current rate. Saved as a new revision, so the entry comes back with a new id, though no revision history is exposed.

Scope: write

FieldTypeRequiredDescription
idstringYesId of the entry to correct
Any /mcp/addHourEntry fieldNoThe new value

Returns the revised entry, with its new id. 404 if the entry doesn't exist; 403 if you don't own it; 409 if it has already been edited or deleted, in which case search again for the current version.

POST /mcp/updateHourEntry
Authorization: Bearer csk_live_...
Content-Type: application/json

{
  "id": "def456",
  "description": "Checkout page layout and review"
}

POST /mcp/deleteHourEntry

Delete an entry you own. Not a hard delete, same soft-cease as /mcp/deleteCheat, but the time stops counting toward every total and every timesheet export.

Scope: write

FieldTypeRequiredDescription
idstringYesId of the entry to delete

Returns { success: true }. 404 if the entry doesn't exist; 403 if you don't own it.

POST /mcp/deleteHourEntry
Authorization: Bearer csk_live_...
Content-Type: application/json

{
  "id": "def456"
}

Dues

Dues is your own private record of money other people owe you. A payer is a client or a tenant, a service is one recurring thing you bill them for, and a due is one amount owed on one date. Like Pennies it is private-only, never appears in cheat search, and has no visible revision history.

Two things about it shape these endpoints. Each new due is priced at whatever the previous one for that service was priced at, not at the figure on the service, so editing a raised but unpaid due is what changes a payer's price from then on. And a service bills either on a fixed calendar date, whether or not the last one was paid, or only once the last one has been paid, which is what markDuePaid can quietly set in motion.

Three things the browser can do that these endpoints deliberately cannot. Freezing a payer stops billing a real client, and sending a reminder puts an email in front of them, so both stay on the page where you can see who you are doing it to. Editing a due reprices every future one, which is too much consequence for a call made in passing. All three are managed on the Dues page only.

Writing to Dues needs a Pro plan, the same as every other write endpoint here. Reading it does not.

GET /mcp/searchDuePayers

The people and companies who owe you money, sorted by company then name. Use it to turn a name into an id, or to check somebody is not already on the list.

Scope: read

ParameterTypeRequiredDescription
qstringNoSubstring match over name and company, case-insensitive
includeArchivedstringNo1 to include archived payers, who are filed away but still owed money

Returns { payers: [{ id, name, company, email, paymentMethod, paymentHandle, currency, notes, frozen, frozenAtMs, frozenReason, archived, createdDateMs }] }.

GET /mcp/searchDuePayers?q=acme
Authorization: Bearer csk_live_...

GET /mcp/searchDueServices

The recurring things you bill your payers for, soonest next date first. Each carries how many charges it currently has unpaid, which is the thing a bare nextDueDate cannot tell you.

Scope: read

ParameterTypeRequiredDescription
payerIdstringNoRestrict to one payer
qstringNoSubstring match over the service label, case-insensitive
includeArchivedstringNo1 to include archived services

Returns { services: [{ id, payerID, label, amount, taxRate, currency, cycle, schedule, anchorDay, nextDueDate, outstandingCount, oldestUnpaidDueDate, frozen, archived, createdDateMs }] }. A service on the onPayment schedule will not bill again until its open charge is paid, whatever nextDueDate says.

GET /mcp/searchDueServices
Authorization: Bearer csk_live_...

POST /mcp/addDuePayer

Add a person or company who owes you money. This is the one write on this API that stores another person's details, so it is worth being deliberate about what goes in it.

Scope: write

ParameterTypeRequiredDescription
namestringYesWho owes the money, max 60 chars
companystringNoTheir company, max 60 chars. Also groups the list
emailstringNoWhere a payment reminder would go, max 120 chars. A payer without one simply cannot be sent a reminder
paymentMethodstringNoHow you want this payer to pay you, max 40 chars, e.g. PayPal or Monzo. Your own detail, not theirs, recorded per payer because different clients pay different ways
paymentHandlestringNoWhere the money goes, max 120 chars: your PayPal address, your Monzo or Revolut handle, an account reference. Stored verbatim, so an @ or _ survives
currencystringNoISO 4217 code their charges default to. Falls back to your account's base currency; a 400 names the field when neither is set
notesstringNoFor your own reference, never emailed, max 200 chars

Returns the payer, same shape as one entry of searchDuePayers.

POST /mcp/addDuePayer
Authorization: Bearer csk_live_...
Content-Type: application/json

{
  "name": "Bob Smith",
  "company": "Acme Ltd",
  "email": "[email protected]"
}

POST /mcp/addDueService

Set up a recurring thing a payer is billed for. This is what makes charges appear on their own later, so it is the most consequential call in this section.

A nextDueDate in the past raises every charge owed since then immediately, and the response says how many as raised. A monthly service back-dated a year creates twelve unpaid charges on the spot. That is how you record something you have already been billing for a while; a future date raises nothing.

Scope: write

ParameterTypeRequiredDescription
payerNamestringEitherWho is billed, matched exactly and case-insensitively against a payer's name or company. An ambiguous name is a 400 rather than a guess
payerIdstringEitherWho is billed, by id. Takes precedence over payerName
labelstringYesWhat it is, max 60 chars. Becomes the description of the first charge
amountnumberYesWhat it costs each time, before VAT. Only the seed price: every later charge copies the one before it
taxRatenumberNoVAT percentage on top of amount, 0 to 100. Omitting it and sending 0 are different
currencystringNoISO 4217 code. Defaults to the payer's own
cyclestringYesOne of monthly, quarterly, yearly, none. none is a one-off
schedulestringNofixed (default) bills on its date whether or not the last one was paid, so arrears stack up. onPayment raises the next one only once the current one is marked paid. Ignored for a none cycle
nextDueDatenumberYesWhen the first charge falls due, as a millisecond timestamp, normalized to UTC midnight. Its day of the month becomes the anchor the cycle bills on

Returns the service, same shape as one entry of searchDueServices, plus raised, the number of charges created on the spot.

POST /mcp/addDueService
Authorization: Bearer csk_live_...
Content-Type: application/json

{
  "payerName": "Acme Ltd",
  "label": "Website hosting",
  "amount": 120,
  "taxRate": 20,
  "cycle": "yearly",
  "schedule": "onPayment",
  "nextDueDate": 1789171200000
}

GET /mcp/searchDues

List what people currently owe you, most pressing first: unpaid before paid, oldest first within each. Not a text search, since the useful filters are who owes it and whether it is late.

Scope: read

ParameterTypeRequiredDescription
payerIdstringNoRestrict to one payer. The payer list comes back on every response, so one call gets you the ids
statusstringNoOne of unpaid, paid, or overdue. Overdue is the narrower slice of unpaid that is past its due date
pagenumberNo1-indexed page number, default 1

Returns { dues: [{ id, payerID, payerName, payerCompany, payerEmail, payerFrozen, serviceID, serviceLabel, description, amount, taxRate, taxAmount, grossAmount, currency, dueDate, status, overdue, daysOverdue, paidDate, paidAmount, reminderSentAtMs, reminderCount, createdDateMs }], totalPages, filters, payers }. amount is before tax and grossAmount is what is owed. overdue and daysOverdue are worked out from the date at the moment you ask, so they are never stale.

GET /mcp/searchDues?status=overdue
Authorization: Bearer csk_live_...

GET /mcp/getDue

One due in full by id, including the payer's email and which service raised it.

Scope: read

ParameterTypeRequiredDescription
idstringYesThe due's id

Returns the same shape as one entry of searchDues. 404 if it does not exist, 403 if it is not yours.

GET /mcp/getDue?id=abc123
Authorization: Bearer csk_live_...

GET /mcp/getDuesSummary

Your balances rather than the raw list. Money comes back per currency, and additionally converted into your base currency only when every due in that tally could be converted, so a baseAmount of null means mixed currencies rather than zero. Conversion uses the rate on each due's own date.

Scope: read

ParameterTypeRequiredDescription
payerIdstringNoRestrict to one payer. Omit for everybody

Returns { baseCurrency, filters, payers, tallies: { outstanding, overdue, upcoming, paidThisYear }, payerBreakdown, truncated }. Each tally is { amounts: [{ currency, amount }], baseAmount, dueCount }. upcoming is the next 30 days. Each payerBreakdown row adds oldestUnpaidDate, daysOverdue, overdueCount and frozen.

GET /mcp/getDuesSummary
Authorization: Bearer csk_live_...

POST /mcp/addDue

Record a one-off amount somebody owes you. Recurring charges are set up as services on the Dues page and raise themselves, so this is for one-offs and for catching up on something missed.

Scope: write

ParameterTypeRequiredDescription
payerNamestringEitherWho owes it, matched exactly and case-insensitively against a payer's name or company. A name matching more than one payer is a 400 rather than a guess
payerIdstringEitherWho owes it, by id. Takes precedence over payerName
descriptionstringYesWhat it is for, max 200 chars
amountnumberYesThe amount owed before tax, 0 to 10000000
taxRatenumberNoVAT percentage on top of amount, 0 to 100. Omitting it and sending 0 are different: only one of them prints a VAT line
currencystringNoISO 4217 code. Defaults to the payer's own currency
dueDatenumberYesWhen it falls due, as a millisecond timestamp. Normalized to UTC midnight of that day

Returns { id }.

POST /mcp/addDue
Authorization: Bearer csk_live_...
Content-Type: application/json

{
  "payerName": "Acme Ltd",
  "description": "Website hosting 2027",
  "amount": 120,
  "taxRate": 20,
  "dueDate": 1806019200000
}

POST /mcp/markDuePaid

Record that a due has been settled. If it came from a service that recurs only after payment, this also raises the next one straight away, dated one cycle on from this one's own due date and priced at whatever this one was priced at. That makes it more than a status change, so use it only when the money has actually arrived.

Scope: write

ParameterTypeRequiredDescription
idstringYesThe due's id
paidDatenumberNoWhen it was paid, as a millisecond timestamp. Defaults to today
paidAmountnumberNoWhat actually landed, if it differed from the gross owed. Defaults to the full gross

Returns { success: true, id, rolledChildID }, where rolledChildID is the successor this raised, or null when nothing recurred. 400 if it is already marked paid.

POST /mcp/markDuePaid
Authorization: Bearer csk_live_...
Content-Type: application/json

{
  "id": "abc123"
}

Connecting an MCP client

Everything above documents the REST API. To instead point Claude Code, Claude Desktop, or the Claude mobile app at Cheatsheet, including a sign-in flow that needs no key pasted anywhere, see Connect an MCP Client.