API Reference
All API endpoints require authentication unless noted otherwise. Unauthenticated requests receive a 401 Unauthorized response. Authenticate by including a valid session cookie (set during login via the web UI).
Some endpoints are admin only — non-admin users receive a 403 Forbidden response.
WebSocket (Chat)
Section titled “WebSocket (Chat)”Agent chat is handled over WebSocket at /api/ws, not via REST. The browser sends JSON messages and receives streaming responses. For details on the WebSocket protocol, message types, and session management, see the Architecture page.
Health
Section titled “Health”GET /api/health
Section titled “GET /api/health”Service health check. Public — no authentication required. Suitable for load-balancer probes and uptime monitoring.
Response: 200 OK when Pinchy is running. 503 Service Unavailable when upstream dependencies (database, OpenClaw) are unreachable.
GET /api/health/openclaw
Section titled “GET /api/health/openclaw”OpenClaw gateway reachability check. Public — no authentication required.
Useful for monitoring the WebSocket gateway separately from the web process.
Setup (public)
Section titled “Setup (public)”Setup endpoints handle the initial installation and first-admin account. They are accessible only before setup has completed — once an admin exists, they return 409 Conflict.
GET /api/setup/status
Section titled “GET /api/setup/status”Check whether setup is required.
Response:
{ "setupComplete": false, "providerConfigured": false}POST /api/setup
Section titled “POST /api/setup”Create the first admin account.
Request body:
| Field | Type | Required | Description |
|---|---|---|---|
name | string | Yes | Admin display name |
email | string | Yes | Admin email address |
password | string | Yes | Admin password (min 8 chars) |
Response: 201 Created. A session cookie is set — the admin is logged in immediately.
POST /api/setup/provider
Section titled “POST /api/setup/provider”Configure the first LLM provider as part of the setup flow.
Request body:
| Field | Type | Required | Description |
|---|---|---|---|
provider | string | Yes | anthropic, openai, google, ollama-cloud, ollama-local |
apiKey | string | Cond. | Required for cloud providers |
baseUrl | string | Cond. | Required for ollama-local |
Response: { "success": true }
Agents
Section titled “Agents”GET /api/agents
Section titled “GET /api/agents”List agents visible to the current user. Admins see all agents. Non-admin users see shared agents and their own personal agents.
Response:
[ { "id": "uuid", "name": "HR Policy Assistant", "model": "anthropic/claude-haiku-4-5-20251001", "templateId": "knowledge-base", "allowedTools": ["pinchy_ls", "pinchy_read"], "pluginConfig": { "allowed_paths": ["/data/hr-policies"] }, "isPersonal": false, "ownerId": "user-uuid", "createdAt": "2025-01-15T10:00:00.000Z", "updatedAt": "2025-01-15T10:00:00.000Z" }]POST /api/agents
Section titled “POST /api/agents”Create a new agent. The agent inherits default tool permissions from its template. Use PATCH /api/agents/:id to configure permissions after creation.
Request body:
| Field | Type | Required | Description |
|---|---|---|---|
name | string | Yes | Display name for the agent |
templateId | string | Yes | Template to use (knowledge-base or custom) |
Example — Knowledge Base agent:
{ "name": "HR Policy Assistant", "templateId": "knowledge-base"}Example — Custom agent:
{ "name": "General Assistant", "templateId": "custom"}Response: 201 Created with the created agent object.
Errors:
400— missing or invalidnameortemplateId401— not authenticated
GET /api/agents/:id
Section titled “GET /api/agents/:id”Get a single agent by ID.
Response: The agent object, or 404 if not found.
PATCH /api/agents/:id
Section titled “PATCH /api/agents/:id”Update an agent’s settings. Any combination of fields can be included.
Request body:
| Field | Type | Required | Description |
|---|---|---|---|
name | string | No | New display name |
model | string | No | New model identifier |
allowedTools | string[] | No | List of tool IDs the agent can use (admin only) |
pluginConfig | object | No | Plugin configuration, e.g. { "allowed_paths": ["/data/hr"] } (admin only) |
Example — update permissions:
{ "allowedTools": ["pinchy_ls", "pinchy_read"], "pluginConfig": { "allowed_paths": ["/data/hr-policies"] }}Response: The updated agent object.
Errors:
400— cannot change permissions for personal agents401— not authenticated403— only admins can changeallowedToolsorpluginConfig404— agent not found
DELETE /api/agents/:id
Section titled “DELETE /api/agents/:id”Delete an agent. Admin only. Personal agents (auto-created Smithers) cannot be deleted.
Response: 200 OK
{ "success": true}Errors:
400— attempting to delete a personal agent401— not authenticated403— not an admin404— agent not found
Templates
Section titled “Templates”GET /api/templates
Section titled “GET /api/templates”List available agent templates.
Response:
{ "templates": [ { "id": "knowledge-base", "name": "Knowledge Base", "description": "Answer questions from your docs" }, { "id": "custom", "name": "Custom Agent", "description": "Start from scratch" } ]}Data Directories
Section titled “Data Directories”GET /api/data-directories
Section titled “GET /api/data-directories”List directories available under /data/ for agent configuration.
Response:
{ "directories": [ { "path": "/data/hr-policies", "name": "hr-policies" }, { "path": "/data/engineering-docs", "name": "engineering-docs" } ]}Returns an empty array if /data/ does not exist or contains no subdirectories. Hidden directories (starting with .) are excluded.
Settings
Section titled “Settings”GET /api/settings
Section titled “GET /api/settings”Get all application settings. Encrypted values (such as API keys) are masked in the response.
Response:
[ { "key": "default_provider", "value": "anthropic", "encrypted": false }, { "key": "anthropic_api_key", "value": "--------", "encrypted": true }]POST /api/settings
Section titled “POST /api/settings”Update a setting.
Request body:
| Field | Type | Required | Description |
|---|---|---|---|
key | string | Yes | Setting key |
value | string | Yes | Setting value |
Settings with api_key in the key name are automatically encrypted at rest.
Response: { "success": true }
LLM Providers (admin only)
Section titled “LLM Providers (admin only)”GET /api/settings/providers
Section titled “GET /api/settings/providers”List configured LLM providers. Credentials are masked.
Response:
[ { "provider": "anthropic", "configured": true, "isDefault": true }, { "provider": "openai", "configured": true, "isDefault": false }]POST /api/settings/providers
Section titled “POST /api/settings/providers”Add or update a provider.
Request body:
| Field | Type | Required | Description |
|---|---|---|---|
provider | string | Yes | Provider identifier |
apiKey | string | Cond. | API key (required for cloud providers) |
baseUrl | string | Cond. | URL (required for ollama-local) |
isDefault | boolean | No | Mark as the default provider for new agents |
Pinchy validates the credentials by calling the provider’s /models endpoint before persisting. API keys are encrypted at rest with AES-256-GCM.
DELETE /api/settings/providers?provider=<id>
Section titled “DELETE /api/settings/providers?provider=<id>”Remove a provider. Any agent still using a model from this provider will fail to start chats until reassigned.
GET /api/providers/models
Section titled “GET /api/providers/models”List available models across all configured providers. Populates the model dropdown in agent settings.
Response:
{ "models": [ { "id": "claude-sonnet-4-6", "provider": "anthropic", "displayName": "Claude Sonnet 4.6", "toolUse": true } ]}The response is cached for one hour for cloud providers. Local Ollama is always fetched live.
Domain Lock (admin only)
Section titled “Domain Lock (admin only)”GET /api/settings/domain
Section titled “GET /api/settings/domain”Get the current domain lock status.
Response:
{ "lockedDomain": "pinchy.example.com", "insecureMode": false}PUT /api/settings/domain
Section titled “PUT /api/settings/domain”Set or clear the locked domain. See Lock Pinchy to a Domain for the full flow and safety guarantees.
Request body:
| Field | Type | Required | Description |
|---|---|---|---|
lockedDomain | string|null | Yes | Canonical HTTPS hostname. Pass null to disable domain locking. |
Organization Context (admin only)
Section titled “Organization Context (admin only)”GET /api/settings/context
Section titled “GET /api/settings/context”Get the organization context. Admin only.
Response:
{ "content": "Acme Corp is a SaaS company focused on..."}Returns { "content": "" } if no org context has been set.
PUT /api/settings/context
Section titled “PUT /api/settings/context”Update the organization context. This is synced to all shared agent workspaces and triggers a runtime restart.
Request body:
| Field | Type | Required | Description |
|---|---|---|---|
content | string | Yes | Organization context (Markdown) |
Response: { "success": true }
Errors:
400— content is not a string401— not authenticated403— not an admin
Users (admin only)
Section titled “Users (admin only)”GET /api/users
Section titled “GET /api/users”List all users.
Response:
{ "users": [ { "id": "uuid", "name": "Alice", "email": "alice@example.com", "role": "admin", "banned": false, "groups": [{ "id": "group-uuid", "name": "Engineering" }] } ]}DELETE /api/users/:id
Section titled “DELETE /api/users/:id”Deactivate a user (soft delete — sets banned status). Their personal agents are removed from the shared view but the account record is retained and can be reactivated via POST /api/users/:id/reactivate.
Response: 200 OK
{ "success": true}Errors:
400— attempting to delete yourself401— not authenticated403— not an admin404— user not found
PATCH /api/users/:id
Section titled “PATCH /api/users/:id”Update a user’s role. Admin only.
Request body:
| Field | Type | Required | Description |
|---|---|---|---|
role | string | Yes | New role (admin or member) |
Response:
{ "success": true}Errors:
400— invalid role, attempting to change your own role, or demoting the last admin401— not authenticated403— not an admin404— user not found
POST /api/users/:id/reactivate
Section titled “POST /api/users/:id/reactivate”Reactivate a deactivated user. Admin only.
Response:
{ "success": true}Errors:
401— not authenticated403— not an admin404— user not found or user is not deactivated
POST /api/users/:id/reset
Section titled “POST /api/users/:id/reset”Generate a password reset token for a user. The admin constructs the reset URL from the returned token: {origin}/invite/{token}.
Response: 201 Created
{ "token": "abc123..."}Errors:
401— not authenticated403— not an admin404— user not found
Invites (admin only)
Section titled “Invites (admin only)”POST /api/users/invite
Section titled “POST /api/users/invite”Create an invite for a new user. Returns the full invite object including a plaintext token (shown only once).
Request body:
| Field | Type | Required | Description |
|---|---|---|---|
role | string | Yes | Role for the invited user (admin or member) |
email | string | No | Email address of the invited user (optional, for reference) |
groupIds | string[] | No | Group IDs to assign the user to on account creation |
Response: 201 Created
{ "id": "uuid", "token": "abc123...", "email": "bob@example.com", "role": "member", "type": "invite", "groups": [{ "id": "group-uuid", "name": "Engineering" }], "expiresAt": "2025-01-22T10:00:00.000Z", "createdAt": "2025-01-15T10:00:00.000Z"}The admin constructs the invite URL from the returned token: {origin}/invite/{token}.
Errors:
400— missing or invalid role401— not authenticated403— not an admin
GET /api/users/invites
Section titled “GET /api/users/invites”List all invites and their status.
Response:
[ { "id": "uuid", "email": "bob@example.com", "role": "member", "status": "pending", "groups": [{ "id": "group-uuid", "name": "Engineering" }], "expiresAt": "2025-01-22T10:00:00.000Z", "createdAt": "2025-01-15T10:00:00.000Z" }]DELETE /api/users/invites/:id
Section titled “DELETE /api/users/invites/:id”Revoke a pending invite.
Response: 200 OK
{ "success": true}Errors:
401— not authenticated403— not an admin404— invite not found
Invite Claim (public)
Section titled “Invite Claim (public)”POST /api/invite/claim
Section titled “POST /api/invite/claim”Claim an invite token to create a new account or reset a password. This endpoint does not require authentication.
Request body:
| Field | Type | Required | Description |
|---|---|---|---|
token | string | Yes | The invite/reset token from the URL |
name | string | Yes | Display name for the new user |
password | string | Yes | Password (min 8 characters) |
Response: 201 Created
{ "success": true}Errors:
400— missing fields, invalid token, or token expired
Telegram Channels
Section titled “Telegram Channels”Connect agents to Telegram bots so users can chat with them from their phone. See Set Up Telegram for the end-to-end flow.
GET /api/agents/:agentId/channels/telegram
Section titled “GET /api/agents/:agentId/channels/telegram”Get the Telegram bot configuration for an agent. Admin only.
Response (not configured):
{ "configured": false, "mainBotConfigured": true }Response (configured):
{ "configured": true, "hint": "8a2f", "mainBotConfigured": true }hint is the last 4 characters of the bot token for visual verification. mainBotConfigured indicates whether the global Smithers/main bot is set up — a prerequisite before any other agent can connect to Telegram.
POST /api/agents/:agentId/channels/telegram
Section titled “POST /api/agents/:agentId/channels/telegram”Connect an agent to a Telegram bot. Admin only. The main bot must be configured first (exception: the main bot itself).
Request body:
| Field | Type | Required | Description |
|---|---|---|---|
botToken | string | Yes | Bot token from @BotFather |
Response:
{ "botUsername": "support_pinchy_bot", "botId": 123456789 }Errors:
400— invalid or non-working bot token409—telegram_not_configured(main bot missing), or the bot token is already used by another agent
DELETE /api/agents/:agentId/channels/telegram
Section titled “DELETE /api/agents/:agentId/channels/telegram”Disconnect a Telegram bot from an agent. Admin only. Personal (Smithers) agents cannot be disconnected individually — use the “Remove Telegram for everyone” action instead.
GET /api/settings/telegram
Section titled “GET /api/settings/telegram”Get the current user’s Telegram link status. Available to any authenticated user.
Response:
{ "linked": true, "channelUserId": "123456789" }POST /api/settings/telegram
Section titled “POST /api/settings/telegram”Link the current user’s Telegram account using a pairing code received from the bot.
Request body:
| Field | Type | Required | Description |
|---|---|---|---|
code | string | Yes | Pairing code shown in Telegram after DM-ing bot |
Response: { "linked": true, "telegramUserId": "..." }
Errors:
400— invalid or expired pairing code
DELETE /api/settings/telegram
Section titled “DELETE /api/settings/telegram”Unlink the current user’s Telegram account.
GET /api/settings/telegram/bots
Section titled “GET /api/settings/telegram/bots”List all Telegram bots connected across agents, with the agent each bot belongs to. Admin only.
DELETE /api/settings/telegram/all
Section titled “DELETE /api/settings/telegram/all”Remove Telegram from every agent at once. Admin only. This is the backend for the “Remove Telegram for everyone” flow in Settings → Telegram.
Integrations (admin only)
Section titled “Integrations (admin only)”External system integrations — currently Odoo. See Integrations for the conceptual overview and Connect Odoo for the setup guide.
GET /api/integrations
Section titled “GET /api/integrations”List all integration connections with masked credentials.
POST /api/integrations
Section titled “POST /api/integrations”Create a new integration connection.
Request body (Odoo):
{ "type": "odoo", "name": "Production Odoo", "description": "Main company instance", "credentials": { "url": "https://odoo.example.com", "db": "production", "login": "api-user@example.com", "apiKey": "..." }}Credentials are encrypted at rest with AES-256-GCM.
Response: 201 Created — the created connection with masked credentials.
Errors:
400— validation failed (bad URL, missing fields) or the URL resolves to a disallowed address (SSRF guard)
GET /api/integrations/:connectionId
Section titled “GET /api/integrations/:connectionId”Get a single connection (credentials masked).
PATCH /api/integrations/:connectionId
Section titled “PATCH /api/integrations/:connectionId”Update a connection’s name, description, or credentials.
DELETE /api/integrations/:connectionId
Section titled “DELETE /api/integrations/:connectionId”Delete a connection. Agents that reference it lose access immediately.
POST /api/integrations/:connectionId/sync
Section titled “POST /api/integrations/:connectionId/sync”Re-probe the external system: rediscover available models and recheck access rights. Permissions that no longer apply are removed.
POST /api/integrations/:connectionId/test
Section titled “POST /api/integrations/:connectionId/test”Test credentials against the external system without persisting changes. Used by the setup wizard for live feedback.
GET /api/agents/:agentId/integrations
Section titled “GET /api/agents/:agentId/integrations”Get an agent’s integration permissions: which connections are enabled, access level (read-only, read-write, full, custom), and which data models are allowed.
PATCH /api/agents/:agentId/integrations
Section titled “PATCH /api/agents/:agentId/integrations”Update an agent’s integration permissions.
Usage Dashboard (admin only)
Section titled “Usage Dashboard (admin only)”Token usage and estimated cost aggregates. See Usage & Costs Dashboard for the UI.
GET /api/usage/summary
Section titled “GET /api/usage/summary”Per-agent usage totals within a time window.
Query parameters:
| Parameter | Type | Default | Description |
|---|---|---|---|
days | number | 30 | Look-back window. 0 means all-time |
agentId | string | — | Filter by a single agent |
Response:
{ "agents": [ { "agentId": "uuid", "agentName": "Smithers", "totalInputTokens": "15432", "totalOutputTokens": "3210", "totalCacheReadTokens": "120", "totalCacheWriteTokens": "45", "totalCost": "0.08", "deleted": false } ], "totals": { "chat": { "inputTokens": "...", "outputTokens": "...", "cacheReadTokens": "...", "cacheWriteTokens": "...", "cost": "..." }, "system": { "inputTokens": "...", "outputTokens": "...", "cacheReadTokens": "...", "cacheWriteTokens": "...", "cost": "..." }, "plugin": { "inputTokens": "...", "outputTokens": "...", "cacheReadTokens": "...", "cacheWriteTokens": "...", "cost": "..." } }}Source buckets classify where the tokens were consumed:
- chat — direct user ↔ agent conversation
- system — background work such as Smithers onboarding or summarization
- plugin — work driven by a plugin subagent call
GET /api/usage/by-user
Section titled “GET /api/usage/by-user”Usage broken down by user. Enterprise license required.
GET /api/usage/timeseries
Section titled “GET /api/usage/timeseries”Usage over time — used to render the dashboard charts. Query parameters: days, agentId, and interval (day or hour).
GET /api/usage/export
Section titled “GET /api/usage/export”Export usage records as CSV. Enterprise license required.
Groups (admin only, enterprise)
Section titled “Groups (admin only, enterprise)”All group endpoints require an enterprise license. Requests without a valid license receive a 403 Forbidden response.
GET /api/groups
Section titled “GET /api/groups”List all groups with member counts. Admin only.
Response:
[ { "id": "uuid", "name": "Engineering", "description": "Backend and frontend engineers", "memberCount": 5 }]POST /api/groups
Section titled “POST /api/groups”Create a new group. Admin only.
Request body:
| Field | Type | Required | Description |
|---|---|---|---|
name | string | Yes | Group name |
description | string | No | Group description |
Response: 201 Created with the created group object.
Errors:
400— missing or invalid name401— not authenticated403— not an admin or no enterprise license
PATCH /api/groups/:id
Section titled “PATCH /api/groups/:id”Update a group. Admin only.
Request body:
| Field | Type | Required | Description |
|---|---|---|---|
name | string | No | New group name |
description | string | No | New group description |
Response: The updated group object.
Errors:
400— invalid fields401— not authenticated403— not an admin or no enterprise license404— group not found
DELETE /api/groups/:id
Section titled “DELETE /api/groups/:id”Delete a group. Admin only.
Response:
{ "success": true}Errors:
401— not authenticated403— not an admin or no enterprise license404— group not found
GET /api/groups/:id/members
Section titled “GET /api/groups/:id/members”List members of a group. Admin only.
Response:
[ { "userId": "user-uuid", "groupId": "group-uuid" }]Errors:
401— not authenticated403— not an admin or no enterprise license404— group not found
PUT /api/groups/:id/members
Section titled “PUT /api/groups/:id/members”Replace the members of a group. Admin only.
Request body:
| Field | Type | Required | Description |
|---|---|---|---|
userIds | string[] | Yes | User IDs to set as group members |
Response:
{ "success": true}Errors:
400— invalid or missing userIds401— not authenticated403— not an admin or no enterprise license404— group not found
Audit Trail (admin only)
Section titled “Audit Trail (admin only)”GET /api/audit
Section titled “GET /api/audit”Retrieve a paginated, filterable audit log. Admin only.
Query parameters:
| Parameter | Type | Default | Description |
|---|---|---|---|
page | number | 1 | Page number |
limit | number | 50 | Entries per page |
eventType | string | — | Filter by event type (e.g., auth.login, tool.shell) |
actorId | string | — | Filter by user ID |
from | string | — | Start date (ISO 8601) |
to | string | — | End date (ISO 8601) |
Response:
{ "entries": [ { "id": 1, "timestamp": "2026-02-21T10:00:00.000Z", "actorType": "user", "actorId": "user-uuid", "actorName": "Alice", "actorDeleted": false, "eventType": "auth.login", "resource": "user:user-uuid", "resourceName": "Alice", "resourceDeleted": false, "detail": {}, "rowHmac": "sha256-hex-string" } ], "total": 142, "page": 1, "limit": 50}Errors:
401— not authenticated403— not an admin
GET /api/audit/verify
Section titled “GET /api/audit/verify”Verify the integrity of audit log entries by recomputing HMAC signatures. Admin only.
Query parameters:
| Parameter | Type | Default | Description |
|---|---|---|---|
fromId | string | — | Start verification from this entry ID (optional) |
toId | string | — | End verification at this entry ID (optional) |
Response:
{ "valid": true, "totalChecked": 142, "invalidIds": []}If tampered entries are found, valid is false and invalidIds contains the IDs of entries with mismatched signatures.
Errors:
401— not authenticated403— not an admin
GET /api/audit/event-types
Section titled “GET /api/audit/event-types”List the distinct event types present in the current audit log. Useful for populating filter dropdowns without enumerating the full schema. Admin only.
Response:
{ "eventTypes": [ "auth.login", "auth.failed", "agent.created", "channel.created", "tool.shell", "tool.denied" ]}GET /api/audit/export
Section titled “GET /api/audit/export”Export the audit log as a CSV file. Supports the same filters as GET /api/audit. Admin only.
Query parameters:
| Parameter | Type | Default | Description |
|---|---|---|---|
eventType | string | — | Filter by event type |
actorId | string | — | Filter by user ID |
from | string | — | Start date (ISO 8601) |
to | string | — | End date (ISO 8601) |
Response: 200 OK with Content-Type: text/csv and Content-Disposition: attachment; filename="audit-log.csv".
Errors:
401— not authenticated403— not an admin
User Self-Service
Section titled “User Self-Service”PATCH /api/users/me
Section titled “PATCH /api/users/me”Update your own display name.
Request body:
| Field | Type | Required | Description |
|---|---|---|---|
name | string | Yes | New display name |
Response:
{ "success": true}Errors:
400— missing or empty name401— not authenticated
GET /api/users/me/context
Section titled “GET /api/users/me/context”Get your personal context.
Response:
{ "content": "I'm Alice, a product manager at Acme Corp..."}Returns { "content": "" } if no context has been set.
PUT /api/users/me/context
Section titled “PUT /api/users/me/context”Update your personal context. This is synced to your Smithers agent’s workspace and triggers a runtime restart.
Request body:
| Field | Type | Required | Description |
|---|---|---|---|
content | string | Yes | Your personal context (Markdown) |
Response: { "success": true }
Errors:
400— content is not a string401— not authenticated
POST /api/users/me/password
Section titled “POST /api/users/me/password”Change your own password.
Request body:
| Field | Type | Required | Description |
|---|---|---|---|
currentPassword | string | Yes | Current password |
newPassword | string | Yes | New password (min 8 characters) |
Response: { "success": true }
Errors:
400— missing fields or new password too short401— not authenticated or current password incorrect