Skip to main content

Server API

The Server API is the localhost HTTP API served by Toby’s background service (daemon). Toby.app, scheduled jobs, and other local clients use it for chat, sessions, configuration, recordings metadata, projects, memories, and more.

This page is the endpoint reference. For access model, config, and how this differs from Toby.app’s Native API, see Local APIs.

:::note Native API is separate Paths under /api/native/* are not part of this API. They are hosted by Toby.app on the port in ~/.toby/native-port. See the Native API reference. :::

Access model

ItemDetail
Base URLhttp://127.0.0.1:<port> (default 7847)
BindingLocalhost only
AuthNone (local trust)
EncodingJSON; SSE for chat turns
Errors{ "error": string } on non-2xx
Unknown /api/*404
{
"web": {
"enabled": true,
"port": 7847
}
}

Common types

type ChatSessionSettings = {
persona?: string;
modules?: readonly string[];
dryRun?: boolean;
debug?: boolean;
};

type SessionSummary = {
id: string;
name: string;
createdAt: string;
updatedAt: string;
};

type PlanSummary = {
id: string;
goal: string;
status: string;
phases: readonly {
id: string;
label: string;
status: string;
}[];
};

Transcript entries and chat stream events are structured JSON objects emitted during a turn (see Chat turns).

Endpoint index

MethodPathPurpose
ANY/api/healthDaemon health and identity handshake
GET/api/statusVersion, persona, model, integrations, skill count
GET/api/dashboardAggregator payload (all categories; internal / debug)
GET/api/dashboard/:sectionAggregator list for one category (not used by home cards)
GET/api/dashboard/:section/contentHome card block content (flow output; preferred)
GET/api/dashboard/:section/summaryAlias of /content
GET/api/flowsFlow list for the app UI (id, name, description, builtin, persona, nodes, timestamps; built-ins seeded on list)
GET/api/flows/runsFlow execution history summaries
GET/api/flows/runs/:idOne flow run with per-node detail
POST/api/issuesFile or forward a GitHub issue report
GET/api/daemon/statusProcess + inbound chat status
POST/api/daemon/restartRestart the daemon
POST/api/daemon/stopStop the daemon
GET/api/listen/statusListen manager state
POST/api/listen/startStart helper-backed capture
POST/api/listen/stopStop capture (save or discard)
GET/api/listen/recordingsList saved recordings
GET/api/listen/recordings/:idRecording detail + transcript + summary
PATCH/api/listen/recordings/:idUpdate name, description, or linked chat
DELETE/api/listen/recordings/:idDelete a recording
POST/api/listen/recordings/:id/transcribeTranscribe or retranscribe
POST/api/listen/recordings/:id/summarizeSummarize or re-summarize a transcript
GET/api/sessionsList chat sessions
POST/api/sessionsCreate a session
GET/api/sessions/:idSession transcript and settings
PATCH/api/sessions/:idRename or update settings
DELETE/api/sessions/:idDelete a session
POST/api/sessions/:id/bootstrapAssemble-only bootstrap
POST/api/sessions/:id/turnChat turn (SSE stream)
POST/api/sessions/:id/turn/:turnId/cancelCancel an in-flight turn
POST/api/sessions/:id/turn/:turnId/ask-user/:requestIdAnswer askUser prompt
GET/api/sessions/:id/planActive plan summary
POST/api/sessions/:id/plan/skipSkip a plan phase
POST/api/sessions/:id/plan/cancelCancel the active plan
GET/api/personasPersona picker list
GET/api/personas/:namePersona detail
GET/api/personas/image/:filenamePersona image asset
GET/api/ai/providersAI provider catalog with live models when configured
GET/api/ai/providers/usagePlan usage / balance for all providers
GET/api/ai/providers/:id/usagePlan usage / balance for one provider
GET/api/modulesConnected chat modules
GET/api/skillsLocal skills list
GET/api/skills/:nameSkill body
GET/api/skills/:name/iconSkill icon
GET/api/pluginsDiscovered plugins
GET/api/plugins/:name/iconPlugin icon
GET/api/projectsList projects
POST/api/projectsCreate a project
GET / PATCH / DELETE/api/projects/:idOrSlugProject detail, update, delete
GET/api/projects/:idOrSlug/treeProject folder tree
POST/api/projects/:idOrSlug/sessionsCreate a project-scoped session
GET/api/releases/changelogChangelog payload
GET/api/memoriesSearch / page memories
POST/api/memoriesCreate a manual memory
GET/api/memories/:idOne memory
PATCH/api/memories/:idUpdate a memory
DELETE/api/memories/:idDelete a memory
GET/api/memories/:id/explainSource / audit explanation
GET/api/configure/treeFull configure tree + values
GET/api/configure/sectionsLightweight settings sections
GET/api/configure/sections/:sectionKeyOne section detail
PATCH/api/configure/valuesPersist configure changes
POST/api/configure/actions/:actionNamed configure actions
POST/api/schedules/parse-cronParse cron / natural language
GET/api/schedules/runs/:idSchedule run detail
GET/api/integrations/:name/statusIntegration status
GET/api/integrations/:name/setup-guideSetup wizard steps
POST/api/integrations/:name/connectConnect
POST/api/integrations/:name/disconnectDisconnect
POST/api/integrations/:name/reauthorizeReauthorize
POST/api/integrations/:name/setupRun plugin setup
GET/icons/*Static provider / asset icons

Health and status

ANY /api/health

Returns 200 when the daemon HTTP server is reachable. Prefer GET.

Includes an identity payload so Toby.app can confirm it is talking to the expected binary (version, path, compiled vs source).

{
"ok": true,
"daemon": true,
"identity": {
"version": "1.2.3",
"executablePath": "/Applications/Toby.app/Contents/Resources/toby",
"execKind": "compiled",
"tobyDir": "/Users/you/.toby",
"pid": 12345,
"startedAt": "2026-01-01T00:00:00.000Z",
"entryScript": null
}
}
FieldMeaning
versionToby release version
executablePathAbsolute path of the binary or entry script
execKind"compiled" or "source"
tobyDirData directory for this process
entryScriptTS/JS entry path when execKind is "source"; otherwise null

GET /api/status

type ChatStatusResponse = {
version: string;
persona: string;
model: string;
connectedIntegrations: readonly string[];
skillCount: number;
};

GET /api/daemon/status

type DaemonStatusResponse = {
process: {
pid: number;
uptimeSeconds: number;
startedAt: string;
intervalSeconds: number | null;
logPath: string;
webPort: number | null;
executablePath: string;
execKind: "compiled" | "source";
version: string;
tobyDir: string;
entryScript: string | null;
};
chatInbound: {
enabled: boolean;
integration: string | null;
integrationLabel: string | null;
status: "disabled" | "idle" | "connecting" | "connected" | "error";
detail: string | null;
disabledReason: string | null;
updatedAt: string;
};
};

POST /api/daemon/restart

{ "ok": true, "restarting": true }

Schedules a detached restart after the response is flushed.

POST /api/daemon/stop

{ "ok": true, "stopping": true }

Schedules SIGTERM after the response is flushed. The API goes offline when the process exits.

Dashboard

GET /api/dashboard

Returns aggregator list/count data for all categories. Home cards do not use this; it remains for debug and tooling.

GET /api/dashboard/:section

Returns aggregator detail for one category (email, tasks, calendar). Unknown categories return 404 with { "error": "Unknown dashboard category: …" }.

Optional query: ?fresh=1 bypasses the short category cache.

GET /api/dashboard/:section/content

Returns block content for a home card body (flow-generated markdown plus light meta such as count and launchUrls), or null when no providers are connected. Preferred path for Toby.app.

Optional query: ?fresh=1 regenerates content (bypasses caches; used for manual refresh).

GET /api/dashboard/:section/summary

Alias of /content (legacy path name).

Issues

POST /api/issues

Files or opens a GitHub issue report helper.

Request body:

{
type: "bug" | "feature";
details: string; // required
source?: "tui" | "native-app";
repo?: string; // optional override
}

Success: { "ok": true, "url": string, "number": number }.
If automatic filing fails: { "ok": false, "fallbackUrl": string, "reason": string }.

Listen and recordings

These endpoints manage the daemon-facing recording lifecycle (list, detail, patch, delete, transcribe). Toby.app often captures audio through its Native API, then uses this Server API for transcription and browsing.

Daemon listen start/stop is separate from Toby.app’s in-process native capture state.

GET /api/listen/status

type ListenManagerState = {
status: "idle" | "starting" | "recording" | "stopping" | "error";
session?: {
id: string;
startedAt: string;
sources: { mic: boolean; system: boolean };
};
outputDir?: string;
message?: string;
error?: string;
};

POST /api/listen/start

Starts helper-backed capture (default mic + system audio). Returns ListenManagerState.

  • 409 if already recording
  • 500 if capture cannot start

POST /api/listen/stop

Optional JSON body. Empty body saves by default. Use { "action": "discard" } to discard (when supported by the manager path).

  • 400 invalid JSON
  • 409 no active recording
  • 500 finalization failure

GET /api/listen/recordings

type ListenRecordingsListResponse = {
recordings: Array<{
id: string;
dir: string;
name?: string;
description?: string;
createdAt: string;
startedAt: string;
stoppedAt?: string;
durationMs?: number;
sources: { mic: boolean; system: boolean };
hasAudio: boolean;
hasTranscript: boolean;
}>;
};

Newest first.

GET /api/listen/recordings/:id

Full metadata, playable audioPath (prefers combined.m4a, then mic WAV, then system WAV), transcript text, optional segments/warnings.

  • 404 if not found

PATCH /api/listen/recordings/:id

type PatchRecordingRequest = {
name?: string;
description?: string;
chatSessionId?: string | null;
};

At least one of name, description, or chatSessionId is required. Returns the same detail shape as GET.

  • 400 invalid body
  • 404 not found

DELETE /api/listen/recordings/:id

{ "ok": true }
  • 404 / 500 on failure

POST /api/listen/recordings/:id/transcribe

Runs transcription against the recording’s audio and updates artifacts.

Optional body:

{ "recordingsDir": "/path/to/recordings" }

Clients that send Accept: text/event-stream may receive a streaming transcription progress response; otherwise JSON is returned.

  • 400 no readable audio
  • 404 not found
  • 500 transcription failure

Sessions

GET /api/sessions

QueryDefaultMaxDescription
limit50500Max sessions to return
type SessionsListResponse = {
sessions: SessionSummary[];
};

POST /api/sessions

Empty body is allowed.

type CreateSessionRequest = {
name?: string;
persona?: string;
modules?: readonly string[];
dryRun?: boolean;
debug?: boolean;
bootstrap?: boolean;
};
  • Default name: "New chat"
  • bootstrap: true runs assemble-only bootstrap (no model call)

Response 201:

type CreateSessionResponse = {
id: string;
name: string;
settings: ChatSessionSettings;
};

GET /api/sessions/:id

type SessionDetailResponse = {
id: string;
name: string;
transcript: readonly unknown[];
messageCount: number;
settings: ChatSessionSettings;
activePlan: PlanSummary | null;
};
  • 404 if missing

PATCH /api/sessions/:id

type PatchSessionRequest = Partial<{
name: string;
persona: string;
modules: readonly string[];
dryRun: boolean;
debug: boolean;
}>;

Blank name is ignored; settings merge with existing values.

DELETE /api/sessions/:id

{ "ok": true }

POST /api/sessions/:id/bootstrap

type BootstrapSessionRequest = {
initialText?: string;
};

type BootstrapSessionResponse = {
messageCount: number;
};

Chat turns

POST /api/sessions/:id/turn

Submits a user turn and streams events as server-sent events.

Content-Type of the response: text/event-stream; charset=utf-8.

type TurnRequestBody = {
text: string; // required (non-blank after trim)
persona?: string;
modules?: readonly string[];
dryRun?: boolean;
clientTurnId?: string;
steering?: boolean;
generatePlan?: boolean;
};
  • persona / modules / dryRun override session settings for this turn
  • clientTurnId is optional correlation / idempotency
  • steering: true may cancel an active turn before submitting
  • generatePlan may be present on the shared type; behavior depends on the current handler

SSE format

  • Heartbeats: comment lines : keep-alive
  • Default messages: data: <ChatEvent JSON>
  • Named terminal events: event: done, event: error, event: ask_user_prompt

done payload:

type TurnDonePayload = {
turnId: string;
text: string;
appliedActions: readonly string[];
sessionName?: string;
usage?: unknown;
warnings?: readonly string[];
};

error payload:

type TurnErrorPayload = {
turnId?: string;
error: string;
};

ask_user_prompt payload:

type AskUserPromptPayload = {
turnId: string;
requestId: string;
query: string;
options: readonly string[];
};

Errors before the stream starts: 400 (bad JSON / blank text), 404 (session). Failures after streaming starts use event: error.

Example:

curl -N -X POST "http://127.0.0.1:7847/api/sessions/<id>/turn" \
-H "Content-Type: application/json" \
-d '{"text":"Summarize my calendar for today"}'

POST /api/sessions/:id/turn/:turnId/cancel

type CancelTurnResponse = {
ok: true;
cancelled: true;
};
  • 404 if turn not found, already done, or session missing

POST /api/sessions/:id/turn/:turnId/ask-user/:requestId

type AskUserAnswerRequest = {
selectedIndex: number;
selectedLabel: string;
rawInput?: string;
};

Missing fields normalize to selectedIndex: -1, empty labels/input.

{ "ok": true }

Plans

GET /api/sessions/:id/plan

type SessionPlanResponse = {
plan: PlanSummary | null;
};

POST /api/sessions/:id/plan/skip

{ "planId": "…", "phaseId": "…" }

POST /api/sessions/:id/plan/cancel

{ "planId": "…" }

Metadata

GET /api/personas

type PersonasResponse = {
personas: readonly { name: string; label: string }[];
};

GET /api/personas/:name

Persona detail for the named persona (404 if missing).

GET /api/personas/image/:filename

Serves a persona image asset. Use default.png for the default image.

GET /api/ai/providers

AI provider catalog used by settings and persona editor model pickers.

When a provider is configured (API key / credentials present — or Ollama base URL), Toby queries that provider’s models API and returns that list as-is. When unconfigured or the remote list fails, the response falls back to built-in curated model ids. User customModels from config are appended only when not already present.

type AIProvidersResponse = {
providers: readonly {
id: string;
displayName: string;
models: string[];
allowCustomModel: boolean;
configured: boolean;
}[];
};

GET /api/ai/providers/usage

Plan usage and balance for all registered AI providers. Providers that do not expose balance APIs return supported: false with "N/A" display labels. Results are cached for 60 seconds.

type AIProvidersUsageResponse = {
usage: readonly {
providerId: string;
supported: boolean;
currency?: "USD";
totalSpent?: number;
remaining?: number;
totalSpentLabel?: string;
remainingLabel?: string;
unavailableReason?: string;
fetchedAt: string;
}[];
};

GET /api/ai/providers/:id/usage

Plan usage and balance for a single AI provider. Bypasses the cache, so it is suitable for a manual refresh action.

type AIProviderUsageResponse = {
usage: {
providerId: string;
supported: boolean;
currency?: "USD";
totalSpent?: number;
remaining?: number;
totalSpentLabel?: string;
remainingLabel?: string;
unavailableReason?: string;
fetchedAt: string;
};
};

GET /api/modules

type ModulesResponse = {
modules: readonly {
name: string;
displayName: string;
connected: true;
}[];
};

GET /api/skills

type SkillsResponse = {
skills: readonly {
name: string;
description: string;
}[];
};

GET /api/skills/:name / GET /api/skills/:name/icon

Skill body and optional icon asset.

GET /api/plugins / GET /api/plugins/:name/icon

Discovered installable plugins and icons.

GET /api/releases/changelog

Changelog / release notes payload. Optional limit query (default/max typically 10).

Projects

GET /api/projects

{ projects: ProjectPayload[] }

POST /api/projects

type CreateProjectRequest = {
name?: string;
summary?: string;
folderPath?: string;
personaName?: string | null;
};

Response 201: { project: ProjectPayload }.

GET /api/projects/:idOrSlug

{
project: ProjectPayload;
sessions: SessionSummary[]; // project-scoped sessions
}

ProjectPayload includes id, slug, name, summary, folderPath, personaName, outputsDir, skillsDir, timestamps.

PATCH /api/projects/:idOrSlug

Partial update of name, summary, folderPath, personaName.

DELETE /api/projects/:idOrSlug

{ "ok": true }

GET /api/projects/:idOrSlug/tree

{ "tree": /* folder tree nodes */ }

POST /api/projects/:idOrSlug/sessions

Creates a chat session bound to the project.

type CreateProjectSessionRequest = {
name?: string;
persona?: string;
modules?: readonly string[];
dryRun?: boolean;
debug?: boolean;
};

Response 201: { session: … }.

Memories

GET /api/memories

QueryDefaultDescription
qSearch query
limit50 (max 500)Page size
offset0Skip count
type MemoriesResponse = {
memories: MemoryItem[];
limit: number;
offset: number;
total: number;
hasMore: boolean;
};

type MemoryItem = {
id: string;
type: string;
subject?: string;
value: string;
confidence: number;
sensitivity: string;
visibility: string;
createdAt: string;
updatedAt: string;
};

POST /api/memories

Create a manual memory. Body must include non-empty value. Optional: type, subject, confidence, sensitivity, visibility, expiresAt.

Response 201: { memory: MemoryItem }.

GET /api/memories/:id

{ memory: MemoryItem }

PATCH /api/memories/:id

Updatable fields: value, subject, type, sensitivity, visibility, confidence, expiresAt. At least one required.

DELETE /api/memories/:id

{ "ok": true }

GET /api/memories/:id/explain

type MemoryExplainResponse = {
explanation: {
item: MemoryItem;
sources: Array<{ id: string; system: string; label?: string }>;
auditTrail: Array<{ action: string; at: string; reason?: string }>;
};
};

Configure

GET /api/configure/tree

type ConfigureTreeResponse = {
tree: SettingsItem;
values: Record<string, string>;
integrationLabels: Record<string, string>;
};

type SettingsItem = {
label: string;
kind: string;
key: string;
navKey?: string;
children?: SettingsItem[];
masked?: boolean;
multiline?: boolean;
options?: string[];
selectChoices?: { value: string; label: string }[];
currentValue?: string;
readOnly?: boolean;
};

Secrets are redacted (placeholder ••••••).

GET /api/configure/sections

Lightweight section nodes for settings navigation (for example chatInbound, defaults, ai, projects).

GET /api/configure/sections/:sectionKey

Full section detail for a top-level or nested key (for example ai.openai).

PATCH /api/configure/values

type ConfigurePatchRequest = {
changes: Record<string, string>;
};

Response matches GET /api/configure/tree.

  • 400 if changes missing
  • 403 if the patch cannot be applied

POST /api/configure/actions/:action

Body: Record<string, string>.

ActionRequest fieldsResponse
create-persona{ "ok": true, "personaName": string }
delete-personapersonaName{ "ok": true }
set-default-personapersonaName{ "ok": true }
clear-default-persona{ "ok": true }
update-skill-fielddirName, field, value{ "ok": true }
delete-skilldirName{ "ok": true }
create-schedule{ "ok": true, "scheduleId": string }
update-schedule-fieldscheduleId, field, value{ "ok": true }
delete-schedulescheduleId{ "ok": true }
  • update-skill-field.field: name or description
  • update-schedule-field.field: enabled ("Yes" / "true"), name, prompt, persona, cron

Schedules

POST /api/schedules/parse-cron

Parses natural-language or cron expressions for the schedules UI.

GET /api/schedules/runs/:id

Returns schedule run detail / transcript for a completed or in-progress run.

Integrations

GET /api/integrations/:name/status

Connection state, health, tools list, setup flags, auth methods.

GET /api/integrations/:name/setup-guide

Onboarding wizard steps for the integration.

POST /api/integrations/:name/connect

POST /api/integrations/:name/disconnect

POST /api/integrations/:name/reauthorize

POST /api/integrations/:name/setup

Connect / disconnect / reauthorize / run plugin setup. Success typically { "ok": true }; failures return error JSON with appropriate status codes.

Icons

GET /icons/*

Static AI provider and related icon assets (not under /api/, but served by the same local server).

See also