Documentation

API & MCP documentation

Everything needed to authenticate, call every endpoint, validate payloads, handle responses, and connect AI clients.

Overview

FocusPilot exposes two programmatic surfaces. Both resolve every credential to its owner and workspace on the server, and both stop immediately when the Developers module is disabled.

REST API

For scripts, integrations, mobile workflows, Zapier, Make, n8n, and custom apps.

https://focuspilot.online/api/v1

Credential: API key beginning fpk_…

MCP server

For ChatGPT, Claude, Codex, Cursor, VS Code, and other Model Context Protocol clients.

https://focuspilot.online/api/mcp

Credential: OAuth sign-in, or an MCP token beginning fpm_…

FocusPilot REST API (v1)

Programmatic access to a FocusPilot workspace — tasks, areas, goals, habits, notes, checklists, reminders, focus sessions, energy, and (when the modules are enabled) finance, trading, and bookings.

  • Base URL: https://focuspilot.online/api/v1
  • Auth: Authorization: Bearer fpk_… (an API key from the Developers page)
  • Format: JSON in, JSON out, UTF-8. Send Content-Type: application/json on requests with a body.
Looking for AI-agent access instead? The same workspace is exposed over the Model Context Protocol — see `docs/MCP.md`. MCP uses a different token kind (fpm_…); API keys (fpk_…) are not valid on the MCP server and vice-versa.

Getting an API key

  1. In the app, open Settings → Modules and enable Developers.
  2. Go to the Developers page → API keys → Create.
  3. Give it a name, an optional expiry, and toggle Read/Write for each resource group (these become the key's scopes).
  4. Copy the token — it is shown once and never again. Only a SHA-256 hash is stored, so a lost key can't be recovered, only replaced.

Keys can be renamed, re-scoped, revoked, or deleted at any time from the same page. Revocation and scope changes take effect on the very next request.


Authentication

Send the key as a bearer token on every request:

Authorization: Bearer fpk_1a2b3c4d…

The following are rejected before any handler runs:

ConditionStatuserror.code
Missing / malformed / unknown token401UNAUTHORIZED
An MCP token (fpm_…) used here401UNAUTHORIZED
Revoked token401UNAUTHORIZED
Expired token401UNAUTHORIZED
Token owner no longer a member of the workspace401UNAUTHORIZED
Developers module disabled for the workspace (global kill switch)403FORBIDDEN
Token lacks the scope the endpoint needs403FORBIDDEN (Missing scope: <resource>:<level>)
More than 120 requests in a rolling minute for one key429RATE_LIMITED

Turning the Developers module off in Settings instantly disables every key and MCP token in the workspace — a single switch to cut all programmatic access.


Conventions

Response envelope

Every response is one of:

jsonc
// success
{ "data": <payload> }

// failure
{ "error": { "code": "VALIDATION", "message": "Invalid input", "fields": { "title": ["Title is required"] } } }

fields is present only on VALIDATION errors (a map of field path → messages).

Error codes

codeHTTPMeaning
UNAUTHORIZED401Missing/invalid/expired/revoked token
FORBIDDEN403Module disabled, or missing scope
NOT_FOUND404No such record in your workspace (also returned for another tenant's ids — existence is never leaked)
VALIDATION400Body/query failed schema validation (fields explains)
CONFLICT409Optimistic-concurrency version mismatch (see below)
RATE_LIMITED429Per-key rate limit exceeded
INTERNAL500Unexpected server error

Optimistic concurrency (version)

Mutable top-level records carry an integer version. Every PATCH for one of those records must send the version returned by the last read. The write succeeds only if it still matches; otherwise you get 409 `CONFLICT` — someone (or another integration) changed the record since you read it. Recover by re-GETting the record and retrying with the fresh version. On success the response contains the record with an incremented version.

Which resources are versioned: tasks, areas, goals, habits, notes, checklists, reminders, clients, invoices, expenses, payables, trading accounts, trades, and booking profiles. Child operations (checklist items, goal milestones, comments, habit entries), action endpoints, PUT /trading/journal, and all DELETE requests do not take a version.

Deletion behavior

Deleting a top-level domain record (task, area, goal, habit, note, checklist, reminder, finance record, trade/account/setup, or booking profile) is a soft delete: it stops appearing in lists and reads. Checklist items, goal milestones, and custom unused instruments are permanently removed. Task comments are soft deleted. Every successful delete returns { "data": { "id": "..." } }, except milestone deletion, which returns the refreshed GoalDTO. IDs are never reused.

Success status codes

  • GET, PATCH, PUT, DELETE, and action-style POST requests return 200.
  • Resource-creation POST requests return 201, including external notifications, comments, checklist items, milestones, and goal progress logs.
  • Success bodies always use the { "data": ... } envelope; there are no empty 204 responses.

Money and time

  • All money is integer cents (amountCents, unitAmountCents, hourlyRateCents, feesCents, startingBalanceCents, …). Never floats.
  • Timestamps are ISO 8601 strings (2026-07-17T09:30:00.000Z). Fields suffixed At (e.g. dueAt, remindAt, openedAt) are absolute instants. Fields named …Date (e.g. issueDate, dueDate, expenseDate) are calendar dates in YYYY-MM-DD form.

Lists & pagination

List endpoints return the full set for the workspace (optionally filtered by query params). There is no cursor pagination yet. GET /focus-sessions?range=recent is capped at the latest 50 sessions; the other documented lists return every matching record. Filter server-side with the documented query parameters where available.

Rate limits

120 requests per rolling 60-second window per key. Over the limit → 429. Design integrations to be event-driven or to poll modestly.


Scopes

A scope is <resource>:read or <resource>:write. Write implies read — a key with tasks:write can also GET tasks. Assign the narrowest set that works.

ResourceRead scopeWrite scopeCoversNeeds module
taskstasks:readtasks:writeTasks, subtasks, task comments—
areasareas:readareas:writeAreas (projects/contexts)—
checklistschecklists:readchecklists:writeChecklists + items—
goalsgoals:readgoals:writeGoals, milestones, progress—
habitshabits:readhabits:writeHabits + daily entries—
notesnotes:readnotes:writeNotes—
remindersreminders:readreminders:writeReminders—
notifications(none)notifications:writeDeliver external notifications to this account—
focusfocus:readfocus:writeFocus/pomodoro sessions—
energyenergy:readenergy:writeDaily energy logs—
financefinance:readfinance:writeClients, invoices, expenses, payables, ledgerFinance
tradingtrading:readtrading:writeTrades, accounts, instruments, setups, journalTrading
bookingsbookings:readbookings:writeBooking profiles + received bookingsBookings
analyticsanalytics:read(none)Productivity analytics + estimation insights—

How gating actually works: the v1 routes are gated by the key's scope plus the workspace-wide Developers kill switch. The finance/trading/booking services underneath behave exactly as they do in-app for the resolved workspace; if you hold, say, finance:* on the key, those endpoints work. Keep the key's scopes aligned with the modules the workspace actually uses.


Quickstart

Check your identity and scopes:

bash
curl -s https://focuspilot.online/api/v1/me \
  -H "Authorization: Bearer fpk_YOUR_KEY"
json
{
  "data": {
    "workspace": { "id": "clw...", "name": "Aiden's Workspace", "slug": "aiden" },
    "user": { "email": "aiden@example.com", "name": "Aiden" },
    "scopes": ["tasks:write", "goals:read", "analytics:read"]
  }
}

List open tasks:

bash
curl -s "https://focuspilot.online/api/v1/tasks?status=todo" \
  -H "Authorization: Bearer fpk_YOUR_KEY"

Create a task:

bash
curl -s -X POST https://focuspilot.online/api/v1/tasks \
  -H "Authorization: Bearer fpk_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{"title":"Ship the Q3 report","priority":"high","dueAt":"2026-07-20T17:00:00.000Z"}'

Update with optimistic concurrency, handling a 409:

bash
# read → get version: 3
curl -s -X PATCH https://focuspilot.online/api/v1/tasks/clw_task123 \
  -H "Authorization: Bearer fpk_YOUR_KEY" -H "Content-Type: application/json" \
  -d '{"status":"done","version":3}'
# → 200 with version:4, OR 409 CONFLICT if it changed since — re-GET and retry.

The same request with JavaScript fetch:

javascript
const response = await fetch("https://focuspilot.online/api/v1/tasks", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.FOCUSPILOT_API_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({ title: "Ship the Q3 report", priority: "high" }),
});

const payload = await response.json();
if (!response.ok) {
  throw new Error(`${payload.error.code}: ${payload.error.message}`);
}
console.log(payload.data);

Never embed an API key in browser JavaScript or a public mobile bundle. Call FocusPilot from a trusted server, automation platform secret store, or local script environment.


Endpoint reference

Each entry lists the scope required. Request-body tables list every field; req = required. Omitted optional fields are left unchanged (updates) or take their default (creates). nullable fields accept null to clear them.

Success response map

The table below identifies the exact value inside the { "data": ... } envelope. Full TypeScript definitions for every named DTO are included in the Response models section of the in-app documentation.

OperationsHTTPdata payload
GET /me200{ workspace: {id,name,slug}, user: {email,name}, scopes: string[] }
GET /tasks, POST /tasks, GET/PATCH /tasks/{id}200/201TaskDTO[] for list; otherwise TaskDTO
DELETE /tasks/{id}200{ id: string }
GET/POST /tasks/{id}/comments200/201CommentDTO[] / CommentDTO
DELETE /tasks/{id}/comments/{commentId}200{ id: string }
GET/POST /areas, PATCH /areas/{id}200/201AreaDTO[] / AreaDTO
DELETE /areas/{id}200{ id: string }
GET/POST /goals, GET/PATCH /goals/{id}200/201GoalDTO[] / GoalDTO
Milestone create/update/delete; goal progress create200/201Refreshed GoalDTO
DELETE /goals/{id}200{ id: string }
GET/POST /habits, PATCH /habits/{id}200/201HabitDTO[] / HabitDTO
POST /habits/{id}/entries200{ habitId, date, completed }
DELETE /habits/{id}200{ id: string }
GET/POST /notes, PATCH /notes/{id}200/201NoteDTO[] / NoteDTO
DELETE /notes/{id}200{ id: string }
GET/POST /checklists, PATCH /checklists/{id}200/201ChecklistDTO[] / ChecklistDTO
Checklist item create/update201/200ChecklistItemDTO
Checklist/item delete200{ id: string }
POST /checklists/{id}/items/reorder200{ ok: true }
GET/POST /reminders, PATCH /reminders/{id}200/201ReminderDTO[] / ReminderDTO
DELETE /reminders/{id}200{ id: string }
POST /notifications201{ delivery: { inApp, email, push } }
GET/POST /focus-sessions200/201FocusSessionDTO[] / FocusSessionDTO
GET /energy200EnergyLogDTO, EnergyLogDTO[], or null
POST /energy200EnergyLogDTO
GET /analytics200AnalyticsDTO
GET /analytics/overview200AnalyticsOverviewDTO
GET /insights/estimation200EstimationStatsDTO
Client/invoice/expense/payable list/create/update200/201Corresponding DTO array / DTO
Client/invoice/expense/payable delete200{ id: string }
GET /finance/summary200FinanceSummaryDTO
GET /finance/ledger200LedgerEntryDTO[]
Trade/account/instrument/setup list/create/update200/201Corresponding DTO array / DTO
Trade/account/instrument/setup delete200{ id: string }
GET /trading/summary200TradingSummaryDTO
GET/PUT /trading/journal200TradingJournalDTO[] / TradingJournalDTO
DELETE /trading/journal/{id}200{ id: string }
GET/POST /booking-profiles, GET/PATCH /booking-profiles/{id}200/201BookingProfileDTO[] / BookingProfileDTO
DELETE /booking-profiles/{id}200{ id: string }
GET /bookings200BookingDTO[]
POST /bookings/{id}/cancel200Updated BookingDTO

Identity

›GET /me

Scope: none (any valid key). Returns { workspace: {id,name,slug}, user: {email,name}, scopes: string[] }. Useful for verifying a key and discovering what it can do.


Tasks — scope tasks

›GET /tasks

Query params (all optional): status (todo|doing|done|archived), area (area id), parent (parent task id — returns its subtasks), search (title contains), includeDone (true/false), doneAfter (ISO instant — include done tasks completed at/after it). Returns TaskDTO[].

›POST /tasks — scope tasks:write

Creates a task. Returns the TaskDTO (201).

FieldTypeNotes
titlestringreq, 1–500 chars
notesstring | null≤ 20000
statusenumtodo|doing|done|archived
priorityenumlow|medium|high|urgent
areaIdcuid | null
goalIdcuid | null
parentTaskIdcuid | nullmakes this a subtask
dueAtISO instant | null
scheduledAtISO instant | null
plannedMinutesint 1–1440 | null
recurrenceRuleenum | nullsee recurrence rules
recurrenceEndISO instant | null

Example TaskDTO:

json
{
  "data": {
    "id": "clw_task123", "title": "Ship the Q3 report", "notes": null,
    "status": "todo", "priority": "high",
    "areaId": null, "goalId": null, "parentTaskId": null,
    "dueAt": "2026-07-20T17:00:00.000Z", "scheduledAt": null,
    "plannedMinutes": null, "actualMinutes": null, "completedAt": null,
    "recurrenceRule": null, "recurrenceSeriesId": null, "recurrenceEnd": null,
    "subtaskCount": 0, "doneSubtaskCount": 0, "sortOrder": 0, "version": 1
  }
}
›GET /tasks/{id} — scope tasks:read

One TaskDTO, or 404.

›PATCH /tasks/{id} — scope tasks:write

Same fields as create (all optional) plus actualMinutes (int ≥0), sortOrder (int), and `version` (req). Returns the updated TaskDTO; 409 on version mismatch.

›DELETE /tasks/{id} — scope tasks:write

Soft-deletes. Returns { id }.

›GET /tasks/{id}/comments — scope tasks:read

Returns CommentDTO[] for the task.

›POST /tasks/{id}/comments — scope tasks:write

Body: { body: string (1–5000) }. Returns the created CommentDTO.

›DELETE /tasks/{id}/comments/{commentId} — scope tasks:write

Deletes your comment. Returns { id }.


Areas — scope areas

›GET /areas

Returns all areas.

›POST /areas — scope areas:write
FieldTypeNotes
namestringreq, 1–80
colorenumarea color key, default indigo
iconstring | null≤ 40
parentAreaIdcuid | nullnesting
›PATCH /areas/{id} — scope areas:write

Fields above (optional) plus sortOrder (int) and `version` (req).

›DELETE /areas/{id} — scope areas:write

Goals — scope goals

›GET /goals

List goals with progress %, horizon, status, and milestone counts.

›POST /goals — scope goals:write
FieldTypeNotes
titlestringreq, 1–200
descriptionstring | null≤ 2000
areaIdcuid | null
horizonenumweek|month|quarter|year
progressTypeenumbinary|numeric|milestone
startDate / dueDateYYYY-MM-DD | null
targetValue / currentValuenumber | nullfor numeric goals
›GET /goals/{id} — scope goals:read

One goal in full detail, including milestones (with ids) and the progress log.

›PATCH /goals/{id} — scope goals:write

Fields above plus status (active|done|paused|archived) and `version` (req).

›DELETE /goals/{id} — scope goals:write
›POST /goals/{id}/milestones — scope goals:write
FieldTypeNotes
titlestringreq, 1–200
descriptionstring | null≤ 1000
startDate / dueDateYYYY-MM-DD | null
progressint 0–100
›PATCH /goals/{id}/milestones/{mid} — scope goals:write

Same fields (all optional). No version.

›DELETE /goals/{id}/milestones/{mid} — scope goals:write
›POST /goals/{id}/progress — scope goals:write

Body: { value: number, note?: string (≤500) | null }. Appends a progress-log entry.


Habits — scope habits

›GET /habits

Habits with current streak and completion %.

›POST /habits — scope habits:write
FieldTypeNotes
namestringreq, 1–80
descriptionstring | null≤ 500
colorenumarea color key
cadenceenumdaily|weekly|custom
targetPerPeriodint 1–50
›PATCH /habits/{id} — scope habits:write

Fields above plus archived (bool) and `version` (req).

›DELETE /habits/{id} — scope habits:write
›POST /habits/{id}/entries — scope habits:write

Toggle a day's completion. Body: { date: "YYYY-MM-DD", completed: boolean }.


Notes — scope notes

›GET /notes

Query (optional): search (title/body contains), area (area id).

›POST /notes — scope notes:write
FieldTypeNotes
titlestring≤ 200
bodystring≤ 100000
colorenumnote color
pinnedboolean
areaIdcuid | null
›PATCH /notes/{id} — scope notes:write

Fields above plus `version` (req).

›DELETE /notes/{id} — scope notes:write

Note colors: slate, indigo, violet, sky, emerald, amber, rose, teal, fuchsia.


Checklists — scope checklists

›GET /checklists

Checklists with their items.

›POST /checklists — scope checklists:write
FieldTypeNotes
titlestringreq, 1–120
colorenumnote color
resetCadenceenumnone|daily|weekly|monthly
resetWeekdayint 0–6 | nullfor weekly reset (0=Sun)
›PATCH /checklists/{id} — scope checklists:write

Fields above plus `version` (req).

›DELETE /checklists/{id} — scope checklists:write
›POST /checklists/{id}/items — scope checklists:write

Body: { title: string (1–300) }.

›PATCH /checklists/{id}/items/{itemId} — scope checklists:write

Body: { title?: string, completed?: boolean }.

›DELETE /checklists/{id}/items/{itemId} — scope checklists:write
›POST /checklists/{id}/items/reorder — scope checklists:write

Body: { orderedIds: string[] } (≤ 500 item ids in the desired order). Returns { ok: true }.


Reminders — scope reminders

›GET /reminders

Query (optional): includeCompleted (1), taskId (only reminders on a task).

›POST /reminders — scope reminders:write
FieldTypeNotes
titlestringreq, 1–200
notesstring≤ 2000
remindAtISO instantreq, absolute UTC
taskIdcuid | nullattach to a task
recurrenceenumnone|daily|weekly|monthly
›PATCH /reminders/{id} — scope reminders:write

title?, notes?, remindAt?, recurrence?, completed? (bool), plus `version` (req).

›DELETE /reminders/{id} — scope reminders:write

Notifications — scope notifications

›POST /notifications — scope notifications:write

Delivers a notification to the account that owns the API key. The recipient is always resolved from the bearer key; the request cannot supply a user or workspace id. The user's Settings → Notifications preferences decide whether the event is delivered in-app, by browser/native push, and/or by email.

FieldTypeNotes
titlestringreq, 1–160 chars
bodystringoptional, ≤ 4000 chars
targetPathstringoptional FocusPilot path, beginning with one /, ≤ 500 chars
bash
curl -X POST https://focuspilot.online/api/v1/notifications \
  -H "Authorization: Bearer fpk_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{"title":"Build finished","body":"The production deployment succeeded.","targetPath":"/today"}'

Returns HTTP 201 with the channels that accepted the event:

json
{ "data": { "delivery": { "inApp": true, "email": false, "push": true } } }

Each POST is treated as a separate event. Give an external service a dedicated, short-lived key with only notifications:write whenever possible.


Focus sessions — scope focus

›GET /focus-sessions

Query: range = recent (default) or today. Returns FocusSessionDTO[].

›POST /focus-sessions — scope focus:write
FieldTypeNotes
typeenumfocus (default) | short_break | long_break
taskIdcuid | null
startedAtISO instantreq
endedAtISO instantreq
durationSecondsint 0–86400req
notesstring | null≤ 2000
distractionsarray≤ 200 of { note: string(1–500), loggedAt: ISO }

Energy — scope energy

›GET /energy

No query → today's energy log (or null). ?days=N (1–90) → the last N days.

›POST /energy — scope energy:write

Sets today's energy. Body: { level: "low"|"medium"|"high", note?: string(≤500) | null }.


Analytics — scope analytics (read-only)

›GET /analytics

Query: days = 7 (default) | 30 | 90. Focus minutes/sessions, tasks done, distractions, focus-by-hour, and priority mix over the window. Day and hour buckets are resolved in the caller's timezone preference; the response's timezone field states which one.

›GET /analytics/overview

Query (all optional):

ParamValuesNotes
preset7d 30d 90d month quarter yearCalendar presets resolve against the caller's timezone — month means their month
days7 | 30 | 90Simple form; ignored when preset or from/to is given
from, toYYYY-MM-DDCustom range, inclusive. Must be supplied together, from ≤ to, ≤ 730 days
areaIdcuidScopes tasks, focus and goals to one area

Defaults to the last 7 days. The full cross-module read model: task throughput/cycle time/backlog health, focus volume with a weekday×hour heat map and clustered distraction notes, habit consistency, wellbeing trends with their correlation to daily focus, goal pacing, and day-plan adherence.

Every headline number is a MetricDelta — { current, previous, changePct } — comparing against the immediately preceding, equally long window (previousFrom / previousTo). Day keys are YYYY-MM-DD in the caller's timezone.

Sections come back null when they don't apply: habits without tracked habits, goals without goals, planning without day plans in the range, and wellbeing when the Wellbeing module is disabled. An areaId filter also nulls habits, wellbeing and planning — none of them carry an area, so returning unfiltered numbers beside filtered ones would mislead.

›GET /insights/estimation

Query: days = 7 (default) | 30 | 90. Planned-vs-actual time estimation accuracy stats.


Finance — scope finance · Finance module

Amounts are integer cents; currency is a 3-letter ISO code (upper-cased).

›Clients
  • GET /clients — list.
  • POST /clients — name req (1–200), email? (≤200) | null, hourlyRateCents? (int ≥0) | null, currency?, notes? (≤2000) | null.
  • PATCH /clients/{id} — any of the above (partial) plus `version` (req).
  • DELETE /clients/{id}.
›Invoices
  • GET /invoices — list.
  • GET /invoices/{id} — one invoice with its line items.
  • POST /invoices:
FieldTypeNotes
numberstringreq, 1–60
clientIdcuid | null
titlestring | null≤ 200
currency3-letter
issueDateYYYY-MM-DDreq
dueDateYYYY-MM-DD | null
statusenumdraft|sent|partial|paid|overdue|void
notesstring | null≤ 2000
lineItemsarray (≤100){ description(1–300), quantity(>0), unitAmountCents(int) }
  • PATCH /invoices/{id} — fields above plus paidAmountCents? (int ≥0) and `version` (req).
  • DELETE /invoices/{id}.
›Expenses
  • GET /expenses — list.
  • POST /expenses — title req (1–200), vendor? | null, category? (≤100), amountCents req (int ≥0), currency?, expenseDate req (YYYY-MM-DD), status? (paid|pending|reimbursable), clientId?, notes? | null.
  • PATCH /expenses/{id} — partial + `version` (req).
  • DELETE /expenses/{id}.
›Payables
  • GET /payables — list.
  • POST /payables — vendor req (1–200), title? | null, amountCents req (int ≥0), currency?, dueDate req (YYYY-MM-DD), status? (pending|approved|scheduled|paid|overdue), clientId?, notes?.
  • PATCH /payables/{id} — partial + `version` (req).
  • DELETE /payables/{id}.
›Aggregates
  • GET /finance/summary — outstanding, collected, expenses, net, overdue count.
  • GET /finance/ledger — unified cashflow entries (money in/out) with dates and amounts in cents.

Trading — scope trading · Trading module

Prices are plain numbers; feesCents/startingBalanceCents are integer cents.

›Trades
  • GET /trades — list; optional ?accountId=.
  • GET /trades/{id} — one trade.
  • POST /trades:
FieldTypeNotes
accountIdcuidreq
instrumentIdcuidreq
directionenumreq, long|short
statusenumopen|closed|cancelled
openedAtISO instantreq
closedAtISO instant | null
quantitynumber > 0req
entryPricenumberreq
exitPrice / stopPrice / targetPricenumber | null
feesCentsint ≥0
setupIdcuid | null
sessionenum | nullasia|london|ny|other
emotionstring | null≤ 60
ratingint 1–5 | null
notesstring | null≤ 2000
  • PATCH /trades/{id} — all fields optional + `version` (req).
  • DELETE /trades/{id}.
›Screenshot import (authenticated web API)
  • POST /api/trading/import/analyze — multipart request with accountId and a JPG, PNG, or WebP file (maximum 8 MB). Uses the configured Gemini/OpenAI vision provider to return editable drafts for every visible closed trade, including rows from historical dates. The image is processed in memory and is not persisted.
  • POST /api/trading/import/rebind — moves analyzed row identities to another workspace-owned trading account and returns freshly account-scoped fingerprints and duplicate flags. It does not resend the screenshot to AI.
  • POST /api/trading/import/confirm — saves only the reviewed drafts selected by the user. Fingerprints are derived again for the final selected account, and account and instrument ownership are rechecked server-side; (workspaceId, accountId, importFingerprint) uniqueness makes retries and repeated screenshots idempotent without affecting another tenant.

These two routes power the first-party review flow and are not currently part of the token-authenticated /api/v1 surface.

›Accounts
  • GET /trading-accounts — list.
  • POST /trading-accounts — name req (1–120), broker? | null, currency? (3-letter), startingBalanceCents? (int), accountType? (live|demo|prop).
  • PATCH /trading-accounts/{id} — partial + `version` (req).
  • DELETE /trading-accounts/{id}.
›Instruments
  • GET /instruments — list.
  • POST /instruments — symbol req (1–40, upper-cased), name? | null, assetClass req (forex|crypto|stock|future|option|index|commodity), exchange? | null, currency? (3-letter), contractSize?/tickSize? (number >0) | null.
  • DELETE /instruments/{id}.
›Setups
  • GET /trade-setups — list.
  • POST /trade-setups — name req (1–120), description? | null (≤1000), rules? | null (≤2000).
  • DELETE /trade-setups/{id}.
›Summary & journal
  • GET /trading/summary — signed cumulative equity curve, win/loss/break-even record, net P&L, average R, expectancy, profit factor, average winner/loser, maximum drawdown, and P&L breakdowns by instrument, setup, and session; optional ?accountId=.
  • GET /trading/journal — daily journal entries; optional ?q= full-text.
  • PUT /trading/journal — upsert a day's entry (one per date):
FieldTypeNotes
dateYYYY-MM-DDreq
marketOrgstring | null≤ 500
highImpactNewsstring | null≤ 5000
conditionsstring | null≤ 5000
notesstring | null≤ 20000
screenshotsstring[] (≤30)each a valid URL, ≤2000 chars
  • DELETE /trading/journal/{id} — delete a journal entry by id.

Bookings — scope bookings · Bookings module

Manage your Calendly-style booking profiles and view received bookings. (Guests book through the public /book/<handle>/<slug> pages — that flow is not part of this authenticated API.)

›GET /bookings

Received bookings; optional ?profileId=.

›POST /bookings/{id}/cancel — scope bookings:write

Cancels a received booking.

›GET /booking-profiles

List your event-type profiles.

›POST /booking-profiles — scope bookings:write
FieldTypeNotes
slugstringreq, 3–50 lowercase/digits/hyphens
titlestringreq, 1–120
descriptionstring | null≤ 2000
timezonestringIANA tz, ≤ 64
meetingDurationMinutesint 5–480
slotIntervalMinutesint 5–480
enabledboolean
availabilityobjectweekday index "0"–"6" → array (≤6) of { start, end } minutes-from-midnight (0–1440), end > start
questionsarray (≤10){ id(1–40), label(1–160), required: bool }
›GET /booking-profiles/{id} — scope bookings:read
›PATCH /booking-profiles/{id} — scope bookings:write

Fields above (partial) plus `version` (req).

›DELETE /booking-profiles/{id} — scope bookings:write

Reference: enums

  • Task status: todo doing done archived
  • Priority: low medium high urgent
  • Goal horizon: week month quarter year · progress type: binary numeric milestone · status: active done paused archived
  • Habit cadence: daily weekly custom
  • Reminder / checklist reset recurrence: none daily weekly monthly
  • Task recurrence: daily weekdays weekly biweekly monthly
  • Focus type: focus short_break long_break
  • Energy level: low medium high
  • Area colors: indigo violet sky emerald amber rose teal fuchsia
  • Note/checklist colors: slate plus every area color above
  • Invoice status: draft sent partial paid overdue void
  • Expense status: paid pending reimbursable
  • Payable status: pending approved scheduled paid overdue
  • Asset class: forex crypto stock future option index commodity · account type: live demo prop · direction: long short · trade status: open closed cancelled · session: asia london ny other

Reference: create defaults

Omitted optional create fields use these server defaults:

ResourceDefaults
Taskstatus: "todo", priority: "medium"; optional links, dates, minutes, notes, and recurrence are null
Areacolor: "indigo", appended after existing areas
Goalhorizon: "month", progressType: "milestone", status: "active"
Habitcolor: "indigo", cadence: "daily", targetPerPeriod: 1
Noteempty title/body, color: "slate", pinned: false
Checklistcolor: "slate", resetCadence: "none"
Reminderrecurrence: "none", completed: false, status: "scheduled"
Focus sessiontype: "focus", no task, notes, or distractions
Clientcurrency: "USD"
Invoicecurrency: "USD", status: "draft", paidAmountCents: 0, empty line items
Expensecategory: "General", currency: "USD", status: "paid"
Payablecurrency: "USD", status: "pending"
Instrumentcurrency: "USD"; optional market metadata is null
Trading accountcurrency: "USD", startingBalanceCents: 0, accountType: "live"
Tradestatus: "closed" when exitPrice is supplied, otherwise "open"; feesCents: 0
Booking profiletimezone: "UTC", 30-minute meetings/intervals, enabled, Mon–Fri 09:00–17:00 availability

Security notes

  • A key resolves server-side to the workspace of the user who created it. Every query is tenant-scoped by workspaceId; a key can never read or write another workspace, and foreign/unknown ids return 404 (never 403 — existence isn't leaked).
  • Only a SHA-256 hash of each token is stored. Treat keys like passwords; rotate by creating a new key and revoking the old one.
  • Prefer short expiries and minimal scopes for third-party integrations. Revoke immediately if a key may be exposed; toggle the Developers module off to cut all access at once.

Response models

These are the exact JSON shapes shared by the web and mobile clients. Every successful response wraps one model, an array of models, or the endpoint-specific object described above in { data: ... }.

Show all TypeScript response types
typescript
export type ApiSuccess<T> = { data: T };

export type ApiFailure = { error: { code: string; message: string; fields?: Record<string, string[]> } };

export type AreaDTO = {
  id: string;
  name: string;
  color: string;
  icon: string | null;
  sortOrder: number;
  parentAreaId: string | null;
  version: number;
};

export type TaskDTO = {
  id: string;
  title: string;
  notes: string | null;
  status: "todo" | "doing" | "done" | "archived";
  priority: "low" | "medium" | "high" | "urgent";
  areaId: string | null;
  goalId: string | null;
  parentTaskId: string | null;
  dueAt: string | null; // ISO
  scheduledAt: string | null; // ISO
  plannedMinutes: number | null;
  actualMinutes: number | null;
  completedAt: string | null; // ISO
  recurrenceRule: string | null;
  recurrenceSeriesId: string | null;
  recurrenceEnd: string | null; // ISO
  subtaskCount: number;
  doneSubtaskCount: number;
  tags: TagDTO[];
  sortOrder: number;
  version: number;
};

export type CommentDTO = {
  id: string;
  body: string;
  authorUserId: string;
  authorName: string;
  createdAt: string; // ISO
};

export type GoalMilestoneDTO = {
  id: string;
  title: string;
  description: string | null;
  startDate: string | null; // YYYY-MM-DD
  dueDate: string | null; // YYYY-MM-DD
  progress: number; // 0-100
  sortOrder: number;
};

export type GoalProgressLogDTO = {
  id: string;
  value: number;
  note: string | null;
  loggedAt: string; // ISO
};

export type GoalDTO = {
  id: string;
  title: string;
  description: string | null;
  areaId: string | null;
  horizon: "week" | "month" | "quarter" | "year";
  progressType: "binary" | "numeric" | "milestone";
  startDate: string | null; // YYYY-MM-DD
  dueDate: string | null; // YYYY-MM-DD
  targetValue: number | null;
  currentValue: number | null;
  status: "active" | "done" | "paused" | "archived";
  version: number;
  progress: number; // computed 0-100
  milestoneCount: number;
  doneMilestoneCount: number;
  taskCount: number;
  doneTaskCount: number;
  /** Total focus seconds across linked tasks (list + detail). */
  focusSeconds: number;
  milestones?: GoalMilestoneDTO[];
  progressLog?: GoalProgressLogDTO[];
  /** Detail only: every linked task, open and completed. */
  tasks?: GoalTaskDTO[];
  /** Detail only: the time breakdown behind `focusSeconds`. */
  time?: GoalTimeDTO;
};

export type HabitDTO = {
  id: string;
  name: string;
  description: string | null;
  color: string;
  cadence: "daily" | "weekly" | "custom";
  targetPerPeriod: number;
  archived: boolean;
  version: number;
  /** completed date keys (YYYY-MM-DD) within the returned window */
  completedDates: string[];
  streak: number;
  completionPct: number; // over the window
};

export type NoteDTO = {
  id: string;
  title: string;
  body: string;
  color: string;
  pinned: boolean;
  areaId: string | null;
  updatedAt: string; // ISO
  version: number;
};

export type ChecklistItemDTO = {
  id: string;
  title: string;
  completed: boolean;
  sortOrder: number;
};

export type ChecklistDTO = {
  id: string;
  title: string;
  color: string;
  resetCadence: "none" | "daily" | "weekly" | "monthly";
  resetWeekday: number | null;
  lastResetDate: string | null; // ISO
  version: number;
  items: ChecklistItemDTO[];
};

export type ReminderDTO = {
  id: string;
  title: string;
  notes: string | null;
  remindAt: string; // ISO (absolute UTC instant)
  recurrence: "none" | "daily" | "weekly" | "monthly";
  completed: boolean;
  status: "scheduled" | "sent" | "cancelled";
  taskId: string | null;
  taskTitle: string | null; // convenience: the linked task's title, when taskId set
  createdAt: string; // ISO
  version: number;
};

export type FocusSessionDTO = {
  id: string;
  taskId: string | null;
  taskTitle: string | null;
  type: "focus" | "short_break" | "long_break";
  startedAt: string; // ISO
  endedAt: string | null; // ISO
  durationSeconds: number;
  distractionCount: number;
  notes: string | null;
};

export type EnergyLogDTO = {
  id: string;
  date: string; // ISO date
  level: "low" | "medium" | "high";
  note: string | null;
};

export type AnalyticsDTO = {
  rangeDays: number;
  /** IANA timezone every day/hour bucket below was resolved in (the user's). */
  timezone: string;
  totals: {
    focusMinutes: number;
    sessions: number;
    tasksDone: number;
    distractions: number;
  };
  focusByDay: { date: string; label: string; minutes: number }[];
  tasksDoneByDay: { date: string; label: string; count: number }[];
  focusByHour: { hour: number; minutes: number }[];
  priorityMix: { priority: string; count: number }[];
};

export type EstimationBucket = {
  key: string; // stable key (area id / priority value / "overall")
  label: string; // "Work", "High", "No area", "Overall"
  samples: number; // tasks with BOTH planned & actual minutes > 0
  plannedMinutes: number; // sum
  actualMinutes: number; // sum
  biasFactor: number | null; // actualMin/plannedMin; null if samples < MIN_SAMPLES
};

export type EstimationStatsDTO = {
  rangeDays: number;
  /** IANA timezone the day range was resolved in (the user's). */
  timezone: string;
  overall: EstimationBucket;
  byArea: EstimationBucket[];
  byPriority: EstimationBucket[];
};

export type ClientDTO = {
  id: string;
  name: string;
  email: string | null;
  hourlyRateCents: number | null;
  currency: string;
  notes: string | null;
  invoiceCount: number;
  version: number;
};

export type InvoiceLineItemDTO = {
  id: string;
  description: string;
  quantity: number;
  unitAmountCents: number;
  amountCents: number;
  sortOrder: number;
};

export type InvoiceDTO = {
  id: string;
  number: string;
  clientId: string | null;
  clientName: string | null;
  title: string | null;
  amountCents: number;
  paidAmountCents: number;
  currency: string;
  issueDate: string; // YYYY-MM-DD
  dueDate: string | null; // YYYY-MM-DD
  status: "draft" | "sent" | "partial" | "paid" | "overdue" | "void";
  notes: string | null;
  version: number;
  lineItems?: InvoiceLineItemDTO[];
};

export type ExpenseDTO = {
  id: string;
  title: string;
  vendor: string | null;
  category: string;
  amountCents: number;
  currency: string;
  expenseDate: string; // YYYY-MM-DD
  status: "paid" | "pending" | "reimbursable";
  clientId: string | null;
  notes: string | null;
  receiptFileId: string | null;
  receiptName: string | null;
  version: number;
};

export type PayableDTO = {
  id: string;
  vendor: string;
  title: string | null;
  amountCents: number;
  currency: string;
  dueDate: string; // YYYY-MM-DD
  status: "pending" | "approved" | "scheduled" | "paid" | "overdue";
  clientId: string | null;
  notes: string | null;
  version: number;
};

export type LedgerEntryDTO = {
  id: string;
  entityType: string;
  entityId: string;
  direction: "in" | "out";
  amountCents: number;
  currency: string;
  occurredAt: string; // ISO
  label: string;
};

export type FinanceSummaryDTO = {
  currency: string;
  outstandingCents: number; // unpaid invoiced (amount - paid) for non-void/non-paid
  paidCents: number; // collected this range
  expensesCents: number; // expenses this range
  payablesDueCents: number; // unpaid payables
  netCents: number; // paid - expenses (range)
  invoiceCount: number;
  overdueInvoiceCount: number;
  cashflowByMonth: { month: string; label: string; inCents: number; outCents: number }[];
};

export type InstrumentDTO = {
  id: string;
  symbol: string;
  name: string | null;
  assetClass: "forex" | "crypto" | "stock" | "future" | "option" | "index" | "commodity";
  exchange: string | null;
  currency: string;
  contractSize: number | null;
  tickSize: number | null;
  isBuiltin: boolean;
};

export type TradingAccountDTO = {
  id: string;
  name: string;
  broker: string | null;
  currency: string;
  startingBalanceCents: number;
  accountType: "live" | "demo" | "prop";
  version: number;
  tradeCount: number;
  realizedPnlCents: number;
  balanceCents: number;
};

export type TradeSetupDTO = {
  id: string;
  name: string;
  description: string | null;
  rules: string | null;
  tradeCount: number;
};

export type TradingJournalDTO = {
  id: string;
  date: string; // YYYY-MM-DD
  marketOrg: string | null;
  highImpactNews: string | null;
  conditions: string | null;
  notes: string | null;
  screenshots: string[];
  version: number;
  updatedAt: string; // ISO
};

export type TradeDTO = {
  id: string;
  accountId: string;
  accountName: string | null;
  instrumentId: string;
  instrumentSymbol: string | null;
  direction: "long" | "short";
  status: "open" | "closed" | "cancelled";
  openedAt: string; // ISO
  closedAt: string | null; // ISO
  quantity: number;
  entryPrice: number;
  exitPrice: number | null;
  stopPrice: number | null;
  targetPrice: number | null;
  feesCents: number;
  pnlCents: number | null;
  rMultiple: number | null;
  riskAmountCents: number | null;
  setupId: string | null;
  setupName: string | null;
  session: string | null;
  emotion: string | null;
  rating: number | null;
  notes: string | null;
  importSource?: string | null;
  externalTradeId?: string | null;
  currency: string;
  legs: TradeLegDTO[];
  version: number;
};

export type TradingSummaryDTO = {
  totalTrades: number;
  openTrades: number;
  closedTrades: number;
  wins: number;
  losses: number;
  breakevens: number;
  winRate: number; // 0-100
  netPnlCents: number;
  avgRMultiple: number | null;
  expectancyCents: number | null; // avg pnl per closed trade
  profitFactor: number | null;
  avgWinCents: number | null;
  avgLossCents: number | null;
  maxDrawdownCents: number;
  bestCents: number | null;
  worstCents: number | null;
  currency: string;
  equityCurve: { label: string; value: number }[]; // cumulative pnl in currency units
  byInstrument: { symbol: string; trades: number; pnlCents: number }[];
  bySetup: { setup: string; trades: number; pnlCents: number }[];
  bySession: { session: string; trades: number; pnlCents: number }[];
};

export type DayWindow = { start: number; end: number };

export type WeeklyAvailability = {
  // index 0=Sunday … 6=Saturday
  [weekday: number]: DayWindow[];
};

export type BookingQuestion = { id: string; label: string; required: boolean };

export type BookingProfileDTO = {
  id: string;
  /** The workspace's public booking namespace; URL is /book/<handle>/<slug>. */
  handle: string;
  slug: string;
  enabled: boolean;
  title: string;
  description: string | null;
  timezone: string;
  meetingDurationMinutes: number;
  slotIntervalMinutes: number;
  bufferBeforeMinutes: number;
  bufferAfterMinutes: number;
  minimumNoticeMinutes: number;
  bookingWindowDays: number;
  locationType: "custom" | "google_meet";
  location: string | null;
  confirmationMessage: string | null;
  reminderMessage: string | null;
  availability: WeeklyAvailability;
  dateOverrides: BookingDateOverride[];
  questions: BookingQuestion[];
  version: number;
  bookingCount: number;
};

export type BookingDTO = {
  id: string;
  profileId: string;
  profileTitle: string | null;
  guestName: string;
  guestEmail: string;
  guestTimezone: string | null;
  startUtc: string; // ISO
  endUtc: string; // ISO
  answers: { label: string; value: string }[];
  status: "confirmed" | "cancelled";
  notes: string | null;
  conferenceUrl: string | null;
  calendarSyncStatus: "not_applicable" | "pending" | "synced" | "failed";
  createdAt: string; // ISO
};

MCP server

MCP exposes only the tools allowed by the connection's scopes and the workspace's enabled modules. REST API keys and MCP tokens are deliberately not interchangeable.

Sign in with OAuth (recommended)

The server supports OAuth 2.1 (authorization code + PKCE) with Dynamic Client Registration, discovered from the WWW-Authenticate header on a 401 (https://focuspilot.online/.well-known/oauth-protected-resource). Clients that support it (ChatGPT, ChatGPT Work, claude.ai, Claude Code, Codex, Cursor, VS Code) need only the URL. The user approves read-and-write or read-only access, and the issued access token is an ordinary MCP token listed under Developers → MCP tokens.

claude mcp add --transport http --scope user focuspilot https://focuspilot.online/api/mcp
# then run /mcp in Claude Code → focuspilot → Authenticate

codex mcp add focuspilot --url https://focuspilot.online/api/mcp
codex mcp login focuspilot

Or send an MCP token

claude mcp add --transport http focuspilot https://focuspilot.online/api/mcp \
  --header "Authorization: Bearer fpm_YOUR_TOKEN"
{
  "mcpServers": {
    "focuspilot": {
      "url": "https://focuspilot.online/api/mcp",
      "headers": { "Authorization": "Bearer fpm_YOUR_TOKEN" }
    }
  }
}

Step-by-step guides for each app are in the help center under Connect AI tools.

Write tools change workspace data immediately. Give AI clients read-only tokens unless writes are required, grant the narrowest scopes possible, and instruct the client to confirm destructive actions.