Run query
POST /teams/{teamId}/data/run-query
Runs a data query across one or more accounts and returns the aggregated rows. You specify the accounts to pull from, the dimensions to group by, the metrics to aggregate, a date range, and optional filters and sort. Accounts from different connectors can be combined in a single query.
data:read · Team permission:data.query · Rate limit: 15 / minute and 500 / hourThe key holder's role must include Query data over MCP and the API. Every standard role except Viewer includes it. The key holder must also have a seat in the team.
504, and the response body is not JSON. Keep each query small: for many accounts or a long date range, split the query by account or by date range. Because queries are expensive, the route has a tight rate limit of 15 requests per minute and 500 per hour (lower than the default 120 / 60s). Set a generous client timeout, avoid sending queries in a tight loop, and don't retry a slow query until the previous one has returned.Path parameters
Field IDs
There are two types of field ID:
- Common fields work across connectors and have no prefix, for example
date,campaign_name,impressions,clicks,conversionsandamount_spent. For spend, useamount_spent. It maps to each ad connector's own spend field. There is no commoncostfield. Ad sets and ad groups use the same common fields:ad_group_idandad_group_name. - Connector fields belong to one connector and start with its ID, for example
ga4_sessions. GA4 sessions and new users are connector fields:ga4_sessions,ga4_new_users.
List connector fields returns connector field IDs without the prefix. Add <connectorId>_ before you use them here. For example, adset_id from fads becomes fads_adset_id.
The API checks every field ID in dimensions, metrics, filters and sort before the query runs. An unknown ID gives a 400 error that lists each unknown ID. If you send a connector field without its prefix, the error message shows the correct ID.
To find field IDs, use the field explorer.
Request body
gadw_1234567890 for a Google Ads account). Accounts from different connectors can be mixed in one query. An ID that does not start with a known connector prefix is rejected.last_7_days when omitted.Presets:today, yesterday, last_7_days, last_30_days, this_month, last_month, last_3_months, last_6_months, last_year, this_year.Custom object:{ "startDate": "YYYY-MM-DD", "endDate": "YYYY-MM-DD" }. Both dates are required and must be in YYYY-MM-DD format.{ "fieldId": string, "direction": "asc" | "desc" }.{ "fieldId": string, "operator": string, "value": string }. operator is one of equals, notEquals, contains, notContains, greaterThan, lesserThan. value is always a string.400 error.blankValuesToZero(boolean): returns every requested field on every row. A blank metric becomes0and a blank dimension becomesnull. Defaultfalse: a blank field is not in the row.includeDataWithoutInsights(boolean): Facebook Ads and GA4 only. Defaultfalse.- Facebook Ads: also returns campaigns, ad sets and ads that have no data in the date range. This can return many rows.
- GA4: also returns rows where every metric is zero.
currency(string): a 3-letter ISO 4217 code, for exampleUSD. Converts currency metrics to this currency on connectors that support conversion. Default: the currency of each account.
accounts. An unknown key or an unknown attribution window gives a 400 error that lists the valid values.fads(Facebook Ads):conversionWindow(string): attribution windows, for example["7c", "1v"]. Values:default,incrementality,1d_ev,1c,1v,7c,28c,1c1v,7c1v,28c1v. Default["default"]: the attribution setting of the ad account.reportTimeOfStats(string):impressionorconversion. Sets the date that a conversion is reported on. Default: the Meta API default.useUnifiedAttributionSetting(boolean): uses the unified attribution setting of the ad account. Defaultfalse.useAccountAttributionSetting(boolean): uses the default attribution setting of the ad account. Defaultfalse.includeAllItems(boolean): does not filter by status. Includes active, paused, archived and deleted campaigns, ad sets and ads. Defaultfalse: the API sends no status filter, and Meta applies its default.
pinads(Pinterest Ads):conversionWindow(string, one window). Each ID gives the click, engagement and view days:c30_e7_v1is 30-day click, 7-day engagement and 1-day view. Values:default,c60_e60_v60,c60_e60_v30,c60_e60_v7,c60_e60_v1,c60_e30_v30,c60_e30_v7,c60_e30_v1,c60_e7_v7,c60_e7_v1,c60_e1_v1,c30_e30_v30,c30_e30_v7,c30_e30_v1,c30_e7_v7,c30_e7_v1,c30_e1_v1,c7_e7_v7,c7_e7_v1,c7_e1_v1,c1_e1_v1. Defaultdefault: 30-day click, 30-day engagement and 1-day view.reportTimeOfStats(string):defaultortime_of_conversion.
snapads(Snapchat Ads):conversionWindow(string, one window). Each ID gives the view and swipe windows:1dv_28dsis 1-day view and 28-day swipe. Values:1hv_1ds,1hv_7ds,1hv_28ds,3hv_1ds,3hv_7ds,3hv_28ds,6hv_1ds,6hv_7ds,6hv_28ds,1dv_1ds,1dv_7ds,1dv_28ds,7dv_7ds,7dv_28ds,28dv_28ds. Default1dv_28ds.chatgptads(ChatGPT Ads):conversionWindow(string). Values:30c1v,14c1v,7c1v,30c,14c,7c. Default["30c1v"]: 30-day click and 1-day view.klaviyo(Klaviyo):isKlaviyoAttributedValuesOnly(boolean). Returns only values that Klaviyo campaigns and flows caused.
accounts: use your enabled account IDs from List all accounts or List connector accounts with?status=enabled. Each account ID starts with its connector ID. See the full list of connector IDs in List connectors.dimensions,metrics, and thefieldIdin filters and sort: find every available field, and the common fields, in the field explorer.
Request examples
Two accounts, grouped by campaign, with spend and clicks over the last 30 days.
{
"accounts": ["gadw_1234567890", "fads_act_9876543210"],
"dimensions": ["campaign_name"],
"metrics": ["amount_spent", "clicks"],
"dateRange": "last_30_days"
}
curl -X POST https://api.twominutereports.com/v1/teams/0190f8b0-1a2b-7c3d-8e4f-5a6b7c8d9e0f/data/run-query \
-H "Authorization: Bearer tmrc_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" \
-H "Content-Type: application/json" \
-d '{
"accounts": ["gadw_1234567890", "fads_act_9876543210"],
"dimensions": ["campaign_name"],
"metrics": ["amount_spent", "clicks"],
"dateRange": "last_30_days"
}'
A single account over an explicit startDate–endDate window.
{
"accounts": ["gadw_1234567890"],
"dimensions": ["date", "campaign_name"],
"metrics": ["impressions", "amount_spent"],
"dateRange": { "startDate": "2026-05-01", "endDate": "2026-05-31" }
}
curl -X POST https://api.twominutereports.com/v1/teams/0190f8b0-1a2b-7c3d-8e4f-5a6b7c8d9e0f/data/run-query \
-H "Authorization: Bearer tmrc_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" \
-H "Content-Type: application/json" \
-d '{
"accounts": ["gadw_1234567890"],
"dimensions": ["date", "campaign_name"],
"metrics": ["impressions", "amount_spent"],
"dateRange": { "startDate": "2026-05-01", "endDate": "2026-05-31" }
}'
Group by campaign, keep only campaigns whose name contains Brand, and sort by amount spent, highest first.
{
"accounts": ["gadw_1234567890", "fads_act_9876543210"],
"dimensions": ["campaign_name"],
"metrics": ["amount_spent", "clicks", "conversions"],
"dateRange": "last_3_months",
"filters": [
{ "fieldId": "campaign_name", "operator": "contains", "value": "Brand" },
{ "fieldId": "amount_spent", "operator": "greaterThan", "value": "100" }
],
"sort": [
{ "fieldId": "amount_spent", "direction": "desc" }
]
}
curl -X POST https://api.twominutereports.com/v1/teams/0190f8b0-1a2b-7c3d-8e4f-5a6b7c8d9e0f/data/run-query \
-H "Authorization: Bearer tmrc_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" \
-H "Content-Type: application/json" \
-d '{
"accounts": ["gadw_1234567890", "fads_act_9876543210"],
"dimensions": ["campaign_name"],
"metrics": ["amount_spent", "clicks", "conversions"],
"dateRange": "last_3_months",
"filters": [
{ "fieldId": "campaign_name", "operator": "contains", "value": "Brand" },
{ "fieldId": "amount_spent", "operator": "greaterThan", "value": "100" }
],
"sort": [
{ "fieldId": "amount_spent", "direction": "desc" }
]
}'
A Facebook Ads account with a 7-day click and 1-day view attribution window, conversions reported on the conversion date, all statuses, and blank metrics as 0.
{
"accounts": ["fads_act_9876543210"],
"dimensions": ["date", "ad_group_name"],
"metrics": ["amount_spent", "conversions"],
"dateRange": "last_30_days",
"dataOptions": { "blankValuesToZero": true, "currency": "USD" },
"connectorReportOptions": {
"fads": {
"conversionWindow": ["7c1v"],
"reportTimeOfStats": "conversion",
"includeAllItems": true
}
}
}
curl -X POST https://api.twominutereports.com/v1/teams/0190f8b0-1a2b-7c3d-8e4f-5a6b7c8d9e0f/data/run-query \
-H "Authorization: Bearer tmrc_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" \
-H "Content-Type: application/json" \
-d '{
"accounts": ["fads_act_9876543210"],
"dimensions": ["date", "ad_group_name"],
"metrics": ["amount_spent", "conversions"],
"dateRange": "last_30_days",
"dataOptions": { "blankValuesToZero": true, "currency": "USD" },
"connectorReportOptions": {
"fads": {
"conversionWindow": ["7c1v"],
"reportTimeOfStats": "conversion",
"includeAllItems": true
}
}
}'
Response
On success, data is an array of row objects. Each key is a requested field ID and each value is the cell value (a string or number). The set of keys matches the dimensions and metrics you requested. A field with a blank value is not in the row. To get every field on every row, set dataOptions.blankValuesToZero to true.
All rows are in one response. There is no pagination and no row limit. The limit in practice is time: a query that runs too long stops (see the warning at the top of this page). For many accounts or a long date range, split the query by account or by date range.
account_id contains the account ID without the connector prefix, for example 9876543210. To identify the connector of each row, also request data_source. It contains the connector name, for example Facebook Ads.
Rows for the "Filters and sort" example above.
{
"success": true,
"data": [
{ "campaign_name": "Brand - Search", "amount_spent": 1820.45, "clicks": 5421, "conversions": 132 },
{ "campaign_name": "Brand - Display", "amount_spent": 640.10, "clicks": 2210, "conversions": 38 },
{ "campaign_name": "Brand - Retargeting", "amount_spent": 305.77, "clicks": 980, "conversions": 21 }
]
}
Returned when the request body has an incorrect shape. For example: an account ID does not start with a connector ID, accounts is empty or has more than 100 entries, or dataOptions or connectorReportOptions has an unknown key.
{
"success": false,
"error": {
"code": "BAD_REQUEST",
"message": "Validation error: Account ID must start with one of the following prefixes: appstore, cfs, fads, ga, gadw, ... at \"accounts[0]\""
}
}
Returned before the query runs when a field ID or an option is not valid, or when a sort field is not one of the requested dimensions or metrics. The message lists each problem. Here, spend is a connector field sent without its prefix.
{
"success": false,
"error": {
"code": "VALIDATION_FAILED",
"message": "Unknown field \"spend\": unprefixed ids are common fields only. Did you mean \"fads_spend\"? Valid field ids are listed at https://twominutereports.com/help/api/field-explorer."
}
}
The key holder's role does not include Query data over MCP and the API.
{
"success": false,
"error": { "code": "INSUFFICIENT_ROLE", "message": "[email protected] is not allowed to use data.query. This needs a role that includes it; their role is viewer." }
}
Returned when the query ran and failed. Use the status to decide if you can send the query again. Use the code to find the cause.
| Status | code | Cause | What to do |
|---|---|---|---|
400 | AUTH_TOKEN_EXPIRED, ACCOUNT_ACCESS_REVOKED, PROVIDER_PERMISSION_MISSING, CONNECTION_CREDENTIALS_MISSING | The login of the connection expired, or it lost access to the account. Your API key is not the problem. | Reconnect the connection in Two Minute Reports, then send the query again. |
400 | VALIDATION_FAILED or a query code | The query cannot run as written. | Change the query. Do not send it again unchanged. |
402 | PLAN_SUBSCRIPTION_INACTIVE or another plan code | The plan of the team does not allow the query. | Reactivate or change the plan. |
404 | QUERY_TARGET_NOT_FOUND | None of the accounts in accounts is enabled for the team. | Enable the accounts, or correct the account IDs. |
429 | PROVIDER_RATE_LIMITED | The ad platform or data source limited the request. | Wait, then send the query again. |
500 | The code of the cause | A failure on our side. For a job failure, such as JOB_QUEUE_WAIT_EXCEEDED, the message tells you what to do. For an internal error, the message is generic. | Send the query again later. If it continues, contact support with the code. |
503 | The code of the cause | A temporary problem. | Wait, then send the query again. |
The message gives the reason when there is one. Otherwise it is Query failed, or An unexpected error occurred for an internal error.
{
"success": false,
"error": { "code": "AUTH_TOKEN_EXPIRED", "message": "Your connection has expired or been revoked! Please go to the 'Connections' section, edit the corresponding Connection, Re-Authenticate and save it." }
}
Returned when the query does not finish within the time limit of the server. At this time, most requests stop earlier with HTTP 504 (see the warning at the top of this page).
{
"success": false,
"error": { "code": "UPSTREAM_JOB_TIMEOUT", "message": "Data query timed out after 600s." }
}