{"openapi":"3.0.3","info":{"title":"Ktrl API","version":"1.0.0","description":"Read-only HTTP API for Ktrl marketing analytics.\n\nAn MCP server exposing the same data to LLM clients is available at `https://api.kohort.io/api/v1/mcp` — see https://api.kohort.io/api-docs/mcp.md."},"servers":[{"url":"https://api.kohort.io/api"}],"security":[{"apiKey":[]}],"components":{"securitySchemes":{"apiKey":{"type":"http","scheme":"bearer","description":"Ktrl API key, prefixed kht_. Create one under Settings → API Keys."}}},"tags":[{"name":"Ktrl API","description":"Read-only marketing analytics."}],"paths":{"/v1/apps":{"get":{"operationId":"getApps","summary":"Returns list of all live apps with their names and IDs. App IDs are required when making reporting API requests.","tags":["Ktrl API"],"parameters":[],"responses":{"200":{"description":"JSON array of app objects.","content":{"application/json":{"schema":{"type":"array","items":{"type":"object"}},"example":[{"id":1,"name":"My App","createdAt":"2025-01-15T10:30:00.000Z"}]}}},"401":{"description":"Missing or invalid API key."},"403":{"description":"The API key cannot access the requested app."},"429":{"description":"Rate limit exceeded (60 requests/minute)."}}}},"/v1/recommendations":{"get":{"operationId":"getRecommendations","summary":"Returns UA campaign recommendations for a live app based on the latest successful recommendation run.","tags":["Ktrl API"],"parameters":[{"name":"appId","in":"query","required":true,"description":"App ID to fetch recommendations for. Retrievable from /v1/apps endpoint.","schema":{"type":"integer"}},{"name":"granularity","in":"query","required":false,"description":"Dimension grouping level. Use campaign_geo to get one row per country. Countries filter is only valid with campaign_geo.\n\nControls whether recommendations are grouped at the campaign level (country shown as GLOBAL for multi-country campaigns) or at the campaign–geo level (one row per country).","schema":{"type":"string","enum":["campaign","campaign_geo"],"default":"campaign"}},{"name":"countries","in":"query","required":false,"description":"Comma-separated country codes. Omit to include all. Only valid with campaign_geo granularity.\n\nReduces results to the specified countries. Throws a validation error if used with campaign granularity.","schema":{"type":"string"}},{"name":"platforms","in":"query","required":false,"description":"Comma-separated platform filter. Omit to include all.\n\nReduces results to the specified platforms.","schema":{"type":"string","enum":["ios","android","web"]}},{"name":"networks","in":"query","required":false,"description":"Comma-separated network filter. Omit to include all. Exact string match with network names on Ktrl.\n\nReduces results to the specified ad networks.","schema":{"type":"string"}}],"responses":{"200":{"description":"JSON array of recommendation objects.","content":{"application/json":{"schema":{"type":"array","items":{"type":"object","properties":{"id":{"type":"integer","description":"Unique recommendation ID."},"country":{"type":"string","description":"ISO country code. GLOBAL when a campaign row spans countries; Unknown if missing."},"platform":{"type":"string","description":"IOS, ANDROID or WEB; Unknown if missing."},"network":{"type":"string","description":"Ad network; Unknown if missing."},"campaign":{"type":"string","description":"Campaign name; Unknown if missing."},"action":{"type":"string","description":"Spend recommendation comparing Predicted vs Target ROAS at the Target DSI for cohorts in the bet lookback range*: SCALE (above the HOLD band), HOLD (within the band), REDUCE (below the band, or ROAS missing / non-positive). The band is ±the app's configured action threshold around the target (default ±5%)."},"status":{"type":"string","description":"Confidence tier: HIGH (≥ 0.60), MEDIUM (0.30 to 0.60), LOW (< 0.30, daily spend under 5, or non-traditional network)."},"confidenceScore":{"type":"number","description":"Model confidence, 0 to 1."},"dailySpend":{"type":"number","description":"Average daily spend over the bet lookback range*, in app currency."},"paidInstalls":{"type":"integer","description":"Paid installs over the bet lookback range*."},"incrementalBlendedInstalls":{"type":"integer","description":"Paid plus incremental organic installs over the bet lookback range*."},"fullBlendedInstalls":{"type":"integer","description":"Paid plus incremental organic installs plus a share of all \"true\" organic installs, over the bet lookback range*."},"paidCpi":{"type":"number","description":"Cost per install over the bet lookback range*, paid installs only; 0 if no installs."},"incrementalBlendedCpi":{"type":"number","description":"Cost per install across paid and incremental organic installs over the bet lookback range*; 0 if no installs."},"fullBlendedCpi":{"type":"number","description":"Cost per install across paid and all allocated organic installs over the bet lookback range*; 0 if no installs."},"optimisationDsi":{"type":"integer","description":"Conversion window (days since install) the ad network optimises against."},"targetDsi":{"type":"integer","description":"Payback window (days since install) by which a cohort should hit its Target ROAS."},"optimisationLiveRoas":{"type":"number","description":"Bid target currently live in the ad network. Always in Paid Gross."},"optimisationPacedRoas":{"type":"number","description":"Recommended bid target to move performance toward your Target ROAS, capped in size and cadence by each network's pacing rules. Always in Paid Gross."},"optimisationRawRoas":{"type":"number","description":"Recommended, unconstrained bid target to move performance toward your Target ROAS. Always in Paid Gross."},"optimisationPredictedRoas":{"type":"number","description":"Predicted ROAS at the Optimisation DSI for cohorts in the bet lookback range*. Always in Paid Gross."},"targetPaidGrossRoas":{"type":"number","description":"Target ROAS from paid-install revenue, before fees."},"predictedPaidGrossRoas":{"type":"number","description":"Predicted ROAS at the Target DSI for cohorts in the bet lookback range* from paid-install revenue, before fees."},"targetPaidNetRoas":{"type":"number","description":"Target ROAS from paid-install revenue, net of fees, taxes, and refunds."},"predictedPaidNetRoas":{"type":"number","description":"Predicted ROAS at the Target DSI for cohorts in the bet lookback range* from paid-install revenue, net of fees, taxes, and refunds."},"targetIncrementalBlendedGrossRoas":{"type":"number","description":"Target ROAS from paid plus incremental organic revenue, before fees."},"predictedIncrementalBlendedGrossRoas":{"type":"number","description":"Predicted ROAS at the Target DSI for cohorts in the bet lookback range* from paid plus incremental organic revenue, before fees."},"targetIncrementalBlendedNetRoas":{"type":"number","description":"Target ROAS from paid plus incremental organic revenue, net of fees, taxes, and refunds."},"predictedIncrementalBlendedNetRoas":{"type":"number","description":"Predicted ROAS at the Target DSI for cohorts in the bet lookback range* from paid plus incremental organic revenue, net of fees, taxes, and refunds."},"targetFullBlendedGrossRoas":{"type":"number","description":"Target ROAS from paid plus all organic revenue, before fees."},"predictedFullBlendedGrossRoas":{"type":"number","description":"Predicted ROAS at the Target DSI for cohorts in the bet lookback range* from paid plus all organic revenue, before fees."},"targetFullBlendedNetRoas":{"type":"number","description":"Target ROAS from paid plus all organic revenue, net of fees, taxes, and refunds."},"predictedFullBlendedNetRoas":{"type":"number","description":"Predicted ROAS at the Target DSI for cohorts in the bet lookback range* from paid plus all organic revenue, net of fees, taxes, and refunds."},"createdAt":{"type":"string","description":"ISO 8601 timestamp the recommendation was produced."}}}},"example":[{"id":1234,"country":"GLOBAL","platform":"ANDROID","network":"GOOGLE","campaign":"My Campaign","action":"REDUCE","status":"HIGH","confidenceScore":0.86,"dailySpend":1234.12,"paidInstalls":1234,"incrementalBlendedInstalls":1086,"fullBlendedInstalls":912,"paidCpi":1.01,"incrementalBlendedCpi":1.15,"fullBlendedCpi":1.37,"optimisationDsi":7,"targetDsi":269,"optimisationLiveRoas":0.42,"optimisationPacedRoas":0.39,"optimisationRawRoas":0.338,"optimisationPredictedRoas":0.31,"targetPaidGrossRoas":1,"predictedPaidGrossRoas":1,"targetPaidNetRoas":0.7,"predictedPaidNetRoas":0.72,"targetIncrementalBlendedGrossRoas":1.2,"predictedIncrementalBlendedGrossRoas":1.15,"targetIncrementalBlendedNetRoas":0.85,"predictedIncrementalBlendedNetRoas":0.82,"targetFullBlendedGrossRoas":1.5,"predictedFullBlendedGrossRoas":1.42,"targetFullBlendedNetRoas":1.05,"predictedFullBlendedNetRoas":1.01,"createdAt":"2026-06-04T14:32:17.000Z"}]}}},"401":{"description":"Missing or invalid API key."},"403":{"description":"The API key cannot access the requested app."},"429":{"description":"Rate limit exceeded (60 requests/minute)."}}}},"/v1/report":{"get":{"operationId":"getReport","summary":"Export actual and forecasted metrics data into a single report as a Parquet or CSV file.","tags":["Ktrl API"],"parameters":[{"name":"appId","in":"query","required":true,"description":"App ID to export data for. Retrievable from /v1/apps endpoint.","schema":{"type":"integer"}},{"name":"granularity","in":"query","required":true,"description":"Dimension grouping level.\n\nControls whether data is grouped at the campaign level or at the campaign–geo level.","schema":{"type":"string","enum":["campaign","campaign_geo"]}},{"name":"installGrouping","in":"query","required":false,"description":"Controls how install dates are grouped in the report. Daily grouping is not available for apps with weekly prediction aggregation enabled — use weekly or monthly instead.\n\nGroups install cohort dates by day, week, or month. Weekly/monthly reduces row count by aggregating cohorts. Defaults to weekly for apps that require weekly aggregation, daily otherwise.","schema":{"type":"string","enum":["daily","weekly","monthly"]}},{"name":"metrics","in":"query","required":false,"description":"Comma-separated metrics.\n\nCreates new columns for each metric selected. CPI and LTV are empty when installs are 0, and ROAS is empty when spend is 0.","schema":{"type":"string","enum":["ltv","roas","spend","installs","cpi"],"default":"ltv"}},{"name":"roasTypes","in":"query","required":false,"description":"Comma-separated ROAS types. Used for LTV, ROAS, Installs, and CPI metrics.\n\nCreates a separate column for each selected metric, ROAS type, revenue type, and DSI combination. E.g. ltv_paid_gross_d7.","schema":{"type":"string","enum":["paid","incremental_blended","full_blended"],"default":"paid"}},{"name":"revenueTypes","in":"query","required":false,"description":"Comma-separated revenue types. Used for LTV and ROAS metrics.\n\nCreates a separate column for each selected metric, ROAS type, revenue type, and DSI combination. E.g. ltv_paid_gross_d7.","schema":{"type":"string","enum":["gross","net"],"default":"gross"}},{"name":"dsiValues","in":"query","required":false,"description":"Comma-separated days since install. Used for LTV and ROAS metrics.\n\nCreates a separate column for each selected metric, ROAS type, revenue type, and DSI combination. E.g. ltv_paid_gross_d7.","schema":{"type":"string","default":"7,365"}},{"name":"startDate","in":"query","required":false,"description":"Install date range start (YYYY-MM-DD). The earliest date supported is 455 days before the current date if available in database.\n\nCreates a new row per segment selected.","schema":{"type":"string"}},{"name":"endDate","in":"query","required":false,"description":"Install date range end (YYYY-MM-DD).","schema":{"type":"string"}},{"name":"countries","in":"query","required":false,"description":"Comma-separated country codes. Omit to include all. Only valid with campaign_geo granularity.\n\nReduces the number of rows generated - only shows data for the selected countries","schema":{"type":"string"}},{"name":"platforms","in":"query","required":false,"description":"Comma-separated platform filter. Omit to include all.\n\nReduces the number of rows generated - only shows data for the selected platforms.","schema":{"type":"string","enum":["ios","android","web"]}},{"name":"networks","in":"query","required":false,"description":"Comma-separated network filter. Omit to include all. Exact string match with network names on Ktrl.\n\nReduces the number of rows generated - only shows data for the selected networks.","schema":{"type":"string"}},{"name":"campaigns","in":"query","required":false,"description":"Comma-separated campaign filter. Omit to include all. Exact string match with campaign names on Ktrl.\n\nReduces the number of rows generated - only shows data for the selected campaigns.","schema":{"type":"string"}},{"name":"format","in":"query","required":false,"description":"Output file format. Parquet recommended for large exports.","schema":{"type":"string","enum":["parquet","csv"],"default":"parquet"}}],"responses":{"200":{"description":"Parquet or CSV file download.","content":{"application/octet-stream":{"schema":{"type":"string","format":"binary"}},"text/csv":{"schema":{"type":"string"}}}},"401":{"description":"Missing or invalid API key."},"403":{"description":"The API key cannot access the requested app."},"429":{"description":"Rate limit exceeded (60 requests/minute)."}}}},"/v1/roas-history":{"get":{"operationId":"getRoasHistory","summary":"Export ROAS History as a Parquet or CSV file: how the predicted ROAS of each install cohort changed as the cohort aged.","tags":["Ktrl API"],"parameters":[{"name":"appId","in":"query","required":true,"description":"App ID to export ROAS history for. Retrievable from /v1/apps endpoint.","schema":{"type":"integer"}},{"name":"granularity","in":"query","required":true,"description":"Dimension grouping level. campaign and campaign_geo only include campaigns and campaign–geos in the latest successful recommendation run.\n\nControls whether each cohort is one aggregated row, one row per campaign, or one row per campaign and country.","schema":{"type":"string","enum":["app","campaign","campaign_geo"]}},{"name":"dsi","in":"query","required":false,"description":"Days since install the ROAS is measured at. Must be one of the app's available DSIs.\n\nSets which DSI every roas_age_N column reports.","schema":{"type":"integer"}},{"name":"installGrouping","in":"query","required":false,"description":"Controls how install dates are grouped. Daily grouping is not available for apps with weekly prediction aggregation enabled.\n\nGroups install cohorts by day, week or month. Weekly / monthly reduces row count.","schema":{"type":"string","enum":["daily","weekly","monthly"],"default":"weekly"}},{"name":"roasType","in":"query","required":false,"description":"ROAS type, inclusive or exclusive of organic contribution.\n\nChanges the basis of every roas_age_N value.","schema":{"type":"string","enum":["paid","incremental_blended","full_blended"]}},{"name":"revenueType","in":"query","required":false,"description":"Revenue before fees (gross), or net of fees, taxes and refunds (net).\n\nChanges the basis of every roas_age_N value.","schema":{"type":"string","enum":["gross","net"]}},{"name":"startDate","in":"query","required":false,"description":"Cohort install date range start (YYYY-MM-DD). The earliest supported date is the start of the app's available data window, up to 455 days before the latest install date with data.\n\nCreates a new row per segment selected. The oldest cohort in range sets the last roas_age_N column.","schema":{"type":"string"}},{"name":"endDate","in":"query","required":false,"description":"Cohort install date range end (YYYY-MM-DD). Later dates are capped to the latest install date with data.\n\nReduces the number of rows generated.","schema":{"type":"string"}},{"name":"countries","in":"query","required":false,"description":"Comma-separated country codes. Omit to include all. Valid with app and campaign_geo granularity.\n\nAt app granularity, each row covers only the selected countries. At campaign_geo, only rows for the selected countries are returned.","schema":{"type":"string"}},{"name":"platforms","in":"query","required":false,"description":"Comma-separated platform filter. Omit to include all.\n\nReduces the number of rows generated - only shows data for the selected platforms.","schema":{"type":"string","enum":["ios","android","web"]}},{"name":"networks","in":"query","required":false,"description":"Comma-separated network filter. Omit to include all. Exact string match with network names on Ktrl.\n\nReduces the number of rows generated - only shows data for the selected networks.","schema":{"type":"string"}},{"name":"campaigns","in":"query","required":false,"description":"Comma-separated campaign filter. Omit to include all. Exact string match with campaign names on Ktrl.\n\nReduces the number of rows generated - only shows data for the selected campaigns.","schema":{"type":"string"}},{"name":"format","in":"query","required":false,"description":"Output file format. Parquet recommended for large exports.","schema":{"type":"string","enum":["parquet","csv"],"default":"parquet"}}],"responses":{"200":{"description":"Parquet or CSV file download, named {appName}-roas-history-{granularity}-{installGrouping}-d{dsi}-{roasType}-{revenueType}-{startDate}-{endDate}-{lastRunDate}. At campaign and campaign_geo granularity, rows are sorted by daily_spend (highest first), then recommendation_id (descending), then install_cohort; at app granularity, by install_cohort.","content":{"application/octet-stream":{"schema":{"type":"string","format":"binary"}},"text/csv":{"schema":{"type":"string"}}}},"401":{"description":"Missing or invalid API key."},"403":{"description":"The API key cannot access the requested app."},"429":{"description":"Rate limit exceeded (60 requests/minute)."}}}},"/v1/alerts":{"get":{"operationId":"getAlerts","summary":"Returns bid and analytics alerts for a live app, as shown in the Ktrl inbox. Defaults to the latest run; pass startDate for history.","tags":["Ktrl API"],"parameters":[{"name":"appId","in":"query","required":true,"description":"App ID to fetch alerts for. Retrievable from /v1/apps endpoint.","schema":{"type":"integer"}},{"name":"alertGroups","in":"query","required":false,"description":"Comma-separated alert groups. bids = live bid more than ±5% off the paced recommendation. analytics = performance, confidence and custom-rule alerts. Omit to include both.\n\nReduces results to the selected alert groups.","schema":{"type":"string","enum":["bids","analytics"]}},{"name":"countries","in":"query","required":false,"description":"Comma-separated country codes, or GLOBAL for campaign-level alerts (all analytics alerts, plus bid alerts not split by country). Omit to include all.\n\nReduces results to alerts whose country field is one of the values.","schema":{"type":"string"}},{"name":"platforms","in":"query","required":false,"description":"Comma-separated platform filter. Omit to include all.\n\nReduces results to the specified platforms.","schema":{"type":"string","enum":["ios","android","web"]}},{"name":"networks","in":"query","required":false,"description":"Comma-separated network filter. Omit to include all. Exact string match with network names on Ktrl.\n\nReduces results to the specified ad networks.","schema":{"type":"string"}},{"name":"campaigns","in":"query","required":false,"description":"Comma-separated campaign filter. Omit to include all. Exact string match with campaign names on Ktrl.\n\nReduces results to the specified campaigns.","schema":{"type":"string"}},{"name":"startDate","in":"query","required":false,"description":"Start of the alert date range (YYYY-MM-DD, UTC, inclusive). Omit both dates to get only the latest alert run.\n\nSwitches from the latest run to every alert created in the date range, newest run first.","schema":{"type":"string"}},{"name":"endDate","in":"query","required":false,"description":"End of the alert date range (YYYY-MM-DD, UTC, inclusive). Requires startDate.","schema":{"type":"string"}}],"responses":{"200":{"description":"JSON array of alert objects, newest run first and highest daily spend first within a run.","content":{"application/json":{"schema":{"type":"array","items":{"type":"object","properties":{"id":{"type":"integer","description":"Unique alert ID."},"appId":{"type":"integer","description":"App the alert belongs to. Matches /v1/apps."},"createdAt":{"type":"string","description":"ISO 8601 timestamp the alert was raised."},"alertGroup":{"type":"string","description":"BIDS (New bid available: the live bid is more than ±5% from the paced recommendation) or ANALYTICS (performance, confidence and custom-rule alerts)."},"title":{"type":"string","description":"Short name of the alert, as shown in Ktrl."},"campaign":{"type":"string","description":"Campaign the alert is about; Unknown if missing."},"country":{"type":"string","description":"ISO country code when the campaign runs in a single country; GLOBAL when it spans several. Same rule as /v1/recommendations."},"platform":{"type":"string","description":"IOS, ANDROID or WEB; Unknown if missing."},"network":{"type":"string","description":"Ad network the campaign runs on; Unknown if missing."},"dailySpend":{"type":"number","description":"Average daily spend of the affected campaign over the bet lookback range*, in app currency."},"summary":{"type":"string","description":"One line explaining what happened."},"detail":{"type":"string","description":"The fuller explanation of why the alert fired."},"spendDetail":{"type":"string","description":"How much spend is affected and why it matters. Analytics alerts only; null on bid alerts."},"recommendedAction":{"type":"string","description":"What Ktrl suggests doing next, in plain language; null when there is no action."}}}},"example":[{"id":88213,"appId":1,"createdAt":"2026-08-20T06:14:02.000Z","alertGroup":"ANALYTICS","title":"ROAS anomaly","campaign":"US_iOS_AEO","country":"GLOBAL","platform":"IOS","network":"APPLOVIN","dailySpend":1250,"summary":"2.6σ below 30-day mean • 40% vs 55%","detail":"Gross paid D365 ROAS fell 2.6σ below its 30-day mean on US_iOS_AEO.","spendDetail":"It moved to 40% from a 30-day mean of 55% on $1,250 of spend, clearing the 2.5σ anomaly bar in the negative direction.","recommendedAction":"Investigate the drop and consider reducing spend if the trend holds."},{"id":88214,"appId":1,"createdAt":"2026-08-20T06:14:01.000Z","alertGroup":"BIDS","title":"Bid above recommendation","campaign":"US_iOS_AEO","country":"US","platform":"IOS","network":"APPLOVIN","dailySpend":1235,"summary":"Live 36% → Paced 30% (+20% D7 ROAS)","detail":"Live bid is above the paced recommendation. Lowering the bid will unlock spend and scale the campaign.","spendDetail":null,"recommendedAction":"Lower bid (underscaling)"}]}}},"401":{"description":"Missing or invalid API key."},"403":{"description":"The API key cannot access the requested app."},"429":{"description":"Rate limit exceeded (60 requests/minute)."}}}}}}