Ownware
Home›SaaS Kit›API
SaaS Kit · API

SaaS Kit API, as shipped in the download

Ownware SaaS Kit — Control-Plane API & MCP (3.1)

The kit is a control plane, not a business app. It provisions tenants, wraps a product for each one, and keeps the audit trail. So this document is shorter than a product's, and the shape is different in one way worth saying at the top:

There is no REST API. The kit has exactly one machine door — POST /mcp — and it is read-only. Everything that changes a tenant is a human click in the console, behind a session and a CSRF token. That is a decision, not a gap, and the reasoning is under Why there is no REST API below.

Authentication

One key type: a console API key, minted in Console → Admin → API & AI (/admin/api-settings).

Authorization: Bearer ck_<id>_<secret>

The token is shown once at mint time and stored only as a SHA-256 hash, so the console cannot show it to you again — revoke and mint a new one instead. Keys are revocable individually (/admin/api-settings/revoke).

The key is an owner-level credential for your whole fleet. It is not a tenant key and there is no per-tenant scoping: a key that can list tenants can list all of them. Treat it as you would the console password.

The one machine door

AddressPOST https://<your-console-host>/mcp
AuthAuthorization: Bearer ck_… — 401 without it
TransportMCP over streamable HTTP, stateless: one JSON-RPC 2.0 request in, one response out
Wrong methodGET or HEAD answers 405 with allow: POST and a JSON hint

It lives on the console host only. The route is mounted after tenant dispatch has returned, so /mcp on a tenant subdomain can never reach the control plane — a tenant's own product answers there with its own door and its own key, and the two cannot be confused.

The 405 matters more than it looks. /your-ai-can-vet-us tells a visiting agent to run curl -sI <host>/mcp and expect 405 with allow: POST; a 404 reads as *"this host has no MCP door"*, which is what happened until 2026-09-07, when the route was matched on POST alone. Answering the wrong method without a key leaks nothing and is the difference between an agent finding the door and concluding there is none.

Why there is no REST API

ControlApi mints, authenticates and revokes keys. It does not route anything. The console's 49 registered routes (34 GET, 21 POST, across 49 distinct paths) are session-guarded HTML for a person, and the operations that matter — create a tenant, suspend one, change a plan, edit settings — are deliberately not reachable by any token.

A control plane's write operations are the ones you least want an agent to reach by accident: a mistaken tenant_delete is somebody's live business. So the kit gives an agent everything it needs to answer questions about the fleet, and nothing that can change it.

MCP — the agent door

The kit speaks the Model Context Protocol, so Claude, ChatGPT agents, n8n's AI nodes or your own code can inspect the fleet without a screen.

curl -s -X POST https://console.example.com/mcp \
  -H "Authorization: Bearer ck_1_xxxx" -H 'Content-Type: application/json' \
  -d '{"jsonrpc":"2.0","id":1,"method":"initialize",
       "params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"my-agent","version":"1"}}}'

Then tools/list to discover, tools/call to act:

curl -s -X POST https://console.example.com/mcp \
  -H "Authorization: Bearer ck_1_xxxx" -H 'Content-Type: application/json' \
  -d '{"jsonrpc":"2.0","id":2,"method":"tools/call",
       "params":{"name":"tenant_counts","arguments":{}}}'

Protocol versions accepted: 2025-06-18, 2025-03-26, 2024-11-05.

The server introduces itself with one paragraph of instructions, which says the same thing this document does: *"Read-only control-plane access: inspect the tenant fleet, registered products and the audit trail. Provisioning, suspension and settings are console-only human actions."*

Connect it to an assistant

Mint the key first: Console → Admin → API & AI.

Every client needs the same three facts, and nothing in the handshake is vendor-specific:

The addresshttps://console.example.com/mcp
The keyheader Authorization: Bearer ck_1_xxxx
The transportMCP over streamable HTTP, stateless

Claude — one command, or the same URL and header as a custom connector in the desktop and web apps:

claude mcp add --transport http saas-kit https://console.example.com/mcp \
  --header "Authorization: Bearer ck_1_xxxx"

ChatGPT and the OpenAI API — one entry in the Responses API's tools array (in ChatGPT itself, the same URL and key go in as a connector):

{
  "type": "mcp",
  "server_label": "saas-kit",
  "server_url": "https://console.example.com/mcp",
  "authorization": "ck_1_xxxx",
  "require_approval": "never"
}

Own Your AI reads a list of servers in this shape:

{
  "mcpServers": [
    { "id": "saas-kit", "name": "Ownware SaaS Kit", "url": "https://console.example.com/mcp",
      "token": "ck_1_xxxx", "enabled": true }
  ]
}

**Every other client spells the same three facts differently — copy the shape from its own documentation, not from here.** VS Code is the clearest example of why: its configuration reference (read 6 September 2026) puts servers in .vscode/mcp.json under a "servers" object — *"an object that maps server names to their configurations"* — not an mcpServers array. Pasted as-is, the block above will not load there. The id, the URL and the token are what travel; the JSON around them belongs to whichever client you are configuring.

A local model, n8n, or your own code — n8n's MCP Client node takes the URL and the same Authorization: Bearer header; a model running on your own machine reaches it through any MCP client, so nothing leaves your network at all. Writing it yourself is one POST of JSON-RPC 2.0:

curl -X POST https://console.example.com/mcp \
  -H "Authorization: Bearer ck_1_xxxx" -H "Content-Type: application/json" \
  -d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'

Any client that speaks MCP's streamable-HTTP transport works, including ones that do not exist yet: the kit implements the protocol, not an integration with a particular vendor.

Tools

Four, and every one is read-only. They are annotated readOnlyHint: true in tools/list, so a client can see it before it calls anything.

ToolWrites?ArgumentsWhat it does
tenants_listnostatus (optional)Every tenant with product, plan and status, plus the count. Filters to one status when given
tenant_getnosubdomain (required)Public detail for one tenant. Returns {"error": "no such tenant", …} rather than guessing
tenant_countsnononeTenant counts grouped by status
audit_searchnoactor, entity, action (all optional)The control-plane audit trail, most recent first, max 100 rows. Empty filters are dropped rather than matched as blanks

The kit's tool set is read-only by construction, not by key scope. In a product, a read-only key hides the write tools; here there are no write tools to hide, so a full-access console key and a read-only one see exactly the same four. Nothing an agent can reach changes a tenant.

Every /mcp request is written to the audit trail as mcp.request, against the key's own id and label. An agent's activity is as visible as a person's.

What an agent cannot do, and what it cannot see

Cannot do, because no tool exists: create a tenant · suspend, resume or cancel one · change a plan · register or remove a product · edit settings · mint or revoke keys · delete anything. All of those are console clicks behind a session and a CSRF token.

Cannot see: a tenant is projected to exactly five fields — subdomain, product, plan, status, created_at. Database credentials, connection strings, support-SSO material, billing identifiers and the tenant's own application data are never in an MCP response, and the projection is applied in one place so a new tool cannot widen it by accident.

A tenant's own product data is not reachable from here at all. The control plane knows that a tenant exists and which product it runs; it does not read inside it. To ask a product a question, use that product's MCP door on the tenant's own host, with a key minted there.

Operational endpoints

EndpointGuardWhat it does
GET /healthznoneLiveness: {"ok":true,"app":"saas-kit","version":"3.1.13"}
GET /.well-known/agent.jsonnoneThe vet facts as JSON, with the live and coming-soon counts
GET /.well-known/security.txtnoneRFC 9116 machine-readable security contact
GET /sitemap.xmlnoneEvery indexable URL on the plane
GET /admin/tenants.csvowner sessionThe tenant table as CSV
GET /admin/audit.csvowner sessionThe audit trail as CSV
GET /admin/backup.jsonowner sessionRedacted control-plane backup
GET /account/export/{sub}tenant account sessionA tenant's own export of what the plane holds about them

/healthz, agent.json, security.txt and sitemap.xml are the only unauthenticated machine routes, and none of them names a tenant.

Limits, honestly

  • audit_search returns at most 100 rows and there is no pagination or cursor. It is for answering "what happened to this tenant recently", not for exporting the trail — use /admin/audit.csv for that.
  • tenants_list is unpaged. At a few hundred tenants that is fine; at a few thousand the response gets large, and the projection is the only thing keeping it small.
  • The version string above is the application's, reported by /healthz and in the MCP handshake. It is not the release number on the download — those are deliberately separate across the whole catalogue, and the zip filename is the one that tracks releases.
  • There is no rate limit on /mcp. The key is the control; if you need throttling, put it in front of the console.

← Back to SaaS Kit · Quickstart · Test run

Affiliate program
Recommend tools people own — earn 35% on every sale. 90-day tracking, instant delivery, payouts by Lemon Squeezy.
Become an affiliate →