Skip to content

External API & Tool Integration

Document 09 | Version: 1.0.0 | Date: April 2026 | Status: Active

This document defines Thinklio's external API surfaces, the tool integration framework, and the MCP server reference. It covers the three API surfaces (Channel, Platform, and Integration), the complete endpoint reference, the accounts and onboarding API, the developer guide for building tool integrations via MCP, and the reference documentation for the platform's built-in MCP servers.

The API strategy establishes three distinct surfaces serving different integration patterns: conversational access (Channel API), programmatic orchestration (Platform API), and bidirectional capability exchange (Integration API). All three surfaces resolve to the same authorisation model and governance framework. The tool integration guide explains how external developers build MCP servers that expose third-party API operations as tools available to Thinklio agents. The MCP server reference documents the three production MCP servers (Cliniko, Notion, Xero) maintained in the thinklio-services repository.

For the agent architecture and tool execution model, see 03 Agent Architecture & Extensibility. For entity definitions (Tool, AgentTool), see 04 Data Model. For credential storage via the secrets vault, see 07 Security & Governance. For the agent catalogue and platform services layer, see 08 Agents Catalogue & Platform Services.


Table of Contents

  1. API Strategy & Surface Design
  2. Common Conventions
  3. Channel API -- Chatal Access
  4. Platform API -- Orchestration and Management
  5. Integration API -- Bidirectional Capability Exchange
  6. Internal Service APIs
  7. Complete Endpoint Reference
  8. Accounts, Onboarding & Team Management
  9. Tool Integration Developer Guide
  10. MCP Server Reference
  11. Implementation Phases
  12. Revision History

1. API Strategy & Surface Design

Thinklio exposes three distinct API surfaces, each serving a different relationship between the platform and the outside world. These are not three versions of the same API. They represent fundamentally different integration patterns with different consumers, different interaction models, and different architectural implications.

Every architectural decision, data model design, and governance mechanism should be evaluated against all three API surfaces. If a design choice makes one surface awkward or impossible, that is a signal to reconsider the design, not to deprioritise the surface.

1.1 Channel API -- Chatal Access

A messaging interface. An external system sends messages to an agent and receives responses, the same way a user would interact via Telegram or web chat.

Who uses it: Applications that embed Thinklio agents into their own user experience. A mobile app with an in-app assistant. A SaaS product that offers AI-powered support through its own interface. An internal tool that provides a chat widget backed by a Thinklio agent.

Interaction model: Chatal. The external system acts on behalf of a user. Messages go in, responses come back. Sessions maintain conversational context across messages. The external system is a channel adapter, the same as Telegram or web chat, just accessed programmatically.

What the architecture must preserve: Channel agnosticism. Any feature that works in Telegram must work through the Channel API. Any governance rule enforced in web chat must be enforced through the Channel API. The agent must be unaware of which channel is in use.

1.2 Platform API -- Orchestration and Management

Programmatic access to Thinklio's orchestration capabilities. Creating agents, dispatching jobs, registering observers, managing knowledge, querying usage, and controlling the full lifecycle of agents and their work.

Who uses it: Service providers building on Thinklio. Operations teams automating agent deployment. Dashboards and reporting tools. CI/CD pipelines that deploy agent configurations. External workflow engines that coordinate with Thinklio agents as part of a larger process.

Interaction model: Command and query. The external system is not talking to an agent -- it is managing agents, their work, and their environment.

Key capabilities: Agent lifecycle management (create, configure, pause, resume, archive agents; assign agents to contexts; update system prompts, tool assignments, and capability levels -- the API equivalent of Agent Studio). Job dispatch and observation (create jobs directed at specific agents; provide structured briefs as dispatch payloads; register webhook observers; query job status, subjob progress, and results; cancel jobs). Knowledge management (add, query, and manage knowledge facts across all four layers within the consumer's authorisation scope; bulk import and export). Usage and governance (query usage data, budget status, and cost attribution; adjust budgets and policies; access audit logs).

What the architecture must preserve: The job system's observer model must support webhook observers from the start, not just agent observers. The event model must support external event delivery (webhooks) alongside internal distribution via Convex reactive queries (see 06 Events, Channels & Messaging). Cost attribution must handle API-initiated jobs the same way it handles agent-initiated jobs.

1.3 Integration API -- Bidirectional Capability Exchange

A registration and execution interface that allows external systems to offer their capabilities to Thinklio agents, and to consume Thinklio agent capabilities in return.

Who uses it: CRM systems that register as tools so agents can query customer records. Project management tools that register so agents can create and update tasks. ERP systems, healthcare platforms, document management systems -- any external system that wants to be accessible to Thinklio agents. And conversely, any external system that wants to invoke Thinklio agents as capabilities within its own workflows.

Interaction model: Bidirectional registration and execution. An external system registers a tool definition (parameter schema, return schema, endpoint, trust level requirements) and Thinklio agents can then invoke it through the standard tool execution path. In the other direction, an external system can register a Thinklio agent as a capability in its own orchestration, invoking it through the Platform API's job dispatch mechanism.

Key capabilities: Tool registration (self-service with governance approval). Tool execution contract (schemas, authentication, error handling defined at registration time, enforced at execution time). Capability advertisement (external systems query what agents are available and invoke them as needed). Webhook and event subscription (external systems subscribe to relevant Thinklio events -- job state changes, agent lifecycle events, knowledge updates).

What the architecture must preserve: Tool execution governance must apply identically to dynamically registered external tools and platform-provided internal tools. Trust levels, rate limits, budget checks, and audit logging must not differ based on how the tool was registered. External tool health must be monitored, and unhealthy tools must be circuit-broken.

1.4 Cross-Cutting Concerns

Authentication and Authorisation

Each API surface has different authentication needs, but all resolve to the same authorisation model.

  • Channel API: Authenticates as a user (or on behalf of a user). Credentials map to a platform user identity. All governance applies.
  • Platform API: Authenticates as an account or service account. API keys scoped to specific accounts, teams, or agents.
  • Integration API: Authenticates as an external system. Registration requires account approval. Execution credentials are scoped to specific tool definitions.

The policy engine evaluates all three surfaces uniformly. An action blocked by account policy is blocked whether it originates from a Telegram message, a Platform API call, or an Integration API tool execution.

Governance

All three surfaces are subject to the same governance framework: budget enforcement (API-initiated interactions and jobs consume budget the same as conversational interactions), policy evaluation (tool restrictions, content rules, and approval requirements apply regardless of the API surface), audit trail (every action through every surface is logged with full context, including the originating surface and authentication identity), and rate limiting (each surface has appropriate rate limits, configured independently but enforced by the same mechanism).

Cost Attribution

API-initiated work must attribute costs to the correct entity. Channel API costs are attributed to the user on whose behalf the message was sent. Platform API costs are attributed to the account or team associated with the API key. Integration API tool execution costs are attributed to the agent's interaction context; external systems invoking Thinklio agents via job dispatch are attributed to the external system's associated account.

Versioning

All external APIs are versioned: /v1/.... Each API surface is versioned independently. The Channel API may evolve slowly (messaging is a stable interface). The Platform API may evolve more quickly as orchestration capabilities expand. The Integration API may evolve as the tool registration model matures.

Breaking changes to any surface require a new version with a deprecation period (minimum 6 months) for the previous version. Internal service-to-service APIs are not URL-versioned but use schema versioning in event metadata. Breaking changes to internal APIs require coordinated deployment.

2. Common Conventions

2.1 Authentication

All external API requests require authentication via Bearer token:

Authorization: Bearer {jwt_or_api_key}

Authentication resolves differently per surface: the Channel API authenticates as a user (or on behalf of a user), the Platform API authenticates as an account or service account with scoped API keys, and the Integration API authenticates as an external system with credentials scoped to specific tool registrations.

2.2 Common Response Format

{
    "data": { },
    "meta": {
        "request_id": "uuid",
        "timestamp": "2026-03-14T10:00:00Z"
    }
}

Error responses:

{
    "error": {
        "code": "validation_error",
        "message": "Human-readable description",
        "details": { }
    },
    "meta": {
        "request_id": "uuid",
        "timestamp": "2026-03-14T10:00:00Z"
    }
}

HTTP status codes follow standard conventions: 200 (success), 201 (created), 202 (accepted), 400 (bad request), 401 (unauthorised), 403 (forbidden), 404 (not found), 409 (conflict), 410 (expired), 429 (rate limited), 500 (internal error).


3. Channel API -- Chatal Access

The Channel API provides a messaging interface for external systems to interact with agents conversationally on behalf of users. It follows the same event flow as native channels (Telegram, web chat).

3.1 Send Message to Agent

POST /v1/channels/messages
Body:
{
    "agent_id": "uuid",
    "content": "Research the latest developments in battery technology",
    "session_id": "uuid (optional, auto-created if omitted)",
    "channel_metadata": { }
}

Response 202:
{
    "data": {
        "interaction_id": "uuid",
        "session_id": "uuid",
        "status": "pending"
    }
}

3.2 Get Interaction Status and Response

GET /v1/channels/interactions/{interaction_id}

Response 200:
{
    "data": {
        "id": "uuid",
        "agent_id": "uuid",
        "state": "success",
        "steps": [
            {
                "step_type": "context",
                "state": "success",
                "execution_mode": null,
                "cost": 0.0
            },
            {
                "step_type": "think",
                "state": "success",
                "execution_mode": null,
                "cost": 0.003
            },
            {
                "step_type": "act",
                "state": "success",
                "execution_mode": "deferred",
                "cost": 0.0,
                "job_id": "uuid"
            },
            {
                "step_type": "respond",
                "state": "success",
                "execution_mode": null,
                "cost": 0.0
            }
        ],
        "total_cost": 0.003,
        "response": {
            "text": "I've started researching that. I'll send you the results when they're ready."
        },
        "started_at": "...",
        "completed_at": "..."
    }
}

3.3 Get Chat History

GET /v1/channels/sessions/{session_id}/messages
Query: ?limit=50&before=timestamp

Response 200:
{
    "data": [
        {
            "role": "user",
            "content": "...",
            "timestamp": "..."
        },
        {
            "role": "assistant",
            "content": "...",
            "timestamp": "...",
            "interaction_id": "uuid"
        }
    ]
}

3.4 Channel Connection

POST /v1/channels/connect
Body:
{
    "channel_type": "telegram",
    "external_id": "123456789",
    "display_name": "My Telegram"
}

Response 201:
{
    "id": "uuid",
    "status": "pending_verification",
    "message": "Verification code sent to j***n@example.com"
}
GET /v1/channels/mine

Response 200:
[
    {
        "id": "uuid",
        "channel_type": "telegram",
        "external_id": "123456789",
        "status": "active",
        "display_name": "My Telegram",
        "verified_at": "2025-01-01T00:00:00Z",
        "last_active_at": "2025-01-02T12:00:00Z"
    }
]

4. Platform API -- Orchestration and Management

The Platform API provides programmatic access to agent lifecycle management, job dispatch and observation, knowledge management, and usage reporting.

4.1 Agent Management

List agents

GET /v1/agents
Query: ?status=active&limit=20&offset=0

Response 200:
{
    "data": [
        {
            "id": "uuid",
            "name": "Customer Support Agent",
            "slug": "customer-support",
            "description": "Handles customer inquiries",
            "capability_level": "workflow",
            "status": "active",
            "created_at": "2026-03-14T10:00:00Z"
        }
    ],
    "meta": { "total": 5, "limit": 20, "offset": 0 }
}

Create agent

POST /v1/agents
Body:
{
    "name": "Research Assistant",
    "slug": "research-assistant",
    "description": "Helps with research tasks",
    "system_prompt": "You are a research assistant...",
    "capability_level": "workflow",
    "llm_model": "anthropic/claude-sonnet-4-20250514",
    "template_id": "uuid (optional)"
}

Response 201:
{
    "data": {
        "id": "uuid",
        "name": "Research Assistant",
        "slug": "research-assistant",
        ...
    }
}

Get agent

GET /v1/agents/{agent_id}

Response 200:
{
    "data": {
        "id": "uuid",
        "name": "Research Assistant",
        "slug": "research-assistant",
        "description": "...",
        "system_prompt": "...",
        "capability_level": "workflow",
        "llm_provider": "openrouter",
        "llm_model": "anthropic/claude-sonnet-4-20250514",
        "status": "active",
        "tools": [
            { "slug": "search_web", "type": "internal", "permission": "read" },
            { "slug": "scheduler_agent", "type": "agent", "permission": "readwrite" }
        ],
        "assignments": [
            {
                "context_type": "team",
                "context_id": "uuid",
                "status": "active",
                "tool_restrictions": { }
            }
        ],
        "created_at": "...",
        "updated_at": "..."
    }
}

Update agent

PATCH /v1/agents/{agent_id}
Body: { fields to update }

Response 200: { updated agent }

Pause/resume agent

POST /v1/agents/{agent_id}/pause
POST /v1/agents/{agent_id}/resume

Response 200: { updated agent }

4.2 Agent Assignments

Assign agent to context

POST /v1/agents/{agent_id}/assignments
Body:
{
    "context_type": "team",
    "context_id": "team-uuid",
    "access_level": "interact",
    "budget_monthly": 50.00,
    "tool_restrictions": {
        "send_email": { "blocked": true },
        "scheduler_agent": {
            "allowed_actions": ["find_free_time"],
            "denied_actions": ["create_event"]
        }
    }
}

Response 201: { assignment }

Remove assignment

DELETE /v1/agents/{agent_id}/assignments/{assignment_id}

Response 204

4.3 Job Dispatch and Observation

Create job (direct dispatch)

POST /v1/jobs
Body:
{
    "agent_id": "uuid",
    "type": "research",
    "dispatch_payload": {
        "brief": "Research ACQSC quality standards for aged care",
        "output_format": "summary",
        "max_articles": 5
    },
    "timeout_minutes": 30,
    "observer_webhook": "https://your-system.com/webhooks/thinklio (optional)"
}

Response 201:
{
    "data": {
        "id": "uuid",
        "type": "research",
        "state": "pending",
        "subjobs": [],
        "created_at": "..."
    }
}

Get job status

GET /v1/jobs/{job_id}

Response 200:
{
    "data": {
        "id": "uuid",
        "type": "research",
        "state": "in_progress",
        "has_useful_output": true,
        "subjobs": [
            {
                "id": "uuid",
                "label": "Article 1: Quality Standards Overview",
                "order": 1,
                "state": "completed",
                "completed_at": "..."
            },
            {
                "id": "uuid",
                "label": "Article 2: Compliance Framework",
                "order": 2,
                "state": "running"
            }
        ],
        "created_at": "...",
        "updated_at": "..."
    }
}

Get job results

GET /v1/jobs/{job_id}/results

Response 200:
{
    "data": {
        "job_id": "uuid",
        "state": "resolved",
        "subjobs": [
            {
                "label": "Article 1",
                "state": "completed",
                "result_data": { ... }
            }
        ],
        "context_bundle": { ... }
    }
}

Cancel job

POST /v1/jobs/{job_id}/cancel

Response 200:
{
    "data": {
        "id": "uuid",
        "state": "cancelled"
    }
}

Register observer on job

POST /v1/jobs/{job_id}/observers
Body:
{
    "observer_type": "system",
    "notify_on": "partial_and_completion",
    "webhook_url": "https://your-system.com/webhooks/job-updates",
    "callback_metadata": {
        "external_reference": "your-tracking-id"
    }
}

Response 201: { observer registration }

4.4 Knowledge Management

List knowledge facts

GET /v1/agents/{agent_id}/knowledge
Query: ?scope=team&scope_id=uuid&category=project&limit=50

Response 200:
{
    "data": [
        {
            "id": "uuid",
            "scope": "team",
            "subject": "Project Alpha",
            "predicate": "deadline",
            "value": "2026-06-30",
            "category": "project",
            "confidence": 0.95,
            "created_at": "..."
        }
    ]
}

Add knowledge fact

POST /v1/agents/{agent_id}/knowledge
Body:
{
    "scope": "account",
    "scope_id": "account-uuid",
    "subject": "Expense Policy",
    "predicate": "approval_threshold",
    "value": "Expenses over $500 require manager approval",
    "category": "procedure"
}

Response 201: { fact }

Bulk import knowledge

POST /v1/agents/{agent_id}/knowledge/import
Body:
{
    "scope": "account",
    "scope_id": "account-uuid",
    "facts": [
        { "subject": "...", "predicate": "...", "value": "...", "category": "..." },
        ...
    ]
}

Response 202:
{
    "data": {
        "import_id": "uuid",
        "facts_queued": 150,
        "status": "processing"
    }
}

Delete knowledge fact

DELETE /v1/agents/{agent_id}/knowledge/{fact_id}

Response 204

4.5 Account Management

Get account

GET /v1/accounts/{account_id}

Response 200: { account details including plan, budget, member count }

Update account policies

PATCH /v1/accounts/{account_id}/policies
Body:
{
    "tool_restrictions": {
        "blocked_tools": ["high_risk_tool"],
        "require_approval": ["send_email"]
    },
    "content_rules": {
        "prohibited_topics": []
    },
    "cost_limits": {
        "max_per_interaction": 1.00
    },
    "delegation_limits": {
        "max_delegation_depth": 3,
        "require_approval_for_delegation": false
    }
}

Response 200: { updated policies }

List teams

GET /v1/accounts/{account_id}/teams

Response 200: { teams list }

4.6 Usage and Reporting

Get usage summary

GET /v1/usage
Query: ?account_id=uuid&period=monthly&start=2026-03-01&end=2026-03-31

Response 200:
{
    "data": {
        "period": { "start": "...", "end": "..." },
        "total_cost": 145.23,
        "total_interactions": 3420,
        "total_jobs": 85,
        "by_team": [
            {
                "team_id": "uuid",
                "team_name": "Engineering",
                "cost": 89.50,
                "interactions": 2100,
                "jobs": 42
            }
        ],
        "by_agent": [
            {
                "agent_id": "uuid",
                "agent_name": "Research Assistant",
                "cost": 52.10,
                "interactions": 890,
                "jobs": 28
            }
        ]
    }
}

4.7 Webhooks

Register webhook subscription

POST /v1/webhooks
Body:
{
    "url": "https://your-app.com/webhook",
    "events": [
        "interaction.completed",
        "job.resolved",
        "job.failed",
        "budget.warning"
    ],
    "secret": "webhook-signing-secret"
}

Response 201: { webhook registration }

Webhook payloads are signed with the shared secret using HMAC-SHA256 in the X-Thinklio-Signature header.


5. Integration API -- Bidirectional Capability Exchange

The Integration API allows external systems to register as tools available to Thinklio agents, and to subscribe to platform events relevant to their integration.

5.1 Tool Registration

Register external tool

POST /v1/integrations/tools
Body:
{
    "slug": "crm_customer_lookup",
    "name": "CRM Customer Lookup",
    "description": "Look up customer records in the CRM system",
    "trust_level": "read",
    "parameter_schema": {
        "type": "object",
        "properties": {
            "customer_id": { "type": "string" },
            "query": { "type": "string" }
        }
    },
    "return_schema": {
        "type": "object",
        "properties": {
            "customer": { "type": "object" },
            "status": { "type": "string" }
        }
    },
    "endpoint": {
        "url": "https://your-crm.com/api/thinklio/execute",
        "auth_type": "bearer",
        "auth_token": "encrypted-token"
    },
    "rate_limit": {
        "max_per_minute": 30
    },
    "default_execution_mode": "immediate"
}

Response 201:
{
    "data": {
        "id": "uuid",
        "slug": "crm_customer_lookup",
        "status": "pending_approval",
        "registered_by": "account-uuid"
    }
}

Dynamically registered tools require account approval before becoming available to agents. Once approved, they appear in the tool registry and can be assigned to agents.

Update tool registration

PATCH /v1/integrations/tools/{tool_id}
Body: { fields to update }

Response 200: { updated tool }

Deregister tool

DELETE /v1/integrations/tools/{tool_id}

Response 204

5.2 Tool Execution Contract

When Thinklio invokes a registered external tool, it calls the tool's endpoint:

POST {tool_endpoint_url}
Headers:
    Authorization: Bearer {configured_auth_token}
    X-Thinklio-Request-ID: uuid
    X-Thinklio-Agent-ID: uuid
    Content-Type: application/json

Body:
{
    "parameters": {
        "customer_id": "C-12345",
        "query": "recent support tickets"
    },
    "context": {
        "agent_id": "uuid",
        "account_id": "uuid"
    }
}

Expected Response 200:
{
    "result": {
        "customer": { ... },
        "status": "found"
    }
}

5.3 Event Subscription

Subscribe to events

POST /v1/integrations/subscriptions
Body:
{
    "events": [
        "knowledge.extracted",
        "job.resolved"
    ],
    "filter": {
        "account_id": "uuid",
        "agent_ids": ["uuid", "uuid"]
    },
    "webhook_url": "https://your-system.com/webhooks/thinklio-events",
    "secret": "signing-secret"
}

Response 201: { subscription }

List subscriptions

GET /v1/integrations/subscriptions

Response 200: { list of active subscriptions }

Remove subscription

DELETE /v1/integrations/subscriptions/{subscription_id}

Response 204

6. Internal Service APIs

6.1 Context Service

Build context

POST /internal/context/build
Body:
{
    "agent_id": "uuid",
    "user_id": "uuid",
    "team_id": "uuid",
    "account_id": "uuid",
    "session_id": "uuid",
    "max_tokens": 8000,
    "job_data": {
        "job_id": "uuid",
        "context_bundle": { ... },
        "subjob_results": [ ... ]
    }
}

Response 200:
{
    "system_prompt": "...",
    "agent_knowledge": [ facts ],
    "account_knowledge": [ facts ],
    "team_knowledge": [ facts ],
    "user_knowledge": [ facts ],
    "chat_history": [ messages ],
    "available_tools": [ tool definitions ],
    "job_context": { ... },
    "token_usage": {
        "system_prompt": 500,
        "knowledge": 2000,
        "history": 3000,
        "tools": 1500,
        "job_context": 500,
        "total": 7500,
        "budget": 8000
    }
}

The job_data field is populated for follow-up interactions triggered by job state changes. It includes the job's context_bundle and completed subjob results, which are injected into the agent's context for reasoning.

6.2 Tool Service

Execute tool

POST /internal/tools/execute
Body:
{
    "tool_slug": "search_web",
    "tool_type": "internal | external | agent",
    "agent_id": "uuid",
    "user_id": "uuid",
    "user_trust": "read",
    "account_policies": { },
    "assignment_tool_restrictions": { },
    "parameters": {
        "query": "battery technology 2026"
    },
    "delegation_chain": ["agent-uuid-1", "agent-uuid-2"],
    "delegation_depth": 1,
    "session_cost": 0.05,
    "budget_remaining": 0.95
}

Response 200:
{
    "decision": "allow",
    "result": {
        "output": { tool-specific result },
        "cost": 0.001,
        "duration_ms": 350
    }
}

Response 403 (trust level insufficient):
{
    "decision": "deny",
    "reason": "User trust level 'read' insufficient for tool requiring 'low_risk_write'"
}

Response 403 (delegation depth):
{
    "decision": "deny",
    "reason": "delegation_depth_exceeded",
    "detail": "Delegation depth 4 exceeds maximum of 3"
}

Response 403 (cycle detected):
{
    "decision": "deny",
    "reason": "delegation_cycle_detected",
    "detail": "Agent uuid already in delegation chain"
}

Response 202 (approval required):
{
    "decision": "require_approval",
    "approval_id": "uuid",
    "reason": "High-risk tool requires explicit approval"
}

For agent-type tools, the tool service resolves the delegate agent ID from the tool config, checks delegation depth and cycles, creates a child interaction, and executes the delegate agent's harness.

6.3 Usage Service

Pre-execution budget check

POST /internal/usage/budget-check
Body:
{
    "account_id": "uuid",
    "team_id": "uuid",
    "user_id": "uuid",
    "estimated_cost": 0.01
}

Response 200:
{
    "allowed": true,
    "remaining": {
        "account": 450.00,
        "team": 45.00,
        "user": 8.50
    }
}

Response 200 (over budget):
{
    "allowed": false,
    "reason": "team_budget_exceeded",
    "remaining": {
        "account": 450.00,
        "team": 0.00,
        "user": 8.50
    }
}

Record cost

POST /internal/usage/record
Body:
{
    "interaction_id": "uuid",
    "step_id": "uuid",
    "agent_id": "uuid",
    "user_id": "uuid",
    "team_id": "uuid",
    "account_id": "uuid",
    "cost": 0.005,
    "cost_detail": {
        "tokens_in": 1500,
        "tokens_out": 350,
        "model": "anthropic/claude-sonnet-4-20250514"
    },
    "parent_interaction_id": "uuid (if delegation)",
    "job_id": "uuid (if job-related)"
}

Response 200: { "recorded": true }

6.4 Gateway (Internal Response Delivery)

The gateway subscribes to message.response events on the event bus. No direct API call is needed -- the agent service publishes a response event and the gateway picks it up.

Response event payload:

{
    "channel": "telegram",
    "channel_data": {
        "chat_id": 123456,
        "reply_to_message_id": 789
    },
    "text": "Here is the response...",
    "attachments": []
}

6.5 Webhook Delivery

For Platform API webhooks and Integration API event subscriptions, a delivery path translates internal events to external HTTP webhook calls. The path runs as a Convex scheduled function that reads the event table against every active subscription and dispatches matching events through a Convex action:

event written to Convex event table
    --> webhook delivery scheduler (Convex cron, every 30s)
        --> reads events since last cursor, per subscription
        --> filters by event kind and scope
        --> signs payload with subscription's shared secret
        --> enqueues a Convex action per subscription to POST the payload
        --> records delivery_attempt rows for retry and audit

Delivery actions retry with exponential backoff on failure; after abandonment, the subscription is marked unhealthy and the account admin is notified. The internal event table is never exposed to external subscribers. This path is a Convex-only pipeline; a dedicated Go webhook delivery service is a Tier 3 promotion candidate per 02 System Architecture section 12 if outbound rate or egress fan-out outgrows what Convex actions can sustain. See 06 Events, Channels & Messaging section 10 for the delivery surface shared with inbound channels.

6.6 Contract Change Policy

External API changes are versioned (/v1/, /v2/). Each API surface (Channel, Platform, Integration) is versioned independently. Backward-incompatible changes require a new API version. Deprecated versions are supported for a minimum of 6 months. Internal API changes require coordinated deployment. All contract changes are recorded in the decision log.

7. Complete Endpoint Reference

The following tables list all implemented endpoints across the three API surfaces, including those added during the March 2026 implementation session. Verbose request and response schemas for the most important endpoints appear in the surface sections above. Detailed schemas for account and onboarding, platform services, LLM configuration, core data structures, and agent extensibility are in their respective documents (see cross-references in the frontmatter).

Authentication

All endpoints require a Clerk JWT (Authorization: Bearer <clerk-jwt>) or an API key. The JWT sub claim is the user identifier. Clerk handles signup, login, organisation membership, and token management. See 07 Security & Governance for the full authentication and authorisation model.

Accounts and Onboarding

Method Endpoint Description
GET /v1/accounts List the authenticated user's accounts
POST /v1/accounts Create an account (the caller becomes owner)
GET /v1/accounts/{id} Get account details
PATCH /v1/accounts/{id} Update account settings
GET /v1/accounts/{id}/members List members
PATCH /v1/accounts/{id}/members/{uid} Update member role
DELETE /v1/accounts/{id}/members/{uid} Remove member
GET /v1/accounts/{id}/invitations List pending invitations
POST /v1/accounts/{id}/invitations Invite user by email
DELETE /v1/accounts/{id}/invitations/{id} Revoke invitation
POST /v1/invitations/{token}/accept Accept invitation
GET /v1/accounts/{id}/teams List teams
POST /v1/accounts/{id}/teams Create team
PATCH /v1/accounts/{id}/teams/{tid} Update team
DELETE /v1/accounts/{id}/teams/{tid} Delete team
GET /v1/accounts/{id}/teams/{tid}/members List team members
POST /v1/accounts/{id}/teams/{tid}/members Add team member
DELETE /v1/accounts/{id}/teams/{tid}/members/{uid} Remove team member

Channels

Method Endpoint Description
POST /v1/channels/connect Connect a channel (triggers OTP verification)
GET /v1/channels/mine List the authenticated user's connected channels

Core Data Structures

Method Endpoint Description
GET/POST /v1/tasks List or create tasks (rich filter and sort)
GET /v1/tasks/stats Task counts by status, including overdue
GET/PATCH/DELETE /v1/tasks/{id} Task CRUD
GET/POST /v1/task-lists List or create task lists
PATCH/DELETE /v1/task-lists/{id} Task list CRUD
GET/POST /v1/task-types List or create task types
PATCH/DELETE /v1/task-types/{id} Task type CRUD
GET/POST /v1/contacts List or create contacts (ILIKE search)
GET/PATCH/DELETE /v1/contacts/{id} Contact CRUD (includes interactions)
POST /v1/contacts/{id}/interactions Log an interaction
GET /v1/contacts/{id}/interactions List interactions
GET/POST /v1/items List or create items
GET/PATCH /v1/items/{id} Item CRUD
POST /v1/items/{id}/assign Assign item
GET/POST /v1/item-types List or create item types
PATCH/DELETE /v1/item-types/{id} Item type CRUD
GET/POST /v1/notes List or create notes
GET/PATCH/DELETE /v1/notes/{id} Note CRUD
GET/POST/DELETE /v1/notes/{id}/share Note sharing
GET /v1/notes/{id}/mentions List inline mentions
GET/POST /v1/note-types List or create note types
PATCH/DELETE /v1/note-types/{id} Note type CRUD
GET/POST /v1/tags List or create tags
DELETE /v1/tags/{id} Delete tag
POST /v1/tags/apply Apply tag to entity
POST /v1/tags/remove Remove tag from entity
GET /v1/tags/for Get tags for a given entity
GET /v1/mentions/search Cross-entity mention autocomplete

Briefing and Notifications

Method Endpoint Description
GET /v1/briefing Aggregated daily dashboard
GET /v1/notifications List notifications
POST /v1/notifications Mark all notifications read
POST /v1/notifications/{id}/read Mark a single notification read

Quality Ratings

Method Endpoint Description
POST /v1/ratings Submit, update, or remove a rating

Audit

Method Endpoint Description
GET /v1/admin/audit Filterable audit log

Unified Capabilities

Method Endpoint Description
GET/POST /v1/capabilities List or register tools and agents via manifest
GET /v1/capabilities/{id} Get capability details
POST /v1/callbacks/{call_id} External agent callback
GET /v1/agents/views Get agent view definitions
POST /v1/agent-events External agent inbound event

Platform Services and LLM Configuration

Method Endpoint Description
GET /v1/accounts/services List services with key status
POST /v1/accounts/services Set account API key for a service
GET /v1/accounts/models List LLM tier preferences
POST /v1/accounts/models Set model for a tier
GET /v1/accounts/credits Balance and ledger

App Administration

Method Endpoint Description
GET/PATCH /v1/admin/platform/config Kill switch and margin configuration
GET/POST /v1/admin/platform/services Service registry CRUD
PATCH/DELETE /v1/admin/platform/services/{slug} Service management
GET/POST /v1/admin/platform/models LLM model registry CRUD
PATCH /v1/admin/platform/models/{id} Model management

8. Accounts, Onboarding and Team Management

This section specifies the API contract for account creation, onboarding, user management, invitations, and team management. All endpoints are served by the Gateway service.

Base URL: https://api.thinklio.com (production), http://localhost:8001 (development)

8.1 Role Model

Four-tier roles apply at both account level and team level:

Role Account-level permissions Team-level permissions
owner Full access: billing, delete account, transfer ownership, manage API keys, manage subscription Full team control
admin Manage teams, members, agents, settings (not billing or deletion) Manage team members and settings
editor Create and edit agents, knowledge, and all features Use team agents and resources
viewer Read-only access; can interact with assigned agents Read-only team access

8.2 Onboarding Flow

The onboarding sequence has five steps:

  1. The user signs up via Clerk (client SDK).
  2. Clerk webhook triggers creation of the user_profile record.
  3. The client calls POST /v1/accounts to create the user's first account.
  4. The client submits onboarding settings via PATCH /v1/accounts/{id} (timezone, preferences, and so on).
  5. The user is now the owner of the account with full access.

Frontend implementation notes. After Clerk signup, check GET /v1/accounts. If the response is empty, show the onboarding wizard. The wizard collects an account name, slug, timezone, and any initial settings. Store the active account ID in local state (the user may have multiple accounts later). The Clerk client SDK handles JWT persistence automatically.

8.3 Account Endpoints

GET /v1/accounts

List all accounts the authenticated user belongs to.

Response 200 OK

[
  {
    "id": "uuid",
    "name": "Acme Corp",
    "slug": "acme-corp",
    "plan": "free",
    "budget_monthly": null,
    "status": "active",
    "created_at": "2025-01-01T00:00:00Z"
  }
]

POST /v1/accounts

Create a new account. The authenticated user becomes the owner.

Request

{
  "name": "Acme Corp",
  "slug": "acme-corp",
  "settings": {
    "timezone": "Europe/London",
    "language": "en"
  }
}

Response 201 Created

{
  "id": "uuid",
  "name": "Acme Corp",
  "slug": "acme-corp",
  "plan": "free",
  "status": "active",
  "created_at": "2025-01-01T00:00:00Z"
}

Errors: 409 slug_taken if the slug is already in use.

GET /v1/accounts/{id}

Get account details. Requires membership.

Response 200 OK with the same shape as the list item above.

PATCH /v1/accounts/{id}

Update account settings. Requires admin or owner role. Partial update: only provided fields are changed.

Request

{
  "name": "Acme Corporation",
  "settings": {
    "timezone": "Europe/Berlin",
    "language": "de"
  }
}

Updatable fields: name, slug, settings, policies.

Response 200 OK

{ "status": "updated" }

8.4 Member Endpoints

GET /v1/accounts/{id}/members

List all members of an account. Any member can view.

Response 200 OK

[
  {
    "user_id": "uuid",
    "display_name": "Jane Smith",
    "role": "owner",
    "joined_at": "2025-01-01T00:00:00Z"
  }
]

PATCH /v1/accounts/{id}/members/{user_id}

Update a member's role. Requires admin or owner. Only owner can set admin or owner roles.

Request

{ "role": "admin" }

Response 200 OK

{ "status": "updated" }

DELETE /v1/accounts/{id}/members/{user_id}

Remove a member from the account. Requires admin or owner. Cannot remove yourself.

Response 204 No Content

8.5 Invitation Endpoints

GET /v1/accounts/{id}/invitations

List pending invitations. Requires admin or owner.

Response 200 OK

[
  {
    "id": "uuid",
    "account_id": "uuid",
    "team_id": null,
    "invited_email": "alice@example.com",
    "invited_by": "uuid",
    "role": "editor",
    "status": "pending",
    "expires_at": "2025-01-08T00:00:00Z",
    "created_at": "2025-01-01T00:00:00Z"
  }
]

POST /v1/accounts/{id}/invitations

Invite a user by email. Requires admin or owner. Only owner can invite admin or owner roles. Sends an invitation email via Postmark.

Request

{
  "email": "alice@example.com",
  "role": "editor",
  "team_id": "uuid (optional)"
}

Response 201 Created

{
  "id": "uuid",
  "status": "pending",
  "email": "alice@example.com"
}

DELETE /v1/accounts/{id}/invitations/{invitation_id}

Revoke a pending invitation. Requires admin or owner.

Response 204 No Content

POST /v1/invitations/{token}/accept

Accept an invitation. Requires a Clerk JWT (the invited user must have signed up). Adds the user to the account and, if specified in the invitation, to the team.

Response 200 OK

{ "status": "accepted" }

Errors: 404 if the invitation is not found or already used. 410 expired if the invitation has expired.

8.6 Team Endpoints

GET /v1/accounts/{id}/teams

List all teams in an account. Any member can view.

Response 200 OK

[
  {
    "id": "uuid",
    "account_id": "uuid",
    "name": "Engineering",
    "slug": "engineering",
    "budget_monthly": null,
    "status": "active",
    "created_at": "2025-01-01T00:00:00Z"
  }
]

POST /v1/accounts/{id}/teams

Create a team. Requires admin or owner.

Request

{
  "name": "Engineering",
  "slug": "engineering"
}

Response 201 Created with the team object.

GET /v1/accounts/{id}/teams/{team_id}

Get team details. Any account member can view.

Response 200 OK with the same shape as the team list item.

PATCH /v1/accounts/{id}/teams/{team_id}

Update team name or slug. Requires admin or owner.

Request

{ "name": "Product Engineering" }

Response 200 OK

{ "status": "updated" }

DELETE /v1/accounts/{id}/teams/{team_id}

Delete a team. Requires admin or owner. Cascades to team memberships.

Response 204 No Content

8.7 Team Member Endpoints

GET /v1/accounts/{id}/teams/{team_id}/members

List team members. Any account member can view.

Response 200 OK

[
  {
    "user_id": "uuid",
    "display_name": "Jane Smith",
    "role": "editor",
    "joined_at": "2025-01-01T00:00:00Z"
  }
]

POST /v1/accounts/{id}/teams/{team_id}/members

Add a member to a team. Requires admin or owner. The user must already be an account member.

Request

{
  "user_id": "uuid",
  "role": "editor"
}

Response 201 Created

{ "status": "added" }

DELETE /v1/accounts/{id}/teams/{team_id}/members/{user_id}

Remove a user from a team. Requires admin or owner.

Response 204 No Content

8.8 Channel Connection

POST /v1/channels/connect

Connect a messaging channel (for example, Telegram). Creates a user_channel record and triggers OTP verification via email.

Request

{
  "channel_type": "telegram",
  "external_id": "123456789",
  "display_name": "My Telegram"
}

Response 201 Created

{
  "id": "uuid",
  "status": "pending_verification",
  "message": "Verification code sent to j***n@example.com"
}

GET /v1/channels/mine

List all connected channels for the authenticated user.

Response 200 OK

[
  {
    "id": "uuid",
    "channel_type": "telegram",
    "external_id": "123456789",
    "status": "active",
    "display_name": "My Telegram",
    "verified_at": "2025-01-01T00:00:00Z",
    "last_active_at": "2025-01-02T12:00:00Z"
  }
]

8.9 Error Format

All errors follow a consistent format:

{
  "error": {
    "code": "error_code",
    "message": "Human-readable description"
  }
}

Common HTTP status codes: 400 (bad request or validation error), 401 (missing or invalid auth token), 403 (insufficient role or permissions), 404 (resource not found or no membership), 409 (conflict such as duplicate slug), 410 (resource expired such as invitation), 500 (internal server error).

9. Tool Integration Developer Guide

This section is intended for external developers building tool integrations that connect third-party APIs to Thinklio agents via the Model Context Protocol (MCP). It is designed to be self-contained: you should not need any other Thinklio documentation to build and deploy a working integration.

9.1 What You Are Building

An MCP server: a small HTTP service that exposes API operations as tools. Your MCP server connects to an external API on behalf of the user, exposes each API operation as a named tool with a clear description and input schema, handles authentication to the external API along with rate limiting, pagination, and error formatting internally, and returns clean, structured results that an LLM agent can reason about.

Once built, a Thinklio account admin registers your MCP server URL, provides any required credentials, and assigns your tools to their agents. No code changes to Thinklio are needed.

9.2 Architecture

Agent (LLM)  ->  Thinklio Platform  ->  Your MCP Server  ->  External API
                      |                      |
                      +- governance check    +- auth (API key from config)
                      +- audit log           +- rate limiting
                      +- usage tracking      +- response formatting

Thinklio handles: permission checks, audit logging, usage tracking, credential injection, tool discovery, and agent-to-tool assignment.

Your server handles: external API authentication, request construction, error handling, response shaping, and rate limit compliance.

9.3 Transport: Streamable HTTP

Thinklio connects to MCP servers using the Streamable HTTP transport. This is the current MCP standard for remote server communication, replacing the older SSE-based transport which is deprecated.

Your server exposes a single HTTP endpoint (for example, https://your-server.com/mcp) that accepts both POST and GET requests. POST carries client-to-server messages (tool calls, the initial initialize handshake). GET opens an optional Server-Sent Events stream for server-initiated notifications (not required for most tool integrations). A typical tool call is a single POST request and response. There is no persistent connection to maintain between calls.

Session management. Thinklio sends an initialize request when it first connects to your server. Your server may optionally return an Mcp-Session-Id header on the response. If provided, Thinklio will include this header on all subsequent requests, allowing your server to maintain session state (cached API tokens, connection pools, and so on). For stateless integrations, you can ignore sessions entirely.

What this means for you. Your server is a standard HTTP service. Deploy it anywhere that can serve HTTPS traffic. You do not need to manage WebSocket connections, long-lived SSE streams, or stdio pipes. The MCP TypeScript SDK provides StreamableHTTPServerTransport which handles the protocol details. You write tool handlers; the SDK handles the rest.

Minimum requirements: an HTTPS endpoint (TLS required for production; Thinklio will not connect over plain HTTP); responds to POST requests with JSON (Content-Type: application/json); handles the MCP initialize handshake (the SDK does this automatically); returns tool results in the MCP response format (see section 9.11).

9.4 Building an MCP Tool Server

Step 1: Choose Your SDK

MCP servers can be built in any language. The official SDKs are:

  • TypeScript: @modelcontextprotocol/sdk
  • Python: mcp

TypeScript examples are used throughout this guide.

Step 2: Define Your Tools

Each API operation should be a separate tool. Choose names that are specific and action-oriented.

Naming convention: {service}_{verb}_{noun}

Good examples: cliniko_list_patients, cliniko_get_appointment, cliniko_create_appointment, cliniko_search_patients.

Bad examples: cliniko (too vague; the agent does not know what it does), get_data (not specific enough), cliniko_api_call (router pattern; LLMs handle discrete tools better).

Step 3: Write Tool Descriptions for the LLM

The description field is what the agent reads to decide when to call your tool. Write it for an LLM, not a human developer.

Good description:

Search for patients by name, email, or phone number. Returns matching
patients with their ID, name, contact details, and next appointment.
Use this when the user asks about a specific patient or wants to find
someone in the system.

Bad description:

GET /patients endpoint with query parameters

Step 4: Define Input Schemas

Use JSON Schema (or Zod in TypeScript) to define what the tool accepts. Include description on every parameter because the LLM uses these to decide what values to pass.

{
  name: "cliniko_search_patients",
  description: "Search for patients by name, email, or phone number...",
  inputSchema: {
    type: "object",
    properties: {
      query: {
        type: "string",
        description: "Search term — matches against patient name, email, or phone number"
      },
      limit: {
        type: "number",
        description: "Maximum number of results to return (default: 10, max: 50)"
      }
    },
    required: ["query"]
  }
}

Step 5: Implement the Server

Here is a minimal MCP server for a Cliniko integration using Streamable HTTP transport:

import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
import express from "express";
import { z } from "zod";
import { randomUUID } from "crypto";

// --- Cliniko API Client ---

class ClinikoClient {
  private baseUrl: string;
  private authHeader: string;

  constructor(apiKey: string) {
    // Cliniko API keys have the shard appended: "...key-au2"
    const parts = apiKey.split("-");
    const shard = parts[parts.length - 1].match(/^[a-z]{2}\d{1,2}$/i)
      ? parts.pop()
      : "au1";
    this.baseUrl = `https://api.${shard}.cliniko.com/v1`;
    this.authHeader =
      "Basic " + Buffer.from(apiKey + ":").toString("base64");
  }

  async request(path: string, options: RequestInit = {}) {
    const response = await fetch(`${this.baseUrl}${path}`, {
      ...options,
      headers: {
        Authorization: this.authHeader,
        Accept: "application/json",
        "Content-Type": "application/json",
        "User-Agent": "Thinklio MCP Integration (support@thinklio.ai)",
        ...options.headers,
      },
    });

    if (!response.ok) {
      const body = await response.text();
      throw new Error(
        `Cliniko API error ${response.status}: ${body.slice(0, 200)}`
      );
    }

    return response.json();
  }
}

// --- MCP Server Setup ---

const server = new McpServer({
  name: "cliniko",
  version: "1.0.0",
});

// --- Tool: Search Patients ---

server.tool(
  "cliniko_search_patients",
  "Search for patients by name, email, or phone number. Returns matching " +
    "patients with their ID, name, and contact details.",
  {
    query: z.string().describe("Search term — name, email, or phone"),
    limit: z
      .number()
      .optional()
      .describe("Max results to return (default: 10)"),
  },
  async ({ query, limit }) => {
    const encoded = encodeURIComponent(query);
    const data = await cliniko.request(
      `/patients?q[]=${encoded}&per_page=${limit ?? 10}`
    );
    const patients = data.patients.map((p: any) => ({
      id: p.id,
      name: `${p.first_name} ${p.last_name}`,
      email: p.email,
      phone: p.patient_phone_numbers?.[0]?.number ?? null,
    }));
    return {
      content: [{ type: "text" as const, text: JSON.stringify(patients) }],
    };
  }
);

// --- Tool: List Today's Appointments ---

server.tool(
  "cliniko_list_todays_appointments",
  "List all appointments for today. Returns each appointment's time, " +
    "patient name, practitioner, and status. Useful for daily briefings.",
  {},
  async () => {
    const today = new Date().toISOString().split("T")[0];
    const data = await cliniko.request(
      `/individual_appointments?q[]=starts_at:>=${today}T00:00:00Z` +
        `&q[]=starts_at:<=${today}T23:59:59Z&per_page=50`
    );
    const appointments = data.individual_appointments.map((a: any) => ({
      id: a.id,
      patient: a.patient_name,
      startsAt: a.starts_at,
      endsAt: a.ends_at,
      arrived: a.patient_arrived,
      cancelled: !!a.cancelled_at,
    }));
    return {
      content: [
        { type: "text" as const, text: JSON.stringify(appointments) },
      ],
    };
  }
);

// --- Tool: Create Appointment ---

server.tool(
  "cliniko_create_appointment",
  "Create a new appointment for a patient. Requires the patient ID, " +
    "practitioner ID, appointment type ID, and start time. Thinklio's " +
    "governance system will prompt the user for confirmation before this " +
    "tool is called.",
  {
    patientId: z.number().describe("Cliniko patient ID"),
    practitionerId: z.number().describe("Cliniko practitioner ID"),
    appointmentTypeId: z.number().describe("Cliniko appointment type ID"),
    startsAt: z
      .string()
      .describe("Start time in ISO 8601 format (e.g. 2026-04-10T09:00:00Z)"),
    notes: z
      .string()
      .optional()
      .describe("Optional notes for the appointment"),
  },
  async ({ patientId, practitionerId, appointmentTypeId, startsAt, notes }) => {
    const result = await cliniko.request("/individual_appointments", {
      method: "POST",
      body: JSON.stringify({
        patient_id: patientId,
        practitioner_id: practitionerId,
        appointment_type_id: appointmentTypeId,
        starts_at: startsAt,
        notes: notes ?? null,
      }),
    });
    return {
      content: [
        {
          type: "text" as const,
          text: JSON.stringify({
            id: result.id,
            patient: result.patient_name,
            startsAt: result.starts_at,
            endsAt: result.ends_at,
            status: "created",
          }),
        },
      ],
    };
  }
);

// Add more tools as needed (see Appendix for the full Cliniko tool list)

// --- HTTP Server with Streamable HTTP Transport ---

const app = express();

// Credentials are injected via HTTP headers by Thinklio (see section 9.6)
let cliniko: ClinikoClient;

// Session management: map session IDs to transport instances
const sessions = new Map<string, StreamableHTTPServerTransport>();

app.post("/mcp", async (req, res) => {
  // Extract credentials from Thinklio's request headers
  const apiKey = req.headers["x-cliniko-api-key"] as string;
  if (!apiKey) {
    res.status(401).json({ error: "Missing API key" });
    return;
  }
  cliniko = new ClinikoClient(apiKey);

  // Check for existing session
  const sessionId = req.headers["mcp-session-id"] as string | undefined;

  if (sessionId && sessions.has(sessionId)) {
    // Existing session — route to its transport
    const transport = sessions.get(sessionId)!;
    await transport.handleRequest(req, res);
    return;
  }

  // New session — create transport and connect
  const newSessionId = randomUUID();
  const transport = new StreamableHTTPServerTransport({
    sessionId: newSessionId,
  });
  sessions.set(newSessionId, transport);
  await server.connect(transport);
  await transport.handleRequest(req, res);
});

// Optional: GET endpoint for SSE stream (server-initiated notifications)
app.get("/mcp", async (req, res) => {
  const sessionId = req.headers["mcp-session-id"] as string | undefined;
  if (!sessionId || !sessions.has(sessionId)) {
    res.status(400).json({ error: "Invalid session" });
    return;
  }
  const transport = sessions.get(sessionId)!;
  await transport.handleRequest(req, res);
});

app.listen(3001, () => {
  console.log("Cliniko MCP server running on port 3001");
});

Note: The Cliniko API examples above are illustrative. The actual Cliniko filter query syntax uses field-specific matchers (for example, ?q[]=first_name:~=John). Consult the Cliniko API documentation for exact query syntax when building a production integration.

9.5 Credential Handling

Thinklio injects credentials into your MCP server via HTTP headers on each request. This works consistently regardless of where or how your server is hosted.

Credential flow:

  1. Account admin adds your MCP server in Thinklio and provides the required credentials (for example, a Cliniko API key).
  2. Credentials are stored encrypted in Thinklio's secrets vault.
  3. When an agent calls one of your tools, Thinklio sends the tool call to your server with credentials included as HTTP headers.

Header convention: X-{Service}-Api-Key (for example, X-Cliniko-Api-Key).

Your server declares which credentials it needs in its registration metadata (see section 9.8). Thinklio uses this declaration to prompt the admin for the right values and to include the right headers on each request.

9.6 Response Shaping

The agent reads your tool's output and reasons about it. Shape responses for LLM consumption.

Do: return only relevant fields (not the entire 50-field API response); use clear field names (startsAt not starts_at); include counts and pagination info ({ patients: [...], total: 142 }); return meaningful errors ("No patient found with ID 12345").

Do not: return raw HTML or XML; include internal IDs the agent cannot use; return binary data; include deeply nested objects when a flat summary will do.

9.7 Error Handling

Return errors as structured text, not exceptions. The agent needs to understand what went wrong so it can tell the user.

try {
  const data = await cliniko.request(`/patients/${id}`);
  return { content: [{ type: "text", text: JSON.stringify(data) }] };
} catch (error) {
  return {
    content: [
      {
        type: "text",
        text: JSON.stringify({
          error: true,
          message: error instanceof Error ? error.message : "Unknown error",
          suggestion: "Check the patient ID and try again",
        }),
      },
    ],
    isError: true,
  };
}

9.8 Rate Limits and Retries

Most APIs have rate limits. Handle them in your server; do not let 429 errors bubble up to the agent. Track request counts and sleep or queue when approaching limits. Cliniko, for example, allows 200 requests per minute. Implement a simple token bucket. Return a clear message if rate-limited: "Rate limit reached, please wait a moment".

Thinklio may retry a failed tool call (for example, if a network timeout occurs between Thinklio and your server). Your tools should be safe to call more than once with the same inputs. For read operations (list, get, search), this is inherently safe. For write operations (create, update, delete), use one of these strategies: accept an optional requestId parameter for idempotency keys (Thinklio generates and passes a unique request ID for each tool invocation); rely on natural idempotency where the operation produces the same result regardless of repetition; or use check-before-write for operations where duplicates would be harmful.

9.9 Timeouts

Thinklio enforces a 60-second timeout on individual tool calls. If your server does not respond within 60 seconds, the call is treated as failed and may be retried.

If your tool needs to call a slow external API, consider paginating internally and returning the first page of results, setting timeouts on your outbound requests (for example, 45 seconds) so you can return a meaningful error before Thinklio's timeout fires, or returning partial results with a note that more data is available.

9.10 Registering Your Server in Thinklio

Once your MCP server is running and accessible over HTTPS:

  1. Navigate to Admin, then Integrations, then Add MCP Server.
  2. Enter the server name, MCP endpoint URL (for example, https://your-server.com/mcp), and credential requirements.
  3. Thinklio connects, sends an initialize request, and discovers your tools automatically via the MCP tools/list method.
  4. Each discovered tool appears in the tool list with its name, description, and input schema.

Tool discovery and updates. Thinklio re-discovers your tools periodically (currently every 24 hours) and whenever an admin manually triggers a refresh from the Integrations page. If you add, remove, or change tools on your server, the changes will be picked up on the next discovery cycle. There is no need to notify Thinklio when you update your tools. Just deploy the new version of your server and the changes will be detected.

Assigning tools to agents. Navigate to Admin, then Agents, then Edit Agent, then Tools. Your MCP tools appear alongside built-in tools. Toggle which operations this agent can use. The agent sees the selected tools in its next chat turn.

Credential scoping. Different integrations need credentials at different levels:

Scope Who provides the key Example
Platform Thinklio (us) OpenRouter, internal services
Account Organisation admin, shared by all agents Cliniko, Xero, practice management
User Each user provides their own Personal Gmail, personal calendar

Your MCP server declares which scope it needs in its registration metadata. The Thinklio UI adapts, showing the credential input in the right place (admin settings for account-level, user settings for user-level).

9.11 Design Guidelines

How many tools? One tool per distinct operation, not one mega-tool with an "operation" parameter. Aim for 5 to 15 tools per integration. Group related read operations where it makes sense (for example, one list_patients with optional filters rather than list_all_patients plus list_active_patients plus list_archived_patients).

What makes a good tool:

Trait Good Bad
Name cliniko_search_patients search
Description Explains when and why to use it Restates the name
Parameters Minimal, well-described Optional params with no defaults
Response Relevant fields, flat structure Raw API dump
Errors Actionable message Stack trace

Trust levels. When Thinklio registers your tools, each is assigned a trust level: read (queries data, no side effects), standard (modifies data within normal bounds), elevated (creates or deletes records or sends communications), admin (administrative operations). Mark your tools with the appropriate level. Thinklio's governance system uses this to enforce policy (for example, an account policy might require approval for all elevated-trust tool calls).

Governance integration. You do not need to implement governance checks. Thinklio handles this. Before your tool is called, the platform checks the agent's trust level against the tool's required trust level, account policies are evaluated, and if an approval gate triggers the tool call is paused and the user is prompted for approval. Only after approval does your tool receive the call. From your server's perspective, every tool call that arrives has already passed governance checks. Your tool still needs to validate its inputs and handle errors, but permission enforcement is Thinklio's responsibility.

9.12 Authentication Between Thinklio and Your Server

Thinklio authenticates itself to your MCP server so you can verify that incoming requests are genuine. Two approaches are supported.

Bearer token (recommended for most integrations). Thinklio sends a shared secret in the Authorization header: Authorization: Bearer <your-server-secret>. You generate this secret when you deploy your server and provide it to the Thinklio admin during registration. Your server validates the token on every request.

OAuth 2.1 (for public MCP servers). If you are building an MCP server intended for use by multiple Thinklio accounts (or other MCP clients beyond Thinklio), implement OAuth 2.1 with PKCE per the MCP specification. This provides scoped, expiring tokens and a proper consent flow. For private, single-account integrations, a bearer token is sufficient and much simpler.

9.13 Testing

Local testing. Use the MCP Inspector to test your server standalone:

npx @modelcontextprotocol/inspector --transport streamableHttp --url http://localhost:3001/mcp

This lets you call tools interactively and verify responses before connecting to Thinklio.

Integration testing. Register your MCP server on a Thinklio development account. Assign one or more tools to an agent. Open a channel with that agent and try natural language requests: "Show me today's appointments", "Find patient John Smith", "Book an appointment for patient 12345 tomorrow at 10am". Check the audit log to verify tool calls were recorded correctly.

9.14 Packaging and Distribution

Your MCP server needs to be reachable by Thinklio over HTTPS. Hosting options include cloud functions (AWS Lambda, Cloudflare Worker, Vercel) for serverless deployments that scale automatically, Docker containers for integrations that need persistent state such as connection pools or caches, and dedicated servers for high-volume or low-latency needs. The Streamable HTTP transport works with all of these. Your server just needs to handle POST requests at its MCP endpoint.

What to provide the account admin: MCP endpoint URL, server authentication secret (bearer token) or OAuth configuration, list of required external API credentials and where to get them, brief description of what tools are included, and any setup instructions specific to the external service.

9.15 MCP Tool Response Format

Every tool handler must return an object with a content array:

{
  content: [
    {
      type: "text",    // always "text" for structured data
      text: string     // JSON-serialised result
    }
  ],
  isError?: boolean    // set true for error responses
}

Multiple content items are supported but a single JSON text block is the common pattern.

9.16 Appendix: Cliniko Integration Complete Tool List

A full Cliniko integration would expose approximately these tools:

Patients (read: 3, elevated: 1). cliniko_list_patients lists all patients with pagination. cliniko_search_patients searches by name, email, or phone. cliniko_get_patient gets the full patient record by ID. cliniko_create_patient creates a new patient record.

Appointments (read: 3, elevated: 2). cliniko_list_todays_appointments retrieves today's schedule. cliniko_list_appointments retrieves appointments with date range filter. cliniko_get_appointment retrieves single appointment details. cliniko_create_appointment books a new appointment. cliniko_cancel_appointment cancels an existing appointment.

Practitioners (read: 2). cliniko_list_practitioners lists all practitioners. cliniko_get_practitioner retrieves practitioner details and schedule.

Availability (read: 2). cliniko_get_available_times retrieves open slots for a practitioner. cliniko_list_appointment_types retrieves available appointment types.

Treatment Notes (read: 1, standard: 1). cliniko_get_treatment_notes retrieves notes for a patient. cliniko_create_treatment_note adds a treatment note.

Invoices (read: 2). cliniko_list_invoices retrieves invoices for a patient. cliniko_get_invoice retrieves invoice details.

Total: approximately 16 tools covering the core Cliniko workflow.

10. MCP Server Reference

This section is the reference for the three MCP servers maintained in the thinklio-services repository. Each server wraps a third-party API and exposes it as a set of tools that Thinklio agents can call via the Model Context Protocol.

Server Upstream API Tools Credential scope Language
Cliniko Cliniko Practice Management (v1) 6 User (per-clinician API key) Go
Notion Notion API (2025-09-03) 15 Account (workspace integration token) Go
Xero Xero Accounting API (2.0) 18 Account (OAuth 2.0, managed by Thinklio) Go

All servers share the same architecture: a single Go binary exposing /mcp (Streamable HTTP), /health, and /meta endpoints. They are stateless: credentials arrive as HTTP headers on every request and are never stored.

10.1 Common Architecture

Endpoints

Path Method Purpose
/mcp POST / GET MCP protocol endpoint (Streamable HTTP). Handles initialize, tools/list, and tools/call.
/health GET Health check. Returns {"status": "ok", "tools": <count>}.
/meta GET Integration metadata. Returns server identity, credential config, and tool list with trust levels.

Configuration

Each server reads a single environment variable:

Variable Default Description
PORT 8080 HTTP listen port

All other configuration (API credentials, tenant IDs) arrives per-request via HTTP headers injected by the Thinklio platform.

Deployment

Each server ships as a multi-stage Docker image built on golang:1.26-alpine (builder) and alpine:3.21 (runtime). The runtime image contains only the static binary and CA certificates, and runs as nobody. All images expose port 8080.

Build context is the repository root:

docker build -f services/cliniko/Dockerfile .

Dependencies

All servers depend on the mark3labs/mcp-go library (v0.47.0) for MCP protocol handling. There are no other external Go dependencies.

Response Shaping

Raw API responses are never passed through to the agent. Each server transforms upstream JSON into a simplified, consistent shape, removing nested links, flattening property structures, and stripping fields that are not useful for agent reasoning. This keeps tool responses compact and predictable.

Error Handling

All servers follow the same error pattern. Missing credential headers return a tool error: "missing X-<Header-Name> header". Invalid parameters return: "Invalid parameters: <detail>". Upstream API errors return: "<Service> API error <status>: <message>" (response bodies are truncated to 500 characters). Rate limit responses (HTTP 429) return a specific message asking the agent to wait.

Errors are returned as mcp.NewToolResultError(), not as HTTP errors. The MCP transport always succeeds; the tool result carries the error.

Tool Annotations

Every tool carries MCP hint annotations that describe its behaviour:

Annotation Read tools Write tools
readOnlyHint true false
destructiveHint false false
idempotentHint true false
openWorldHint false false

No tools in any server are marked as destructive. Write tools are not idempotent (creating or updating resources has side effects).

10.2 Cliniko MCP Server

Cliniko is a practice management system used by allied health clinics. This server provides read-only access to patients, appointments, treatment notes, and invoices. All six tools are read-only at the read trust level.

Authentication

Header Required Description
X-Cliniko-Api-Key Yes The user's personal Cliniko API key

The API key determines both identity and data access: each clinician has their own key with visibility scoped by Cliniko's internal permissions. The server extracts the regional shard from the key suffix (for example, -au1, -uk1) and routes requests to the correct regional API endpoint (api.<shard>.cliniko.com/v1). If no shard suffix is present, it defaults to au1.

Authentication to the upstream API uses HTTP Basic Auth with the API key as the username and an empty password.

Meta Endpoint

GET /meta
{
  "name": "cliniko",
  "version": "1.0.0",
  "description": "Cliniko practice management system integration. Provides access to patients, appointments, treatment notes, and invoices.",
  "documentationUrl": "https://docs.thinklio.ai/integrations/cliniko",
  "credentialConfig": {
    "scope": "user",
    "headers": [
      {
        "name": "X-Cliniko-Api-Key",
        "label": "Cliniko API Key",
        "description": "Your personal Cliniko API key. Find it in Cliniko, then My Info, then Manage API Keys.",
        "required": true
      }
    ]
  },
  "tools": [
    { "name": "cliniko_search_patients", "trustLevel": "read", "description": "Search for patients by last name." },
    { "name": "cliniko_get_patient", "trustLevel": "read", "description": "Get full details of a specific patient." },
    { "name": "cliniko_list_todays_appointments", "trustLevel": "read", "description": "List all appointments for today." },
    { "name": "cliniko_get_treatment_notes", "trustLevel": "read", "description": "Get treatment notes for a patient." },
    { "name": "cliniko_list_invoices", "trustLevel": "read", "description": "List invoices for a patient." },
    { "name": "cliniko_get_invoice", "trustLevel": "read", "description": "Get full details of a specific invoice." }
  ]
}

Tools

cliniko_search_patients

Search for patients by last name. Returns matching patients with their ID, name, contact details, and date of birth.

Parameter Type Required Description
query string Yes Search term, matches against patient last name
limit number No Maximum results to return (default: 10, max: 50)

Response shape:

{
  "patients": [
    { "id": "...", "name": "Jane Smith", "email": "...", "phone": "...", "dateOfBirth": "1990-01-15" }
  ],
  "total": 3
}

The Cliniko API uses the filter syntax q[]=last_name:~<value> for fuzzy last-name matching. The search term is URL-encoded before being sent.

cliniko_get_patient

Get full details of a specific patient by their Cliniko ID.

Parameter Type Required Description
patientId string Yes Cliniko patient ID

Response shape:

{
  "id": "...",
  "firstName": "Jane",
  "lastName": "Smith",
  "email": "...",
  "phone": "...",
  "dateOfBirth": "1990-01-15",
  "address": "123 Main St, Sydney, NSW, 2000, AU",
  "createdAt": "2024-01-01T00:00:00Z"
}
cliniko_list_todays_appointments

List all appointments for today (UTC). Optionally filter by practitioner.

Parameter Type Required Description
practitionerId string No Filter to a specific practitioner by their Cliniko ID

Response shape:

{
  "appointments": [
    { "id": "...", "patient": "Jane Smith", "startsAt": "...", "endsAt": "...", "arrived": true, "cancelled": false }
  ],
  "total": 12
}

Queries individual_appointments filtered to today's date range. Returns up to 50 appointments.

cliniko_get_treatment_notes

Get treatment notes for a specific patient.

Parameter Type Required Description
patientId string Yes Cliniko patient ID
limit number No Maximum notes to return (default: 10)

Response shape:

{
  "notes": [
    { "id": "...", "date": "...", "practitioner": "Dr. Jones", "title": "Initial Consultation", "draft": false }
  ],
  "total": 5
}

Note body content is not included in the response, only metadata. The draft field indicates whether the note has been finalised.

cliniko_list_invoices

List invoices for a specific patient.

Parameter Type Required Description
patientId string Yes Cliniko patient ID

Response shape:

{
  "invoices": [
    { "id": "...", "number": 1042, "date": "2025-03-15", "amount": "150.00", "status": "Paid" }
  ],
  "total": 8
}
cliniko_get_invoice

Get full details of a specific invoice including line items.

Parameter Type Required Description
invoiceId string Yes Cliniko invoice ID

Response shape:

{
  "id": "...",
  "number": 1042,
  "date": "2025-03-15",
  "total": "150.00",
  "tax": "15.00",
  "net": "135.00",
  "status": "Paid",
  "items": [
    { "name": "Initial Consultation", "code": "IC001", "quantity": "1", "unitPrice": "150.00", "tax": "15.00", "total": "165.00" }
  ]
}

Invoice items are fetched separately via the invoice_items endpoint and merged into the response.

10.3 Notion MCP Server

This server integrates with the Notion API to provide workspace access: searching, reading and writing pages, querying databases (data sources), managing comments, and listing users. It covers both read and write operations across 15 tools.

Authentication

Header Required Description
X-Notion-Api-Key Yes Notion internal integration token

The token is scoped to a Notion workspace. The integration must be added to specific pages and databases in Notion before it can access them. The server sends this as a Bearer token and pins the API version to 2025-09-03.

Meta Endpoint

GET /meta
{
  "name": "notion",
  "version": "1.0.0",
  "description": "Notion workspace integration. Provides access to pages, databases, comments, and users across the connected workspace.",
  "documentationUrl": "https://docs.thinklio.ai/integrations/notion",
  "credentialConfig": {
    "scope": "account",
    "headers": [
      {
        "name": "X-Notion-Api-Key",
        "label": "Notion Integration Token",
        "description": "Internal integration token for your Notion workspace. Create one at notion.so/my-integrations.",
        "required": true
      }
    ]
  },
  "tools": [
    { "name": "notion_search", "trustLevel": "read" },
    { "name": "notion_list_users", "trustLevel": "read" },
    { "name": "notion_get_page", "trustLevel": "read" },
    { "name": "notion_get_page_content", "trustLevel": "read" },
    { "name": "notion_create_page", "trustLevel": "elevated" },
    { "name": "notion_update_page", "trustLevel": "standard" },
    { "name": "notion_update_page_content", "trustLevel": "standard" },
    { "name": "notion_get_database", "trustLevel": "read" },
    { "name": "notion_get_data_source", "trustLevel": "read" },
    { "name": "notion_query_data_source", "trustLevel": "read" },
    { "name": "notion_get_block_children", "trustLevel": "read" },
    { "name": "notion_append_blocks", "trustLevel": "elevated" },
    { "name": "notion_list_comments", "trustLevel": "read" },
    { "name": "notion_create_comment", "trustLevel": "standard" },
    { "name": "notion_get_self", "trustLevel": "read" }
  ]
}

Response Shaping: Property Flattening

The Notion API returns deeply nested property structures. This server flattens them into simple key-value pairs. For example, a Notion page with a status property like {"Status": {"type": "status", "status": {"name": "Done"}}} is flattened to {"Status": "Done"}.

Supported property types and their flattened representations:

Notion type Flattened to
title, rich_text Plain string
number, checkbox, url, email, phone_number Raw value
date "2025-01-15" or "2025-01-15 -> 2025-01-20" for ranges
select, status Option name string
multi_select, people, relation, files Array of strings (names, IDs, or URLs)
formula, rollup Computed value
unique_id "PREFIX-123" or just the number
created_time, last_edited_time ISO timestamp
created_by, last_edited_by User name string

Tools

Search and Discovery

notion_search. Search across the Notion workspace for pages and data sources by title. Parameters: query (string, required), filter (string, optional, page or data_source), limit (number, optional, default 10, max 100). Response: { "results": [{ "id", "type", "title", "url", "lastEditedTime" }], "hasMore": false }.

notion_list_users. List all users in the Notion workspace. Useful for finding user IDs to assign in people properties. Parameters: limit (number, optional, default 50, max 100). Response: { "users": [{ "id", "name", "type", "email", "avatarUrl" }], "hasMore": false }.

Pages

notion_get_page. Get a page's properties by its Notion ID. Returns the page title, all properties (status, dates, people, and so on), and metadata. Parameters: pageId (string, required). Properties are flattened as described above. Use notion_get_page_content to read the page body.

notion_get_page_content. Get the full content of a Notion page as markdown. This is the most efficient way to read page content. Parameters: pageId (string, required). Response: { "markdown": "# Page Title\n\nContent here..." }.

notion_create_page (trust level: elevated). Create a new page in Notion. Can be a child of another page or a new record in a data source (database). Parameters: parentId (string, required), parentType (string, required, page or data_source), title (string, required), properties (object, optional), markdown (string, optional). For data source pages, the server fetches the parent's property schema and converts simple key-value properties into Notion's native format automatically. Use notion_get_data_source first to see available properties.

notion_update_page (trust level: standard). Update a page's properties. Use this to change status, dates, checkboxes, assignments, and other database fields. Parameters: pageId (string, required), properties (object, required). The server fetches the page's parent data source schema to convert simple values into Notion's property format.

notion_update_page_content (trust level: standard). Update a page's body content using markdown. Parameters: pageId (string, required), markdown (string, required), mode (string, optional, replace_all or append, defaults to replace_all).

Databases and Data Sources

notion_get_database. Get a database's metadata including its title, description, and property schema. Parameters: databaseId (string, required).

notion_get_data_source. Get a data source's property schema. Shows all available properties, their types, and options (for select and status fields). Parameters: dataSourceId (string, required). Use this before notion_query_data_source or notion_create_page to understand the available fields and valid values.

notion_query_data_source. Query a data source (database) with optional filters and sorting. Parameters: dataSourceId (string, required), filter (object, optional), sorts (array, optional), limit (number, optional, default 20, max 100), startCursor (string, optional).

Filter examples:

// Simple filter
{ "property": "Status", "status": { "equals": "Done" } }

// Compound filter
{
  "and": [
    { "property": "Status", "status": { "equals": "Done" } },
    { "property": "Priority", "select": { "equals": "High" } }
  ]
}

Sort example: [{ "property": "Created", "direction": "descending" }]

Blocks

notion_get_block_children. Get the child blocks of a page or block. Returns one level of blocks with their type and content. Parameters: blockId (string, required), limit (number, optional, default 50, max 100), startCursor (string, optional). For reading full page content, prefer notion_get_page_content (markdown) as it is simpler and more efficient.

notion_append_blocks (trust level: elevated). Append content blocks to a page or block. Use for structured content like tables or code blocks that markdown cannot express well. Parameters: blockId (string, required), children (array, required). For most content, prefer notion_update_page_content with markdown instead.

Comments

notion_list_comments. List comments on a Notion page or block. Parameters: blockId (string, required), limit (number, optional, default 50, max 100).

notion_create_comment (trust level: standard). Add a comment to a Notion page, or reply to an existing discussion thread. Parameters: pageId (string, required if no discussionId), discussionId (string, required if no pageId), content (string, required). One of pageId or discussionId is required.

Utility

notion_get_self. Get information about the Notion integration bot. Useful for verifying the connection is working. No parameters.

10.4 Xero MCP Server

This server integrates with the Xero Accounting API, providing access to contacts, invoices, payments, bank transactions, accounts, items, tax rates, financial reports, and credit notes. It includes both read and write operations across 18 tools.

Authentication

Header Required Description
X-Xero-Access-Token Yes OAuth 2.0 access token (managed by Thinklio)
X-Xero-Tenant-Id Yes Xero tenant/organisation ID

Token lifecycle (refresh, expiry) is handled by Thinklio's credential manager: the MCP server receives a valid token on every request. See 07 Security & Governance for the OAuth flow.

The server uses two upstream API base URLs. The Accounting API at https://api.xero.com/api.xro/2.0 is used for all data operations. The Identity API at https://api.xero.com is used only by xero_get_connections to list accessible tenants.

Meta Endpoint

GET /meta
{
  "name": "xero",
  "version": "1.0.0",
  "description": "Xero accounting integration. Provides access to contacts, invoices, payments, bank transactions, accounts, and financial reports.",
  "documentationUrl": "https://docs.thinklio.ai/integrations/xero",
  "credentialConfig": {
    "scope": "account",
    "headers": [
      { "name": "X-Xero-Access-Token", "managed": true },
      { "name": "X-Xero-Tenant-Id", "managed": true }
    ],
    "oauth": {
      "provider": "xero",
      "authorizeUrl": "https://login.xero.com/identity/connect/authorize",
      "tokenUrl": "https://identity.xero.com/connect/token",
      "scopes": ["openid", "profile", "email", "accounting.transactions", "accounting.contacts", "accounting.reports.read"],
      "tokenExpirySeconds": 1800,
      "refreshable": true
    }
  }
}

Filtering and Pagination

Xero list tools share common pagination and filtering patterns. Most list tools accept limit (default 50, max 100) and page (1-indexed, default 1). Responses include "hasMore": true when more results are available. Filters are constructed as Xero where clauses using the OData-style syntax that the Xero API expects. The server builds these internally from the tool parameters: agents never need to write raw where clauses. Multiple conditions are joined with AND.

Tools

Organisation and Discovery

xero_get_organisation. Get the connected Xero organisation's details including name, country, currency, and tax settings. No parameters.

Response:

{
  "name": "Acme Pty Ltd",
  "legalName": "Acme Proprietary Limited",
  "country": "AU",
  "baseCurrency": "AUD",
  "financialYearEndDay": 30,
  "financialYearEndMonth": 6,
  "taxNumber": "12345678901",
  "organisationType": "COMPANY"
}

xero_get_connections. List Xero organisations accessible with the current token. Useful for verifying which organisations the integration can access. No parameters. This tool only requires X-Xero-Access-Token (not the tenant ID). It calls the Xero Identity API, not the Accounting API.

Contacts

xero_list_contacts. List or search contacts (customers and suppliers). Parameters: search (string, optional), status (string, optional, ACTIVE or ARCHIVED), limit (number, optional), page (number, optional).

xero_get_contact. Get full details of a specific contact including addresses, phones, and balances. Parameters: contactId (string, required). Extends the list shape with firstName, lastName, accountNumber, taxNumber, bankAccountDetails, and addresses.

xero_create_contact (trust level: elevated). Create a new contact (customer or supplier). Parameters: name (string, required), email (string, optional), phone (string, optional), accountNumber (string, optional), taxNumber (string, optional).

Invoices

xero_list_invoices. List invoices with optional filters. Parameters: status (string, optional), type (string, optional, ACCREC for sales invoices or ACCPAY for bills), contactId (string, optional), dateFrom (string, optional), dateTo (string, optional), limit (number, optional), page (number, optional).

xero_get_invoice. Get full details of a specific invoice including line items, payments, and credit notes. Parameters: invoiceId (string, required). Extends the list shape with reference, subTotal, totalTax, lineItems, and payments.

xero_create_invoice (trust level: elevated). Create a new sales invoice (ACCREC) or bill (ACCPAY). Parameters: type (string, required), contactId (string, required), lineItems (array, required), date (string, optional), dueDate (string, optional), reference (string, optional), status (string, optional, defaults to DRAFT). Line item format: { "description": "...", "quantity": 1, "unitAmount": 100, "accountCode": "200", "taxType": "OUTPUT" }. Use xero_list_accounts to find valid account codes and xero_list_tax_rates for tax types.

xero_update_invoice (trust level: standard). Update an existing invoice. Can change status, due date, reference, or line items. Parameters: invoiceId (string, required), status (string, optional), dueDate (string, optional), reference (string, optional), lineItems (array, optional).

Payments

xero_list_payments. List payments, optionally filtered by status or invoice. Parameters: status (string, optional), invoiceId (string, optional), limit (number, optional), page (number, optional).

xero_create_payment (trust level: elevated). Record a payment against an invoice. Parameters: invoiceId (string, required), accountId (string, required), amount (number, required), date (string, optional), reference (string, optional). Use xero_list_accounts with type=BANK to find valid bank account IDs.

Bank Transactions

xero_list_bank_transactions. List bank transactions (spend or receive money). Parameters: type (string, optional), status (string, optional), dateFrom (string, optional), dateTo (string, optional), limit (number, optional), page (number, optional).

xero_get_bank_transaction. Get full details of a specific bank transaction including line items. Parameters: transactionId (string, required).

Accounts and Reference Data

xero_list_accounts. List the chart of accounts. Filter by type, class, or status. Parameters: type (string, optional), class (string, optional), status (string, optional).

xero_list_items. List inventory items. Items can be referenced on invoices and purchase orders. Parameters: limit (number, optional), page (number, optional).

xero_list_tax_rates. List available tax rates for the organisation. Use these when creating invoices or bills. No parameters.

Reports

xero_get_report. Get a financial report from Xero. Parameters: report (string, required; one of ProfitAndLoss, BalanceSheet, TrialBalance, BankSummary, AgedReceivablesByContact, AgedPayablesByContact), fromDate (string, optional), toDate (string, optional), periods (number, optional), timeframe (string, optional, MONTH, QUARTER, or YEAR).

Response:

{
  "name": "Profit and Loss",
  "date": "1 April 2025 to 30 April 2025",
  "updateDate": "...",
  "headers": ["Account", "Amount"],
  "rows": [
    { "section": "Revenue", "values": ["Sales", "50000.00"] },
    { "section": "Summary", "values": ["Net Profit", "12000.00"] }
  ]
}

Xero's nested report structure (Rows, then Sections, then Cells) is flattened into a simple rows array with section labels and value arrays.

Credit Notes

xero_list_credit_notes. List credit notes, optionally filtered by status or contact. Parameters: status (string, optional), contactId (string, optional), limit (number, optional), page (number, optional).

10.5 Trust Level Summary

The trust level assigned to each tool determines which users can access it based on their permission level. See 07 Security & Governance for the full trust level model.

Trust level Tools
read All Cliniko tools (6), notion_search, notion_list_users, notion_get_page, notion_get_page_content, notion_get_database, notion_get_data_source, notion_query_data_source, notion_get_block_children, notion_list_comments, notion_get_self, xero_get_organisation, xero_get_connections, xero_list_contacts, xero_get_contact, xero_list_invoices, xero_get_invoice, xero_list_payments, xero_list_bank_transactions, xero_get_bank_transaction, xero_list_accounts, xero_list_items, xero_list_tax_rates, xero_get_report, xero_list_credit_notes
standard notion_update_page, notion_update_page_content, notion_create_comment, xero_update_invoice
elevated notion_create_page, notion_append_blocks, xero_create_contact, xero_create_invoice, xero_create_payment

No tools require admin trust level. No tools are marked as destructive.

11. Implementation Phases

Phase 1: Core API Surfaces

  • Implement the Gateway's public routing layer with versioned path prefixes (/v1/...).
  • Deploy Channel API: message send, interaction retrieval, and chat history endpoints.
  • Deploy Platform API: agent CRUD, context assignments, and basic job dispatch.
  • Implement common conventions: envelope format, pagination, error codes, rate limiting.
  • Deploy authentication middleware resolving Clerk JWTs and API keys to the unified authorisation model.

Phase 2: Integration API and Webhook Delivery

  • Implement tool registration endpoints and the MCP discovery cycle (24-hour refresh).
  • Deploy the tool execution contract path with governance checks, credential injection, and audit logging.
  • Implement the Webhook Delivery Service: event subscription registration, reliable delivery with exponential backoff, circuit breaking for unhealthy endpoints.
  • Deploy event subscription endpoints for external consumers.

Phase 3: Accounts, Onboarding, and Team Management

  • Deploy the full accounts and onboarding API (account CRUD, member management, invitations, teams).
  • Implement the four-tier role model with Clerk organisation mapping.
  • Deploy channel connection and OTP verification flow.
  • Implement invitation email delivery via Postmark.

Phase 4: MCP Server Deployment

  • Deploy the Cliniko MCP server (6 read-only tools, Go, Docker).
  • Deploy the Notion MCP server (15 tools across read, standard, and elevated trust levels, Go, Docker).
  • Deploy the Xero MCP server (18 tools with OAuth 2.0 managed by Thinklio, Go, Docker).
  • Implement the /meta endpoint contract for credential configuration discovery.
  • Integration-test all three servers against Thinklio's tool execution path.

Phase 5: Platform Services and Administration API

  • Deploy platform services and LLM configuration endpoints.
  • Deploy credit balance and ledger query endpoints.
  • Deploy app administration endpoints (kill switch, service registry, model registry).
  • Implement usage reporting and cost attribution across all three API surfaces.

Phase 6: Developer Experience and Documentation

  • Publish the tool integration developer guide as hosted documentation.
  • Deploy the MCP Inspector integration for local testing.
  • Implement the developer portal with interactive API reference (OpenAPI-generated).
  • Deploy sandbox accounts for external developer testing.

12. Revision History

Version Date Description
1.0.0 April 2026 Consolidated from old docs 09 (API Contracts v03), 16 (External API Strategy v02), 28 (API Reference: Accounts & Onboarding v02), 51 (Tool Integration Developer Guide v02), and 53 (MCP Server Reference v01).