Public API
PebbleFlow includes a REST API that lets you control everything programmatically: run agents, manage tools, schedule tasks, and more. The API lives on the same local server that powers the desktop app and browser bridge.
Base URL: http://localhost:3847/v1
Authentication: Two ways to authenticate, both gated by the API toggle in settings:
For external consumers (scripts, integrations, curl): Set an API access token in Settings > API Access, then use it as a Bearer token:
curl -H "Authorization: Bearer YOUR_API_TOKEN" http://localhost:3847/v1/providers
For the local app (automatic):
The PebbleFlow desktop app, browser extensions, and mobile apps authenticate automatically via the existing relay auth header (X-Relay-Auth). No manual setup needed — the app handles this behind the scenes.
Setup:
- Open PebbleFlow Settings > API Access
- Toggle Enable Public API on
- Set an API access token (any string you choose — treat it like a password)
- Use that token in all API requests
The API is available on localhost and through the private relay. Check GET /v1/auth/info (no auth required) for current status and setup instructions.
Providers & Models
Discover which LLM providers are configured and what models are available.
List providers:
GET /v1/providers
Returns all configured provider types (Anthropic, OpenAI, Google, OpenRouter, Ollama, Poe, MLX, Baseten, and others as they're added) with capability flags (supportsVision, supportsToolCalling, supportsStreaming, etc.) and whether an API key is configured.
List models for a provider:
GET /v1/providers/anthropic/models
Returns the model catalog for that provider. Each model includes id, displayName, and contextLength where available.
Flat catalog across all providers:
GET /v1/models
Merges models from every configured provider into one list. Providers without API keys are skipped and listed in warnings.
Agents
Agents are the core of PebbleFlow. Each agent is a Mode — a configured personality with its own system prompt, tools, variables, and skills.
List all agents:
GET /v1/agents
Returns builtin agents (Shopping, Workplace, General) and any custom agents you've created. Each is tagged with source: "builtin" or source: "custom".
Create a custom agent:
POST /v1/agents
Content-Type: application/json
{
"id": "my-research-agent",
"branding": {
"name": "Research Agent",
"description": "Searches the web and summarizes findings"
},
"defaultSettings": {
"systemPrompt": "You are a research assistant. Always cite sources.",
"enabledTools": { "web_browsing": true, "search_tools": true }
},
"settingLevels": {}
}
Returns 201 with the created agent. A vector clock is attached automatically for sync.
Update an agent:
PATCH /v1/agents/my-research-agent
Content-Type: application/json
{ "branding": { "name": "Research Agent", "description": "Updated description" } }
Merges the patch into the existing agent and bumps the vector clock. Builtin agents return 403 — they're read-only.
Delete an agent:
DELETE /v1/agents/my-research-agent
Soft-deletes via tombstone (syncs across devices). Returns 204.
Running Agents
This is the main event — invoke an agent to process a message.
Synchronous Mode
Wait for the full response:
POST /v1/runs
Content-Type: application/json
{
"agentId": "general",
"input": { "message": "What's the weather in Paris today?" },
"mode": "sync"
}
Returns 200 with { content, usage, status: "completed" } after the agent finishes. If the agent errors, returns 500 with { error, status: "error" }.
Asynchronous Mode
Fire and forget — useful for long-running tasks:
POST /v1/runs
Content-Type: application/json
{
"agentId": "my-research-agent",
"input": { "message": "Write a 2000-word analysis of renewable energy trends" },
"mode": "async"
}
Returns 202 immediately with { runId, threadId, status: "running" }.
Poll for status:
GET /v1/runs/{runId}
Returns { run: { runId, threadId, agentId, status, createdAt, content?, usage?, error? } }. Status is one of running, completed, error, or cancelled.
Stream events in real time (SSE):
GET /v1/runs/{runId}/events
Returns a text/event-stream with every agent event as it happens: GENERATION_STARTED, STREAMING_CONTENT, tool calls, subagent activity, and the terminal event (GENERATION_COMPLETE, GENERATION_ERROR, or GENERATION_CANCELLED). The stream ends after the terminal event.
Cancel a run:
POST /v1/runs/{runId}/cancel
Returns { run: { ..., status: "cancelled" } }.
Threads
Threads are conversations. Every agent run happens within a thread, and threads persist across sessions. The API lets you list, read, create, and manage threads programmatically.
List all threads (metadata only):
GET /v1/threads
Returns threads for the current profile with messages stripped for performance. Each thread includes id, title, createdAt, updatedAt, modeId, archived, and usage stats.
Get a thread with full messages:
GET /v1/threads/{id}
Returns the complete thread including its messages array — every user message, assistant response, tool call, and tool result.
Get just the messages:
GET /v1/threads/{id}/messages
Returns only the messages array — lighter than the full thread object when you just need the conversation.
Create a thread:
POST /v1/threads
Content-Type: application/json
{ "title": "Research project", "modeId": "general" }
Returns 201 with the new thread. By default, the API does NOT switch the app's active thread — pass "setActive": true in the body if you want that. The new thread appears in the sidebar immediately (via WebSocket broadcast).
Update a thread:
PATCH /v1/threads/{id}
Content-Type: application/json
{ "title": "Renamed project", "archived": true }
Updatable fields: title, modeId, archived, lastUsedModel. Changes broadcast to the sidebar in real time.
Delete a thread:
DELETE /v1/threads/{id}
Soft-deletes the thread (tombstone for sync). Returns 204. Deleted threads move to trash and can be recovered until trash is emptied.
Active thread:
GET /v1/threads/active # Returns { threadId }
PUT /v1/threads/active # Body: { "threadId": "..." }
Trash management:
GET /v1/threads/trash/count # Returns { count }
POST /v1/threads/trash/empty # Returns { deletedCount, protectedCount }
Protected threads (retained via the data retention toggle) are excluded from trash emptying.
Continuing a conversation via the API:
To send a follow-up message to an existing thread, use POST /v1/runs with the thread's ID:
POST /v1/runs
Content-Type: application/json
{
"agentId": "general",
"threadId": "existing-thread-id",
"input": { "message": "Follow up on that last point" },
"mode": "sync"
}
The agent sees the full conversation history from the thread.
Attachments
Attachments are files linked to threads — screenshots, PDFs, documents, uploaded images, generated artifacts. The API lets you list, upload, download, and manage them.
List all attachments (metadata only):
GET /v1/attachments
Returns attachment metadata for the current profile. Heavy fields (dataUrl, extractedContent, extractedImages) are stripped — use the detail or content endpoints for those.
List attachments for a specific thread:
GET /v1/threads/{threadId}/attachments
Get attachment metadata:
GET /v1/attachments/{id}
Returns full metadata including extractedContent (OCR text, parsed markdown), contentType, fileName, size, and a hasContent flag. The raw binary is NOT included — use the /content endpoint for that.
Download attachment binary:
GET /v1/attachments/{id}/content
Returns the raw file with the correct Content-Type and Content-Disposition headers. Pipe this to a file:
curl -o output.pdf \
-H "Authorization: Bearer $API_TOKEN" \
http://localhost:3847/v1/attachments/{id}/content
Upload an attachment:
POST /v1/attachments
Content-Type: application/json
{
"threadId": "thread-id",
"type": "user_upload",
"contentType": "application/pdf",
"fileName": "report.pdf",
"description": "Quarterly report",
"dataUrl": "data:application/pdf;base64,JVBERi0xLjQ..."
}
The dataUrl is a base64-encoded data URL. Returns 201 with the new attachment ID. The attachment is linked to the specified thread.
Update attachment metadata:
PATCH /v1/attachments/{id}
Content-Type: application/json
{ "description": "Updated description", "fileName": "new-name.pdf" }
Delete an attachment:
DELETE /v1/attachments/{id}
Soft-deletes via tombstone. Returns 204.
MCP Servers
Manage your MCP (Model Context Protocol) server connections — the servers that give agents access to external tools and data sources.
List configured servers:
GET /v1/mcp-servers
Returns all MCP server configs for the current profile. Sensitive fields (authToken, env, credentialId) are stripped from the response.
Get a server's config:
GET /v1/mcp-servers/{id}
Add a new MCP server:
POST /v1/mcp-servers
Content-Type: application/json
{
"id": "my-server",
"name": "My MCP Server",
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-filesystem", "/path/to/files"],
"serverType": "local"
}
For remote HTTP servers, use "url" instead of "command":
{
"id": "remote-server",
"name": "Remote API",
"url": "https://my-mcp-server.example.com/sse",
"serverType": "remote"
}
Update a server:
PATCH /v1/mcp-servers/{id}
Content-Type: application/json
{ "name": "Renamed Server", "args": ["-y", "@mcp/server-v2"] }
Enable/disable a server:
POST /v1/mcp-servers/{id}/toggle
Content-Type: application/json
{ "enabled": false }
Delete a server:
DELETE /v1/mcp-servers/{id}
Process Management
For local (stdio) MCP servers, you can manage the server process directly.
List running processes:
GET /v1/mcp-servers/processes
Returns running server processes with pid, startedAt, and running status.
Start a server:
POST /v1/mcp-servers/{id}/start
Reads the command/args/env from the server config and spawns the process. Returns the process status.
Stop a server:
POST /v1/mcp-servers/{id}/stop
Gracefully shuts down the server process (SIGTERM with fallback to SIGKILL).
Call a JSON-RPC method directly:
POST /v1/mcp-servers/{id}/call
Content-Type: application/json
{ "method": "tools/list", "params": {} }
Sends a raw JSON-RPC 2.0 request to the server and returns the result. Useful for debugging or calling methods not exposed through the tools API.
Tools & Toolkits
Browse and invoke the tools that agents use — web browsing, search, calendar, Gmail, Slate, and more.
List toolkits (grouped):
GET /v1/toolkits
Returns embedded tools grouped by category (Productivity, Search, Utilities, etc.) and any connected MCP servers as separate toolkits, each with their actions listed.
List all tools (flat):
GET /v1/tools
GET /v1/tools?source=embedded # Only builtin tools
GET /v1/tools?source=mcp # Only MCP server tools
Get tool details with input schema:
GET /v1/tools/calculator
Returns the tool's JSON Schema for its input parameters, so you can validate before invoking.
Invoke a tool directly:
POST /v1/tools/calculator/invoke
Content-Type: application/json
{ "input": { "expression": "sqrt(144) + 3^2" } }
Returns { result }. Input is validated against the tool's schema — invalid input returns 422 with details. Remote MCP tools return 501 with guidance to use /v1/runs instead (they require the agent subprocess transport).
Connectors
Manage OAuth integrations — Google, Microsoft, GitHub, Notion, Slack, and more.
Browse available integrations:
GET /v1/connectors/catalog
Returns all registered OAuth providers with their name, category, and default scopes.
List your connected accounts:
GET /v1/connectors
Returns active connections for the current profile. Tokens are never exposed — only metadata (provider, email, status, scopes, timestamps).
Check connection health:
POST /v1/connectors/{id}/test
Returns { health: { status, isTokenExpired, canRefresh } }.
Remove a connection:
DELETE /v1/connectors/{id}
Creating new connections requires the interactive OAuth flow via the app UI or the /auth/* routes.
Triggers
Schedule agents to run automatically — daily briefings, weekly reports, interval-based monitoring.
List triggers:
GET /v1/triggers
Create a scheduled trigger:
POST /v1/triggers
Content-Type: application/json
{
"name": "Morning Briefing",
"prompt": "Summarize my unread emails and today's calendar",
"modeId": "general",
"schedule": { "type": "daily", "time": "08:00" }
}
Supported schedule types:
{ "type": "interval", "minutes": 60 }— every N minutes (min 15, max 1440){ "type": "daily", "time": "09:00" }— daily at a specific time{ "type": "weekly", "day": "mon", "time": "09:00" }— weekly{ "type": "weekdays", "time": "08:30" }— Monday through Friday{ "type": "daysOfWeek", "days": ["mon", "wed", "fri"], "time": "10:00" }— specific days{ "type": "monthly", "dayOfMonth": 1, "time": "09:00" }— monthly{ "type": "manual" }— only when fired via API
Fire a trigger manually:
POST /v1/triggers/{id}/fire
Returns 202 with a threadId for the resulting run.
Update or delete:
PATCH /v1/triggers/{id}
DELETE /v1/triggers/{id}
Webhooks
Webhook triggers let external services (CI/CD, monitoring, form builders) trigger an agent run via HTTP.
Create a webhook trigger:
POST /v1/triggers
Content-Type: application/json
{
"name": "Deploy hook",
"prompt": "A deploy happened: {{webhook.body}}",
"modeId": "general",
"kind": "webhook"
}
Returns 201 with a webhookSecret and webhookPath. Store the secret — you'll need it to sign payloads.
Send a webhook:
# Compute HMAC-SHA256 of the raw request body
SIGNATURE=$(echo -n '{"repo":"my-app","branch":"main"}' | openssl dgst -sha256 -hmac "$WEBHOOK_SECRET" | awk '{print $2}')
POST /v1/webhooks/{triggerId}
Content-Type: application/json
X-Webhook-Signature: $SIGNATURE
{"repo": "my-app", "branch": "main"}
The webhook endpoint does NOT require bearer auth — it uses HMAC verification instead. Returns 202 with the threadId of the dispatched run. The {{webhook.body}} placeholder in the trigger prompt is replaced with the raw request body.
Custom Functions
Create your own tools that agents can call. Functions are written in JavaScript or Python and execute in a sandbox.
List functions:
GET /v1/functions
Create a function:
POST /v1/functions
Content-Type: application/json
{
"name": "calculate_bmi",
"description": "Calculate Body Mass Index from height and weight",
"language": "javascript",
"source": "return { bmi: (input.weightKg / (input.heightM * input.heightM)).toFixed(1) };",
"inputSchema": {
"type": "object",
"properties": {
"weightKg": { "type": "number" },
"heightM": { "type": "number" }
},
"required": ["weightKg", "heightM"]
}
}
JavaScript functions receive input and must return a result. Python functions set a result variable:
# Python example
result = {"bmi": round(input["weightKg"] / (input["heightM"] ** 2), 1)}
Execute a function directly:
POST /v1/functions/{id}/execute
Content-Type: application/json
{ "input": { "weightKg": 75, "heightM": 1.80 } }
Security: JavaScript runs in Node's vm sandbox (no filesystem or network access, 10s timeout). Python runs as a subprocess with a 30s timeout. Both validate input before execution.
Update or delete:
PATCH /v1/functions/{id}
DELETE /v1/functions/{id}
Workflows
Orchestrate multiple agents in a DAG (directed acyclic graph) — run steps in parallel where possible, and feed outputs from earlier steps into later ones.
Validate a workflow graph:
POST /v1/workflows/validate
Content-Type: application/json
{
"graph": {
"nodes": [
{ "id": "research", "agentId": "general", "prompt": "Research renewable energy trends" },
{ "id": "analyze", "agentId": "general", "prompt": "Research competitor pricing" },
{ "id": "report", "agentId": "general", "prompt": "Write a report combining: {{outputs.research}} and {{outputs.analyze}}", "dependsOn": ["research", "analyze"] }
]
}
}
Returns { valid: true/false, errors: [...] }. Checks for cycles, duplicate IDs, and missing dependency references.
Execute a workflow:
POST /v1/workflows/execute
Content-Type: application/json
{
"graph": {
"nodes": [
{ "id": "research", "agentId": "general", "prompt": "Research renewable energy trends" },
{ "id": "summarize", "agentId": "general", "prompt": "Summarize: {{outputs.research}}", "dependsOn": ["research"] }
]
}
}
Returns { status: "completed", outputs: { research: "...", summarize: "..." }, nodeResults: {...} }.
Independent nodes (no shared dependencies) run in parallel. The {{outputs.nodeId}} placeholder in a node's prompt is replaced with the content output of the named upstream node. Each node is a full agent run, so it can use tools, browse the web, and access all the capabilities of the target agent.
Knowledge Bases
Organize documents into searchable collections that agents can reference.
List knowledge bases:
GET /v1/knowledge/bases
Create a knowledge base:
POST /v1/knowledge/bases
Content-Type: application/json
{ "name": "Research Papers" }
Returns 201 with the new base ID.
Upload a document to a knowledge base:
POST /v1/knowledge/bases/{id}/documents
Content-Type: application/json
{
"fileName": "research-paper.pdf",
"contentType": "application/pdf",
"dataUrl": "data:application/pdf;base64,JVBERi0xLjQ...",
"description": "Renewable energy trends 2026"
}
The dataUrl field is a base64-encoded data URL. Returns 201 with the document metadata.
List documents in a knowledge base:
GET /v1/knowledge/bases/{id}/documents
Search within a knowledge base:
POST /v1/knowledge/bases/{id}/search
Content-Type: application/json
{ "query": "renewable energy" }
Returns matching documents based on file name and description. Semantic (vector) search is coming in a future release.
Delete a document or knowledge base:
DELETE /v1/knowledge/bases/{id}/documents/{docId}
DELETE /v1/knowledge/bases/{id}
Export & Import Agents
Share agents as portable packages — across devices, teams, or the Community Hub.
Export an agent:
POST /v1/agents/{id}/export
Returns a JSON package containing the agent definition, tool requirements (derived from enabled tools), connector requirements (which OAuth providers are needed), and trigger templates. Sync metadata is stripped — the package is a clean, self-contained blueprint.
Import an agent:
POST /v1/agents/import
Content-Type: application/json
{
"package": {
"$schema": "pebbleflow.agent.package/v1",
"agent": {
"id": "shared-research-agent",
"branding": { "name": "Research Agent", "description": "From the team" },
"defaultSettings": { "systemPrompt": "You research things." },
"settingLevels": {}
},
"toolRequirements": [
{ "toolId": "web_browsing", "enabled": true },
{ "toolId": "search_tools", "enabled": true }
]
}
}
Returns 201 with the installed agent. ID collisions with builtins or existing agents return 409.
Error Handling
The API uses standard HTTP status codes:
| Code | Meaning |
|---|---|
200 |
Success |
201 |
Created |
202 |
Accepted (async operation started) |
204 |
Deleted (no content) |
400 |
Bad request — check the error field for details |
401 |
Unauthorized — missing or invalid session secret |
403 |
Forbidden — e.g., trying to modify a builtin agent |
404 |
Not found |
409 |
Conflict — e.g., agent ID already exists |
422 |
Validation error — input doesn't match tool schema |
500 |
Server error — check the error field |
501 |
Not implemented — feature exists but isn't available this way |
503 |
Service unavailable — storage or provider not ready |
All error responses include { "error": "human-readable message" }.
Quick Start Example
Here's a complete workflow: create an agent, run it, and stream the results.
# 1. Create a custom agent
curl -X POST http://localhost:3847/v1/agents \
-H "Authorization: Bearer $API_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"id": "quick-summarizer",
"branding": { "name": "Quick Summarizer" },
"defaultSettings": {
"systemPrompt": "Summarize any input concisely in 3 bullet points.",
"enabledTools": { "web_browsing": true }
},
"settingLevels": {}
}'
# 2. Run it asynchronously
RUN=$(curl -s -X POST http://localhost:3847/v1/runs \
-H "Authorization: Bearer $API_TOKEN" \
-H "Content-Type: application/json" \
-d '{ "agentId": "quick-summarizer", "input": { "message": "Summarize https://en.wikipedia.org/wiki/Artificial_intelligence" }, "mode": "async" }')
RUN_ID=$(echo $RUN | jq -r '.runId')
# 3. Stream the events
curl -N http://localhost:3847/v1/runs/$RUN_ID/events \
-H "Authorization: Bearer $API_TOKEN"
# 4. Export the agent for sharing
curl -X POST http://localhost:3847/v1/agents/quick-summarizer/export \
-H "Authorization: Bearer $API_TOKEN"
This guide is maintained by the PebbleFlow team using Slate, our built-in editor.