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/jsonon 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
- In the app, open Settings → Modules and enable Developers.
- Go to the Developers page → API keys → Create.
- Give it a name, an optional expiry, and toggle Read/Write for each resource group (these become the key's scopes).
- 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:
| Condition | Status | error.code |
|---|---|---|
| Missing / malformed / unknown token | 401 | UNAUTHORIZED |
An MCP token (fpm_…) used here | 401 | UNAUTHORIZED |
| Revoked token | 401 | UNAUTHORIZED |
| Expired token | 401 | UNAUTHORIZED |
| Token owner no longer a member of the workspace | 401 | UNAUTHORIZED |
| Developers module disabled for the workspace (global kill switch) | 403 | FORBIDDEN |
| Token lacks the scope the endpoint needs | 403 | FORBIDDEN (Missing scope: <resource>:<level>) |
| More than 120 requests in a rolling minute for one key | 429 | RATE_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:
// 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
code | HTTP | Meaning |
|---|---|---|
UNAUTHORIZED | 401 | Missing/invalid/expired/revoked token |
FORBIDDEN | 403 | Module disabled, or missing scope |
NOT_FOUND | 404 | No such record in your workspace (also returned for another tenant's ids — existence is never leaked) |
VALIDATION | 400 | Body/query failed schema validation (fields explains) |
CONFLICT | 409 | Optimistic-concurrency version mismatch (see below) |
RATE_LIMITED | 429 | Per-key rate limit exceeded |
INTERNAL | 500 | Unexpected 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-stylePOSTrequests return200.- Resource-creation
POSTrequests return201, including external notifications, comments, checklist items, milestones, and goal progress logs. - Success bodies always use the
{ "data": ... }envelope; there are no empty204responses.
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 suffixedAt(e.g.dueAt,remindAt,openedAt) are absolute instants. Fields named…Date(e.g.issueDate,dueDate,expenseDate) are calendar dates inYYYY-MM-DDform.
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.
| Resource | Read scope | Write scope | Covers | Needs module |
|---|---|---|---|---|
tasks | tasks:read | tasks:write | Tasks, subtasks, task comments | — |
areas | areas:read | areas:write | Areas (projects/contexts) | — |
checklists | checklists:read | checklists:write | Checklists + items | — |
goals | goals:read | goals:write | Goals, milestones, progress | — |
habits | habits:read | habits:write | Habits + daily entries | — |
notes | notes:read | notes:write | Notes | — |
reminders | reminders:read | reminders:write | Reminders | — |
notifications | (none) | notifications:write | Deliver external notifications to this account | — |
focus | focus:read | focus:write | Focus/pomodoro sessions | — |
energy | energy:read | energy:write | Daily energy logs | — |
finance | finance:read | finance:write | Clients, invoices, expenses, payables, ledger | Finance |
trading | trading:read | trading:write | Trades, accounts, instruments, setups, journal | Trading |
bookings | bookings:read | bookings:write | Booking profiles + received bookings | Bookings |
analytics | analytics: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:
curl -s https://focuspilot.online/api/v1/me \
-H "Authorization: Bearer fpk_YOUR_KEY"{
"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:
curl -s "https://focuspilot.online/api/v1/tasks?status=todo" \
-H "Authorization: Bearer fpk_YOUR_KEY"Create a task:
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:
# 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:
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.
| Operations | HTTP | data payload |
|---|---|---|
GET /me | 200 | { workspace: {id,name,slug}, user: {email,name}, scopes: string[] } |
GET /tasks, POST /tasks, GET/PATCH /tasks/{id} | 200/201 | TaskDTO[] for list; otherwise TaskDTO |
DELETE /tasks/{id} | 200 | { id: string } |
GET/POST /tasks/{id}/comments | 200/201 | CommentDTO[] / CommentDTO |
DELETE /tasks/{id}/comments/{commentId} | 200 | { id: string } |
GET/POST /areas, PATCH /areas/{id} | 200/201 | AreaDTO[] / AreaDTO |
DELETE /areas/{id} | 200 | { id: string } |
GET/POST /goals, GET/PATCH /goals/{id} | 200/201 | GoalDTO[] / GoalDTO |
| Milestone create/update/delete; goal progress create | 200/201 | Refreshed GoalDTO |
DELETE /goals/{id} | 200 | { id: string } |
GET/POST /habits, PATCH /habits/{id} | 200/201 | HabitDTO[] / HabitDTO |
POST /habits/{id}/entries | 200 | { habitId, date, completed } |
DELETE /habits/{id} | 200 | { id: string } |
GET/POST /notes, PATCH /notes/{id} | 200/201 | NoteDTO[] / NoteDTO |
DELETE /notes/{id} | 200 | { id: string } |
GET/POST /checklists, PATCH /checklists/{id} | 200/201 | ChecklistDTO[] / ChecklistDTO |
| Checklist item create/update | 201/200 | ChecklistItemDTO |
| Checklist/item delete | 200 | { id: string } |
POST /checklists/{id}/items/reorder | 200 | { ok: true } |
GET/POST /reminders, PATCH /reminders/{id} | 200/201 | ReminderDTO[] / ReminderDTO |
DELETE /reminders/{id} | 200 | { id: string } |
POST /notifications | 201 | { delivery: { inApp, email, push } } |
GET/POST /focus-sessions | 200/201 | FocusSessionDTO[] / FocusSessionDTO |
GET /energy | 200 | EnergyLogDTO, EnergyLogDTO[], or null |
POST /energy | 200 | EnergyLogDTO |
GET /analytics | 200 | AnalyticsDTO |
GET /analytics/overview | 200 | AnalyticsOverviewDTO |
GET /insights/estimation | 200 | EstimationStatsDTO |
| Client/invoice/expense/payable list/create/update | 200/201 | Corresponding DTO array / DTO |
| Client/invoice/expense/payable delete | 200 | { id: string } |
GET /finance/summary | 200 | FinanceSummaryDTO |
GET /finance/ledger | 200 | LedgerEntryDTO[] |
| Trade/account/instrument/setup list/create/update | 200/201 | Corresponding DTO array / DTO |
| Trade/account/instrument/setup delete | 200 | { id: string } |
GET /trading/summary | 200 | TradingSummaryDTO |
GET/PUT /trading/journal | 200 | TradingJournalDTO[] / TradingJournalDTO |
DELETE /trading/journal/{id} | 200 | { id: string } |
GET/POST /booking-profiles, GET/PATCH /booking-profiles/{id} | 200/201 | BookingProfileDTO[] / BookingProfileDTO |
DELETE /booking-profiles/{id} | 200 | { id: string } |
GET /bookings | 200 | BookingDTO[] |
POST /bookings/{id}/cancel | 200 | Updated 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).
| Field | Type | Notes |
|---|---|---|
title | string | req, 1–500 chars |
notes | string | null | ≤ 20000 |
status | enum | todo|doing|done|archived |
priority | enum | low|medium|high|urgent |
areaId | cuid | null | |
goalId | cuid | null | |
parentTaskId | cuid | null | makes this a subtask |
dueAt | ISO instant | null | |
scheduledAt | ISO instant | null | |
plannedMinutes | int 1–1440 | null | |
recurrenceRule | enum | null | see recurrence rules |
recurrenceEnd | ISO instant | null |
Example TaskDTO:
{
"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
| Field | Type | Notes |
|---|---|---|
name | string | req, 1–80 |
color | enum | area color key, default indigo |
icon | string | null | ≤ 40 |
parentAreaId | cuid | null | nesting |
›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
| Field | Type | Notes |
|---|---|---|
title | string | req, 1–200 |
description | string | null | ≤ 2000 |
areaId | cuid | null | |
horizon | enum | week|month|quarter|year |
progressType | enum | binary|numeric|milestone |
startDate / dueDate | YYYY-MM-DD | null | |
targetValue / currentValue | number | null | for 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
| Field | Type | Notes |
|---|---|---|
title | string | req, 1–200 |
description | string | null | ≤ 1000 |
startDate / dueDate | YYYY-MM-DD | null | |
progress | int 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
| Field | Type | Notes |
|---|---|---|
name | string | req, 1–80 |
description | string | null | ≤ 500 |
color | enum | area color key |
cadence | enum | daily|weekly|custom |
targetPerPeriod | int 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
| Field | Type | Notes |
|---|---|---|
title | string | ≤ 200 |
body | string | ≤ 100000 |
color | enum | note color |
pinned | boolean | |
areaId | cuid | 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
| Field | Type | Notes |
|---|---|---|
title | string | req, 1–120 |
color | enum | note color |
resetCadence | enum | none|daily|weekly|monthly |
resetWeekday | int 0–6 | null | for 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
| Field | Type | Notes |
|---|---|---|
title | string | req, 1–200 |
notes | string | ≤ 2000 |
remindAt | ISO instant | req, absolute UTC |
taskId | cuid | null | attach to a task |
recurrence | enum | none|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.
| Field | Type | Notes |
|---|---|---|
title | string | req, 1–160 chars |
body | string | optional, ≤ 4000 chars |
targetPath | string | optional FocusPilot path, beginning with one /, ≤ 500 chars |
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:
{ "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
| Field | Type | Notes |
|---|---|---|
type | enum | focus (default) | short_break | long_break |
taskId | cuid | null | |
startedAt | ISO instant | req |
endedAt | ISO instant | req |
durationSeconds | int 0–86400 | req |
notes | string | null | ≤ 2000 |
distractions | array | ≤ 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):
| Param | Values | Notes |
|---|---|---|
preset | 7d 30d 90d month quarter year | Calendar presets resolve against the caller's timezone — month means their month |
days | 7 | 30 | 90 | Simple form; ignored when preset or from/to is given |
from, to | YYYY-MM-DD | Custom range, inclusive. Must be supplied together, from ≤ to, ≤ 730 days |
areaId | cuid | Scopes 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—namereq (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:
| Field | Type | Notes |
|---|---|---|
number | string | req, 1–60 |
clientId | cuid | null | |
title | string | null | ≤ 200 |
currency | 3-letter | |
issueDate | YYYY-MM-DD | req |
dueDate | YYYY-MM-DD | null | |
status | enum | draft|sent|partial|paid|overdue|void |
notes | string | null | ≤ 2000 |
lineItems | array (≤100) | { description(1–300), quantity(>0), unitAmountCents(int) } |
PATCH /invoices/{id}— fields above pluspaidAmountCents?(int ≥0) and `version` (req).DELETE /invoices/{id}.
›Expenses
GET /expenses— list.POST /expenses—titlereq (1–200),vendor?| null,category?(≤100),amountCentsreq (int ≥0),currency?,expenseDatereq (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—vendorreq (1–200),title?| null,amountCentsreq (int ≥0),currency?,dueDatereq (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:
| Field | Type | Notes |
|---|---|---|
accountId | cuid | req |
instrumentId | cuid | req |
direction | enum | req, long|short |
status | enum | open|closed|cancelled |
openedAt | ISO instant | req |
closedAt | ISO instant | null | |
quantity | number > 0 | req |
entryPrice | number | req |
exitPrice / stopPrice / targetPrice | number | null | |
feesCents | int ≥0 | |
setupId | cuid | null | |
session | enum | null | asia|london|ny|other |
emotion | string | null | ≤ 60 |
rating | int 1–5 | null | |
notes | string | 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 withaccountIdand a JPG, PNG, or WebPfile(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—namereq (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—symbolreq (1–40, upper-cased),name?| null,assetClassreq (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—namereq (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):
| Field | Type | Notes |
|---|---|---|
date | YYYY-MM-DD | req |
marketOrg | string | null | ≤ 500 |
highImpactNews | string | null | ≤ 5000 |
conditions | string | null | ≤ 5000 |
notes | string | null | ≤ 20000 |
screenshots | string[] (≤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
| Field | Type | Notes |
|---|---|---|
slug | string | req, 3–50 lowercase/digits/hyphens |
title | string | req, 1–120 |
description | string | null | ≤ 2000 |
timezone | string | IANA tz, ≤ 64 |
meetingDurationMinutes | int 5–480 | |
slotIntervalMinutes | int 5–480 | |
enabled | boolean | |
availability | object | weekday index "0"–"6" → array (≤6) of { start, end } minutes-from-midnight (0–1440), end > start |
questions | array (≤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:
tododoingdonearchived - Priority:
lowmediumhighurgent - Goal horizon:
weekmonthquarteryear· progress type:binarynumericmilestone· status:activedonepausedarchived - Habit cadence:
dailyweeklycustom - Reminder / checklist reset recurrence:
nonedailyweeklymonthly - Task recurrence:
dailyweekdaysweeklybiweeklymonthly - Focus type:
focusshort_breaklong_break - Energy level:
lowmediumhigh - Area colors:
indigovioletskyemeraldamberrosetealfuchsia - Note/checklist colors:
slateplus every area color above - Invoice status:
draftsentpartialpaidoverduevoid - Expense status:
paidpendingreimbursable - Payable status:
pendingapprovedscheduledpaidoverdue - Asset class:
forexcryptostockfutureoptionindexcommodity· account type:livedemoprop· direction:longshort· trade status:openclosedcancelled· session:asialondonnyother
Reference: create defaults
Omitted optional create fields use these server defaults:
| Resource | Defaults |
|---|---|
| Task | status: "todo", priority: "medium"; optional links, dates, minutes, notes, and recurrence are null |
| Area | color: "indigo", appended after existing areas |
| Goal | horizon: "month", progressType: "milestone", status: "active" |
| Habit | color: "indigo", cadence: "daily", targetPerPeriod: 1 |
| Note | empty title/body, color: "slate", pinned: false |
| Checklist | color: "slate", resetCadence: "none" |
| Reminder | recurrence: "none", completed: false, status: "scheduled" |
| Focus session | type: "focus", no task, notes, or distractions |
| Client | currency: "USD" |
| Invoice | currency: "USD", status: "draft", paidAmountCents: 0, empty line items |
| Expense | category: "General", currency: "USD", status: "paid" |
| Payable | currency: "USD", status: "pending" |
| Instrument | currency: "USD"; optional market metadata is null |
| Trading account | currency: "USD", startingBalanceCents: 0, accountType: "live" |
| Trade | status: "closed" when exitPrice is supplied, otherwise "open"; feesCents: 0 |
| Booking profile | timezone: "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
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 focuspilotOr 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.