Read-only HTTP API and MCP server for Ktrl marketing analytics: your apps, spend recommendations, and cohort performance reports.
Machine-readable: openapi.json · api.md · mcp.md · llms.txt
https://api.kohort.io/api/v1/mcpTransport: Streamable HTTP. Every call is an ordinary HTTP POST with a JSON-RPC 2.0 body — there is no session to open or keep alive, so each request stands on its own. GET and DELETE return 405. The 2025 protocol revisions are served to every client; clients that declare the MCP Apps extension (`io.modelcontextprotocol/ui`) in their requests are served protocol 2026-07-28 as well. Rate limit: 60 requests per minute per company, shared with the REST API.
Authorization: Bearer kht_<your-api-key> on every request. Create a key in the Ktrl platform under Settings → API Keys.Your Ktrl user is connected to your company the first time you log in. Every tool call only returns data for the apps that company can access.
| Header | Required | Description |
|---|---|---|
Authorization | Required | Bearer kht_<your-api-key>. |
Content-Type | Required | application/json |
Accept | Required | Must include both application/json and text/event-stream. Sending only one is the most common reason the connection fails. |
Run this once — Claude Code remembers the server and your key.
claude mcp add --transport http ktrl https://api.kohort.io/api/v1/mcp --header "Authorization: Bearer kht_<your-api-key>"
Add this to ~/.cursor/mcp.json, then reload Cursor.
{
"mcpServers": {
"ktrl": {
"url": "https://api.kohort.io/api/v1/mcp",
"headers": {
"Authorization": "Bearer kht_<your-api-key>"
}
}
}
}The official tool for trying tools out before wiring them into a client. Start it, choose the Streamable HTTP transport, paste in the endpoint URL, then add an `Authorization: Bearer kht_<your-api-key>` header.
npx @modelcontextprotocol/inspector
Nothing here is specific to one client — anything that can POST with headers works. This example lists the available tools.
curl -X POST "https://api.kohort.io/api/v1/mcp" \
-H "Authorization: Bearer kht_<your-api-key>" \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'ktrl_list_appsList appsLists the live apps in your Ktrl account. Use the returned 'id' as the appId argument for the other ktrl tools. In hosts that show interactive views, it also shows the apps as cards the user can pick from.
Takes no arguments.
ktrl_get_recommendationsGet spend recommendationsLatest marketing spend recommendations for an app. Each row has an action (SCALE, HOLD or REDUCE), daily spend, installs, CPI, confidence score and current vs target ROAS per campaign (or per campaign+country). Rows are capped by limit; the response says if it was truncated. In hosts that show interactive views, it also shows the recommendations as cards like the Ktrl web app; picking a card asks for the cohort report of that campaign, and each card links to Ktrl where the chat allows it.
| Argument | Type | Required | Description | Default |
|---|---|---|---|---|
appId | integer | Required | App id. Get it from ktrl_list_apps | — |
granularity | 'campaign' | 'campaign_geo' | Optional | 'campaign' = one row per campaign with all geos aggregated; 'campaign_geo' = one row per campaign+country (can return many rows) | campaign |
countries | string[] | Optional | Country code filter, e.g. ['US', 'GB']. Only valid with granularity 'campaign_geo' | — |
platforms | string[] | Optional | Platform filter: 'IOS' and/or 'ANDROID' | — |
networks | string[] | Optional | Ad network filter, e.g. Facebook, Google | — |
limit | integer | Optional | Maximum number of rows to return. Rows are sorted by spend, so the top rows matter most | 100 |
ktrl_get_reportGet cohort reportCohort performance report for an app: spend, installs, CPI, LTV and ROAS per install cohort, broken down by campaign (optionally by country). Rows are capped by limit; the response says if it was truncated — narrow the date range or filters to see the rest. In hosts that show interactive views, it also shows the rows as a chart the user can switch between metrics, filter and download.
| Argument | Type | Required | Description | Default |
|---|---|---|---|---|
appId | integer | Required | App id. Get it from ktrl_list_apps | — |
granularity | 'campaign' | 'campaign_geo' | Optional | 'campaign' = one row per install cohort + campaign; 'campaign_geo' also splits by country (can return many rows) | campaign |
metrics | ('spend' | 'installs' | 'cpi' | 'ltv' | 'roas')[] | Optional | Metrics to include. 'ltv' and 'roas' add one column per roasType x revenueType x dsiValue. cpi and ltv are null when installs are 0, and roas is null when spend is 0: undefined, not 0 | ["spend","installs","cpi"] |
roasTypes | ('PAID' | 'INCREMENTAL_BLENDED' | 'FULL_BLENDED')[] | Optional | PAID = paid traffic only; INCREMENTAL_BLENDED / FULL_BLENDED include organic attribution | ["PAID"] |
revenueTypes | ('GROSS' | 'NET')[] | Optional | GROSS is before fees; NET is net of fees, taxes and refunds | ["GROSS"] |
dsiValues | integer[] | Optional | Days-since-install horizons for ltv/roas metrics (max 5). Only used and validated when metrics include 'ltv' or 'roas'; invalid values are rejected with the list of available ones | [7,365] |
startDate | string | Optional | Install cohort start date, YYYY-MM-DD. Default: 30 days before endDate | — |
endDate | string | Optional | Install cohort end date, YYYY-MM-DD. Default: the app's latest data date | — |
countries | string[] | Optional | Country code filter, e.g. ['US', 'GB']. Only valid with granularity 'campaign_geo' | — |
platforms | string[] | Optional | Platform filter: 'IOS' and/or 'ANDROID' | — |
networks | string[] | Optional | Ad network filter, e.g. Facebook, Google | — |
campaigns | string[] | Optional | Campaign name filter (exact match, case-sensitive) | — |
installGrouping | 'DAILY' | 'WEEKLY' | 'MONTHLY' | Optional | How install cohorts are bucketed. Default: WEEKLY for apps that require weekly aggregation, DAILY otherwise | — |
limit | integer | Optional | Maximum number of rows to return | 100 |
ktrl_export_reportExport report fileBuilds the full cohort report as a CSV or Parquet file, with no row limit (the same file the REST /v1/report endpoint returns), and replies with a download link that works for 15 minutes. Use it when the user wants the report as a file, and give them the link. For rows to read in the conversation, use ktrl_get_report.
| Argument | Type | Required | Description | Default |
|---|---|---|---|---|
appId | integer | Required | App id. Get it from ktrl_list_apps | — |
granularity | 'campaign' | 'campaign_geo' | Optional | 'campaign' = one row per install cohort + campaign; 'campaign_geo' also splits by country (can return many rows) | campaign |
metrics | ('spend' | 'installs' | 'cpi' | 'ltv' | 'roas')[] | Optional | Metrics to include. 'ltv' and 'roas' add one column per roasType x revenueType x dsiValue. cpi and ltv are null when installs are 0, and roas is null when spend is 0: undefined, not 0 | ["spend","installs","cpi"] |
roasTypes | ('PAID' | 'INCREMENTAL_BLENDED' | 'FULL_BLENDED')[] | Optional | PAID = paid traffic only; INCREMENTAL_BLENDED / FULL_BLENDED include organic attribution | ["PAID"] |
revenueTypes | ('GROSS' | 'NET')[] | Optional | GROSS is before fees; NET is net of fees, taxes and refunds | ["GROSS"] |
dsiValues | integer[] | Optional | Days-since-install horizons for ltv/roas metrics (max 5). Only used and validated when metrics include 'ltv' or 'roas'; invalid values are rejected with the list of available ones | [7,365] |
startDate | string | Optional | Install cohort start date, YYYY-MM-DD. Default: 30 days before endDate | — |
endDate | string | Optional | Install cohort end date, YYYY-MM-DD. Default: the app's latest data date | — |
countries | string[] | Optional | Country code filter, e.g. ['US', 'GB']. Only valid with granularity 'campaign_geo' | — |
platforms | string[] | Optional | Platform filter: 'IOS' and/or 'ANDROID' | — |
networks | string[] | Optional | Ad network filter, e.g. Facebook, Google | — |
campaigns | string[] | Optional | Campaign name filter (exact match, case-sensitive) | — |
installGrouping | 'DAILY' | 'WEEKLY' | 'MONTHLY' | Optional | How install cohorts are bucketed. Default: WEEKLY for apps that require weekly aggregation, DAILY otherwise | — |
format | 'parquet' | 'csv' | Optional | CSV opens in spreadsheets; Parquet is smaller and keeps column types, better for large files | csv |
ktrl_get_roas_historyGet ROAS historyHow each install cohort's predicted ROAS changed as the cohort aged — a proxy for model stability. Each row is one cohort (per campaign or campaign+country at finer granularities) with roas_age_N = predicted ROAS at the requested DSI when the cohort was N days old; ages with no data are omitted. Start at 'app' granularity, then filter to specific campaigns or countries. Rows are capped by limit and response size; the response says if it was truncated. For bulk data use the /v1/roas-history file export.
| Argument | Type | Required | Description | Default |
|---|---|---|---|---|
appId | integer | Required | App id. Get it from ktrl_list_apps | — |
granularity | 'app' | 'campaign' | 'campaign_geo' | Optional | 'app' = one row per install cohort (start here); 'campaign' / 'campaign_geo' split by campaign (and country), only for campaigns in the latest successful recommendation run, and can return many rows | app |
dsi | integer | Optional | Days-since-install horizon the ROAS is measured at. Default: the app's target DSI. Invalid values are rejected with the list of available ones | — |
roasType | 'PAID' | 'INCREMENTAL_BLENDED' | 'FULL_BLENDED' | Optional | PAID = paid traffic only; INCREMENTAL_BLENDED / FULL_BLENDED include organic attribution. Default: the app's target ROAS type | — |
revenueType | 'GROSS' | 'NET' | Optional | GROSS is before fees; NET is net of fees, taxes and refunds. Default: the app's target revenue type | — |
installGrouping | 'DAILY' | 'WEEKLY' | 'MONTHLY' | Optional | How install cohorts are bucketed. DAILY is rejected for apps that require weekly aggregation | WEEKLY |
startDate | string | Optional | Install cohort start date, YYYY-MM-DD. Default: 90 days before endDate. Max range 455 days | — |
endDate | string | Optional | Install cohort end date, YYYY-MM-DD. Default: the app's latest data date | — |
countries | string[] | Optional | Country code filter, e.g. ['US', 'GB']. Not valid with granularity 'campaign' | — |
platforms | string[] | Optional | Platform filter: 'IOS' and/or 'ANDROID' | — |
networks | string[] | Optional | Ad network filter, e.g. Facebook, Google | — |
campaigns | string[] | Optional | Campaign name filter (exact match, case-sensitive) | — |
limit | integer | Optional | Maximum number of rows to return. Large responses are also cut by size | 50 |
ktrl_get_alertsGet alertsAlerts Ktrl raised about an app's campaigns — what changed and what to do about it. Each row has the alert title, a one-line summary, the campaign, network, platform and country it concerns, the daily spend affected, a fuller explanation and a recommended action. Returns the latest alert run by default; pass startDate for history. Rows are capped by limit; the response says if it was truncated.
| Argument | Type | Required | Description | Default |
|---|---|---|---|---|
appId | integer | Required | App id. Get it from ktrl_list_apps | — |
alertGroups | ('BIDS' | 'ANALYTICS')[] | Optional | Alert group filter. 'BIDS' = live bid more than ±5% off the paced recommendation; 'ANALYTICS' = performance, confidence and custom-rule alerts. Omit for both | — |
countries | string[] | Optional | Country codes, e.g. ['US', 'GB'], or 'GLOBAL' for campaign-level alerts (all analytics alerts, plus bid alerts not split by country). Omit for all | — |
platforms | string[] | Optional | Platform filter: 'IOS', 'ANDROID' and/or 'WEB' | — |
networks | string[] | Optional | Ad network filter, e.g. Facebook, Google | — |
campaigns | string[] | Optional | Campaign name filter, exact match | — |
startDate | string | Optional | Start of the alert date range (YYYY-MM-DD, UTC, inclusive). Omit both dates to get only the latest run, as shown in the Ktrl inbox | — |
endDate | string | Optional | End of the alert date range (YYYY-MM-DD, UTC, inclusive). Defaults to today; requires startDate | — |
limit | integer | Optional | Maximum number of rows to return. Rows are sorted newest run first, then by daily spend, so the top rows matter most | 100 |
ktrl_get_profileGet profileIdentifies the Ktrl account these credentials belong to: a stable id, plus the name and email of the signed-in user when connected through a login rather than an API key.
Takes no arguments.
Tools that return rows take a limit — 100 by default (50 for ktrl_get_roas_history), 500 at most. ktrl_get_report and ktrl_get_roas_history also cut large responses by size. If more rows matched than you got back, the response sets truncated: true. When that happens, narrow the date range or add filters rather than raising the limit.
| Status | Meaning | Resolution |
|---|---|---|
401 | Your key is missing, malformed, or expired. | Check you are sending Authorization: Bearer kht_<your-api-key>. |
403 | Your key is valid, but it cannot reach the app you asked for — or your user is not connected to a company yet. | Call ktrl_list_apps to see which apps this key can read. |
405 | You sent a GET or DELETE to the MCP endpoint. | Use POST — every MCP call is a POST. |
429 | You have gone over the rate limit. | Wait for the current minute to pass and retry. The limit is 60 requests per minute. |
isError result | The call reached the tool, but the tool rejected one of your arguments — an unsupported dsiValue, for example, or a countries filter used with campaign granularity. | Read the error message. It names the argument at fault and, where it can, lists the values you can use instead. |