# Ktrl REST API

Read-only HTTP API for Ktrl marketing analytics: your apps, spend recommendations, and cohort performance reports.

- Base URL: `https://api.kohort.io/api`
- Authentication: `Authorization: Bearer <your-api-key>` — create a key in the Ktrl platform under Settings → API Keys.
- Rate limit: 60 requests per minute per company.
- All endpoints are read-only; nothing in this API mutates your Ktrl data.

For agent and LLM access to the same data, see the MCP server at `https://api.kohort.io/api-docs/mcp.md`.

## GET /v1/apps

Returns list of all live apps with their names and IDs. App IDs are required when making reporting API requests.

### Example request

```bash
curl "https://api.kohort.io/api/v1/apps" \
  -H "Authorization: Bearer <your-api-key>"
```

### Response

JSON array of app objects.

```json
[
  {
    "id": 1,
    "name": "My App",
    "createdAt": "2025-01-15T10:30:00.000Z"
  }
]
```

## GET /v1/recommendations

Returns UA campaign recommendations for a live app based on the latest successful recommendation run.

### Query parameters

| Parameter | Type | Required | Description | Impact on output | Default | Options |
| --- | --- | --- | --- | --- | --- | --- |
| `appId` | Number | yes | App ID to fetch recommendations for. Retrievable from /v1/apps endpoint. | — | — | — |
| `granularity` | String | no | Dimension grouping level. Use campaign_geo to get one row per country. Countries filter is only valid with campaign_geo. | Controls 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). | `campaign` | `campaign`, `campaign_geo` |
| `countries` | String | no | Comma-separated country codes. Omit to include all. Only valid with campaign_geo granularity. | Reduces results to the specified countries. Throws a validation error if used with campaign granularity. | — | — |
| `platforms` | String | no | Comma-separated platform filter. Omit to include all. | Reduces results to the specified platforms. | — | `ios`, `android`, `web` |
| `networks` | String | no | Comma-separated network filter. Omit to include all. Exact string match with network names on Ktrl. | Reduces results to the specified ad networks. | — | — |

### Example request

```bash
curl "https://api.kohort.io/api/v1/recommendations?appId=1&granularity=campaign_geo" \
  -H "Authorization: Bearer <your-api-key>"
```

### Response

JSON array of recommendation objects.

```json
[
  {
    "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.0,
    "predictedPaidGrossRoas": 1.0,
    "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"
  }
]
```

### Response fields

| Field | Type | Description |
| --- | --- | --- |
| `id` | integer | Unique recommendation ID. |
| `country` | string | ISO country code. GLOBAL when a campaign row spans countries; Unknown if missing. |
| `platform` | string | IOS, ANDROID or WEB; Unknown if missing. |
| `network` | string | Ad network; Unknown if missing. |
| `campaign` | string | Campaign name; Unknown if missing. |
| `action` | string | 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` | string | Confidence tier: HIGH (≥ 0.60), MEDIUM (0.30 to 0.60), LOW (< 0.30, daily spend under 5, or non-traditional network). |
| `confidenceScore` | float | Model confidence, 0 to 1. |
| `dailySpend` | float | Average daily spend over the bet lookback range*, in app currency. |
| `paidInstalls` | integer | Paid installs over the bet lookback range*. |
| `incrementalBlendedInstalls` | integer | Paid plus incremental organic installs over the bet lookback range*. |
| `fullBlendedInstalls` | integer | Paid plus incremental organic installs plus a share of all "true" organic installs, over the bet lookback range*. |
| `paidCpi` | float | Cost per install over the bet lookback range*, paid installs only; 0 if no installs. |
| `incrementalBlendedCpi` | float | Cost per install across paid and incremental organic installs over the bet lookback range*; 0 if no installs. |
| `fullBlendedCpi` | float | Cost per install across paid and all allocated organic installs over the bet lookback range*; 0 if no installs. |
| `optimisationDsi` | integer | Conversion window (days since install) the ad network optimises against. |
| `targetDsi` | integer | Payback window (days since install) by which a cohort should hit its Target ROAS. |
| `optimisationLiveRoas` | float | Bid target currently live in the ad network. Always in Paid Gross. |
| `optimisationPacedRoas` | float | 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` | float | Recommended, unconstrained bid target to move performance toward your Target ROAS. Always in Paid Gross. |
| `optimisationPredictedRoas` | float | Predicted ROAS at the Optimisation DSI for cohorts in the bet lookback range*. Always in Paid Gross. |
| `targetPaidGrossRoas` | float | Target ROAS from paid-install revenue, before fees. |
| `predictedPaidGrossRoas` | float | Predicted ROAS at the Target DSI for cohorts in the bet lookback range* from paid-install revenue, before fees. |
| `targetPaidNetRoas` | float | Target ROAS from paid-install revenue, net of fees, taxes, and refunds. |
| `predictedPaidNetRoas` | float | Predicted ROAS at the Target DSI for cohorts in the bet lookback range* from paid-install revenue, net of fees, taxes, and refunds. |
| `targetIncrementalBlendedGrossRoas` | float | Target ROAS from paid plus incremental organic revenue, before fees. |
| `predictedIncrementalBlendedGrossRoas` | float | Predicted ROAS at the Target DSI for cohorts in the bet lookback range* from paid plus incremental organic revenue, before fees. |
| `targetIncrementalBlendedNetRoas` | float | Target ROAS from paid plus incremental organic revenue, net of fees, taxes, and refunds. |
| `predictedIncrementalBlendedNetRoas` | float | 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` | float | Target ROAS from paid plus all organic revenue, before fees. |
| `predictedFullBlendedGrossRoas` | float | Predicted ROAS at the Target DSI for cohorts in the bet lookback range* from paid plus all organic revenue, before fees. |
| `targetFullBlendedNetRoas` | float | Target ROAS from paid plus all organic revenue, net of fees, taxes, and refunds. |
| `predictedFullBlendedNetRoas` | float | 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` | string | ISO 8601 timestamp the recommendation was produced. |

* Bet lookback range: representative historical cohorts installed X → Y days ago (0 ≤ X < Y ≤ 30), used to generate recommendations. The range can vary by platform and can be found in the Ktrl Platform under the App Settings.

## GET /v1/report

Export actual and forecasted metrics data into a single report as a Parquet or CSV file.

### Query parameters

| Parameter | Type | Required | Description | Impact on output | Default | Options |
| --- | --- | --- | --- | --- | --- | --- |
| `appId` | Number | yes | App ID to export data for. Retrievable from /v1/apps endpoint. | — | — | — |
| `granularity` | String | yes | Dimension grouping level. | Controls whether data is grouped at the campaign level or at the campaign–geo level. | — | `campaign`, `campaign_geo` |
| `installGrouping` | String | no | 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. | Groups 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. | — | `daily`, `weekly`, `monthly` |
| `metrics` | String | no | Comma-separated metrics. | Creates new columns for each metric selected. CPI and LTV are empty when installs are 0, and ROAS is empty when spend is 0. | `ltv` | `ltv`, `roas`, `spend`, `installs`, `cpi` |
| `roasTypes` | String | no | Comma-separated ROAS types. Used for LTV, ROAS, Installs, and CPI metrics. | Creates a separate column for each selected metric, ROAS type, revenue type, and DSI combination. E.g. ltv_paid_gross_d7. | `paid` | `paid`, `incremental_blended`, `full_blended` |
| `revenueTypes` | String | no | Comma-separated revenue types. Used for LTV and ROAS metrics. | Creates a separate column for each selected metric, ROAS type, revenue type, and DSI combination. E.g. ltv_paid_gross_d7. | `gross` | `gross`, `net` |
| `dsiValues` | String | no | Comma-separated days since install. Used for LTV and ROAS metrics. | Creates a separate column for each selected metric, ROAS type, revenue type, and DSI combination. E.g. ltv_paid_gross_d7. | `7,365` | — |
| `startDate` | String | no | Install date range start (YYYY-MM-DD). The earliest date supported is 455 days before the current date if available in database. | Creates a new row per segment selected. | `30 days before endDate` | — |
| `endDate` | String | no | Install date range end (YYYY-MM-DD). | — | `Latest install date with data` | — |
| `countries` | String | no | Comma-separated country codes. Omit to include all. Only valid with campaign_geo granularity. | Reduces the number of rows generated - only shows data for the selected countries | — | — |
| `platforms` | String | no | Comma-separated platform filter. Omit to include all. | Reduces the number of rows generated - only shows data for the selected platforms. | — | `ios`, `android`, `web` |
| `networks` | String | no | Comma-separated network filter. Omit to include all. Exact string match with network names on Ktrl. | Reduces the number of rows generated - only shows data for the selected networks. | — | — |
| `campaigns` | String | no | Comma-separated campaign filter. Omit to include all. Exact string match with campaign names on Ktrl. | Reduces the number of rows generated - only shows data for the selected campaigns. | — | — |
| `format` | String | no | Output file format. Parquet recommended for large exports. | — | `parquet` | `parquet`, `csv` |

### Example request

```bash
curl "https://api.kohort.io/api/v1/report?appId=1&granularity=campaign_geo&metrics=ltv,spend" \
  -H "Authorization: Bearer <your-api-key>" \
  --output report.parquet
```

### Response

Parquet or CSV file download.

## GET /v1/roas-history

Export ROAS History as a Parquet or CSV file: how the predicted ROAS of each install cohort changed as the cohort aged.

### Query parameters

| Parameter | Type | Required | Description | Impact on output | Default | Options |
| --- | --- | --- | --- | --- | --- | --- |
| `appId` | Number | yes | App ID to export ROAS history for. Retrievable from /v1/apps endpoint. | — | — | — |
| `granularity` | String | yes | Dimension grouping level. campaign and campaign_geo only include campaigns and campaign–geos in the latest successful recommendation run. | Controls whether each cohort is one aggregated row, one row per campaign, or one row per campaign and country. | — | `app`, `campaign`, `campaign_geo` |
| `dsi` | Number | no | Days since install the ROAS is measured at. Must be one of the app's available DSIs. | Sets which DSI every roas_age_N column reports. | `App-wide target DSI` | — |
| `installGrouping` | String | no | Controls how install dates are grouped. Daily grouping is not available for apps with weekly prediction aggregation enabled. | Groups install cohorts by day, week or month. Weekly / monthly reduces row count. | `weekly` | `daily`, `weekly`, `monthly` |
| `roasType` | String | no | ROAS type, inclusive or exclusive of organic contribution. | Changes the basis of every roas_age_N value. | `App-wide target ROAS type` | `paid`, `incremental_blended`, `full_blended` |
| `revenueType` | String | no | Revenue before fees (gross), or net of fees, taxes and refunds (net). | Changes the basis of every roas_age_N value. | `App-wide target revenue type` | `gross`, `net` |
| `startDate` | String | no | 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. | Creates a new row per segment selected. The oldest cohort in range sets the last roas_age_N column. | `90 days before endDate` | — |
| `endDate` | String | no | Cohort install date range end (YYYY-MM-DD). Later dates are capped to the latest install date with data. | Reduces the number of rows generated. | `Latest install date with data` | — |
| `countries` | String | no | Comma-separated country codes. Omit to include all. Valid with app and campaign_geo granularity. | At app granularity, each row covers only the selected countries. At campaign_geo, only rows for the selected countries are returned. | — | — |
| `platforms` | String | no | Comma-separated platform filter. Omit to include all. | Reduces the number of rows generated - only shows data for the selected platforms. | — | `ios`, `android`, `web` |
| `networks` | String | no | Comma-separated network filter. Omit to include all. Exact string match with network names on Ktrl. | Reduces the number of rows generated - only shows data for the selected networks. | — | — |
| `campaigns` | String | no | Comma-separated campaign filter. Omit to include all. Exact string match with campaign names on Ktrl. | Reduces the number of rows generated - only shows data for the selected campaigns. | — | — |
| `format` | String | no | Output file format. Parquet recommended for large exports. | — | `parquet` | `parquet`, `csv` |

### Example request

```bash
curl "https://api.kohort.io/api/v1/roas-history?appId=1&granularity=campaign_geo&dsi=90" \
  -H "Authorization: Bearer <your-api-key>" \
  --output roas-history.parquet
```

### Response

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.

### Response fields

| Field | Type | Description |
| --- | --- | --- |
| `created_at` | timestamp | When the ROAS history snapshot was captured. |
| `recommendation_id` | integer | Recommendation ID in the latest successful run. Matches /v1/recommendations. Not returned at app granularity. |
| `platform` | string | IOS, ANDROID or WEB; Unknown if missing. Not returned at app granularity. |
| `network` | string | Ad network; Unknown if missing. Not returned at app granularity. |
| `campaign` | string | Campaign name; Unknown if missing. Not returned at app granularity. |
| `country` | string | ISO country code; Unknown if missing. campaign_geo granularity only. |
| `daily_spend` | float | Average daily spend of the recommended campaign over its bet lookback range, in app currency, as on the recommendation. The same on every install_cohort row of a campaign. Not returned at app granularity. |
| `install_cohort` | date | Cohort install date, truncated to the start of the installGrouping period. |
| `dsi` | integer | Days since install the ROAS is measured at. |
| `roas_type` | string | paid, incremental_blended or full_blended. |
| `revenue_type` | string | gross or net. |
| `roas_age_N` | float | ROAS at the requested dsi when the cohort was N days old, to 4 dp. One column per cohort age; empty where no data was captured at that age. |

Cohort age (N) is the whole days between the start of the cohort's install period and the max cohort install date of the run that produced the value. The first age is the app's bet lookback start (default 2 days), which can vary by platform.

## GET /v1/alerts

Returns bid and analytics alerts for a live app, as shown in the Ktrl inbox. Defaults to the latest run; pass startDate for history.

### Query parameters

| Parameter | Type | Required | Description | Impact on output | Default | Options |
| --- | --- | --- | --- | --- | --- | --- |
| `appId` | Number | yes | App ID to fetch alerts for. Retrievable from /v1/apps endpoint. | — | — | — |
| `alertGroups` | String | no | 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. | Reduces results to the selected alert groups. | — | `bids`, `analytics` |
| `countries` | String | no | Comma-separated country codes, or GLOBAL for campaign-level alerts (all analytics alerts, plus bid alerts not split by country). Omit to include all. | Reduces results to alerts whose country field is one of the values. | — | — |
| `platforms` | String | no | Comma-separated platform filter. Omit to include all. | Reduces results to the specified platforms. | — | `ios`, `android`, `web` |
| `networks` | String | no | Comma-separated network filter. Omit to include all. Exact string match with network names on Ktrl. | Reduces results to the specified ad networks. | — | — |
| `campaigns` | String | no | Comma-separated campaign filter. Omit to include all. Exact string match with campaign names on Ktrl. | Reduces results to the specified campaigns. | — | — |
| `startDate` | String | no | Start of the alert date range (YYYY-MM-DD, UTC, inclusive). Omit both dates to get only the latest alert run. | Switches from the latest run to every alert created in the date range, newest run first. | `latest run` | — |
| `endDate` | String | no | End of the alert date range (YYYY-MM-DD, UTC, inclusive). Requires startDate. | — | `today` | — |

### Example request

```bash
curl "https://api.kohort.io/api/v1/alerts?appId=1&alertGroups=bids&countries=US" \
  -H "Authorization: Bearer <your-api-key>"
```

### Response

JSON array of alert objects, newest run first and highest daily spend first within a run.

```json
[
  {
    "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.0,
    "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.0,
    "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)"
  }
]
```

### Response fields

| Field | Type | Description |
| --- | --- | --- |
| `id` | integer | Unique alert ID. |
| `appId` | integer | App the alert belongs to. Matches /v1/apps. |
| `createdAt` | string | ISO 8601 timestamp the alert was raised. |
| `alertGroup` | string | BIDS (New bid available: the live bid is more than ±5% from the paced recommendation) or ANALYTICS (performance, confidence and custom-rule alerts). |
| `title` | string | Short name of the alert, as shown in Ktrl. |
| `campaign` | string | Campaign the alert is about; Unknown if missing. |
| `country` | string | ISO country code when the campaign runs in a single country; GLOBAL when it spans several. Same rule as /v1/recommendations. |
| `platform` | string | IOS, ANDROID or WEB; Unknown if missing. |
| `network` | string | Ad network the campaign runs on; Unknown if missing. |
| `dailySpend` | float | Average daily spend of the affected campaign over the bet lookback range*, in app currency. |
| `summary` | string | One line explaining what happened. |
| `detail` | string | The fuller explanation of why the alert fired. |
| `spendDetail` | string | How much spend is affected and why it matters. Analytics alerts only; null on bid alerts. |
| `recommendedAction` | string | What Ktrl suggests doing next, in plain language; null when there is no action. |

* Bet lookback range: representative historical cohorts installed X → Y days ago (0 ≤ X < Y ≤ 30), used to generate recommendations. The range can vary by platform and can be found in the Ktrl Platform under the App Settings.

