A valid request URL is required to generate request examples{
"jsonrpc": "2.0",
"id": 1,
"result": {
"content": [
{
"type": "text",
"text": "{\"environment\":\"test\",\"contacts\":[]}"
}
],
"structuredContent": {
"environment": "test",
"contacts": []
}
}
}{
"error": {
"code": "forbidden",
"message": "your role does not allow this action"
}
}{
"error": {
"code": "forbidden",
"message": "your role does not allow this action"
}
}Model Context Protocol endpoint
The endpoint an MCP client connects to, so an AI agent can answer questions about your UserKit data in the tool your team already works in. You do not call this by hand — you paste the URL and an API key into a client’s configuration, and the client speaks Model Context Protocol over it.
It is outside /v1 on purpose, the provider-webhook rule: the version prefix is a promise about endpoints you call from your own code, and this URL lives in a configuration file on somebody’s machine. What moves instead is the protocol version, negotiated in initialize.
Read-only is the default, and the three exceptions are structural rather than a policy. An agent can change your help centre (collections and articles, where every edit is a new version and publishing is a separate act), your catalogue (the plans, prices and features you sell) and your changelog (release notes, where writing a post and announcing it are separate calls — and where publishing has no undo: it enters a cacheable page and delivers a webhook). Everything else answers a question and nothing else: no tool touches a contact, creates a charge, moves a subscription, stores a payment credential or answers with a credential of any kind. A price is never edited either — repricing archives the old row and inserts a new one, so what an existing customer agreed to pay is unreachable from here. Your staff audit log is deliberately not among the tools: audit:read is owner-only, and an API key carries no role, so serving it here would route owner-only records out through a credential any member who can mint keys already holds.
Every write is recorded in your audit trail against the API key that made it, rather than against a person — which is the reason to mint a key for the assistant instead of lending it one your backend uses.
The environment is the key’s environment. No tool takes an environment argument, so a test key can only ever show you test data — including active_contacts_usage, which answers zero with counted: false in test, because test contacts are never metered.
Transport. Streamable HTTP, stateless: one JSON-RPC 2.0 message per request, no session id, and no stream — GET /mcp answers 405, and batched messages are refused. The methods implemented are initialize, ping, tools/list, tools/call, prompts/list and prompts/get; a notification (a message with no id) is acknowledged with 202 and no body.
A JSON-RPC error is still 200. The HTTP status describes the transport, and the transport worked. The statuses that really are about the transport still happen: 401 for a key this endpoint refuses, 429 for the per-key rate limit every API-key route carries.
A tool that cannot answer is not an error either. An id that names nothing, or an argument the model got wrong, comes back as a normal result with isError: true and a sentence in content — because a JSON-RPC error is consumed by the client and never reaches the model, which would then make the same wrong call again.
The reads: run_doctor, search_docs, list_contacts, get_contact, list_segments, active_contacts_usage, list_customers, list_subscriptions, get_subscription, list_invoices, get_catalogue, the help centre’s four, and list_changelog_posts and get_changelog_post. The writes: the help centre’s nine; the catalogue’s create_catalogue_plan, update_catalogue_plan, create_catalogue_price, reprice_catalogue_price, archive_catalogue_price, create_catalogue_feature, update_catalogue_feature, set_catalogue_plan_feature and remove_catalogue_plan_feature; and the changelog’s create_changelog_post, update_changelog_post, publish_changelog_post and delete_changelog_post. Call tools/list for their current arguments — it is the contract, and it is answered by the running server, which also annotates each tool with whether it writes and whether it can be retried.
A valid request URL is required to generate request examples{
"jsonrpc": "2.0",
"id": 1,
"result": {
"content": [
{
"type": "text",
"text": "{\"environment\":\"test\",\"contacts\":[]}"
}
],
"structuredContent": {
"environment": "test",
"contacts": []
}
}
}{
"error": {
"code": "forbidden",
"message": "your role does not allow this action"
}
}{
"error": {
"code": "forbidden",
"message": "your role does not allow this action"
}
}Authorizations
An organization API key, uk_sk_live_… or uk_sk_test_…. The environment is resolved from the stored key row on every request, never from a request parameter.
Every route behind this credential is rate limited per key — 1000 requests a minute — with a second ceiling of 3000 a minute across all the keys of one environment, so splitting your traffic across keys isolates it and minting more keys does not buy more of it. A test key can never spend a live key's allowance. Every response carries the current state in headers; see the rate-limits guide.
Body
One JSON-RPC 2.0 message. An array (a batch) is refused: MCP removed batching, and a server that answered part of one would drop calls where nobody can see it.
"2.0"Notifications such as notifications/initialized are accepted too and answered with 202.
initialize, ping, tools/list, tools/call A string or a number, echoed back verbatim. Omit it and the message is a notification: it is acknowledged with 202 and never answered.
The method's parameters. For tools/call: name and arguments.
Response
The JSON-RPC response. It carries result or error, never both — and an error here is the protocol refusing (unknown method, unknown tool, malformed message), not the transport failing.