GA4 custom dimensions require `customEvent:parameter_name` format in the Data API, not what most guides show. Here's the exact format, an Apps Script example, and how to avoid cardinality corruption.
To query GA4 custom dimensions through the Data API (and therefore in Google Sheets), you must use the format customEvent:your_parameter_name, not the dimension display name you set in the GA4 interface. This format is missing from most integration guides, blog posts, and even parts of Google's own documentation. It is the single most common reason GA4 custom dimension queries return empty results or errors.
The Problem Nobody Warns You About
GA4's custom dimensions are one of its most powerful features. You send a custom event parameter (say, article_category or user_plan_type) and GA4 registers it as a custom dimension you can report on. In the GA4 UI, you give it a display name like "Article Category" and it appears in Explorations and standard reports cleanly.
Then you try to pull it into Google Sheets via the Data API.
"I've been trying to query my custom dimension 'Article Category' through the GA4 API for two days. I've tried the display name, the parameter name, snake_case, camelCase: nothing works. The API just returns an empty dimension or an INVALID_ARGUMENT error." (StackOverflow, 2023)
"Our developer spent a week trying to get custom dimensions out of GA4 into our Sheets dashboard. We eventually gave up and just excluded them. None of the documentation was clear on this." (GA4 Community forum)
The correct API field name is customEvent:article_category. The prefix customEvent: followed by the exact parameter name you pass in your gtag or GTM event. Not the display name. Not the dimension slug. The raw parameter name, with the prefix attached.
This is documented in exactly one place in Google's documentation. The Dimensions and Metrics Explorer, and is absent from the primary Data API reference pages that most developers reach first.
What the GA4 Data API Actually Requires
GA4 has two types of custom dimension registrations: Event-scoped and User-scoped. Each has a different API field name format.
Event-Scoped Custom Dimensions
These are parameters sent with individual events. In the GA4 interface you register them under Admin > Custom Definitions > Custom Dimensions, selecting "Event" as the scope.
API field name format: customEvent:parameter_name
Example: If you track gtag('event', 'page_view', { article_category: 'Technology' }), the API field name is customEvent:article_category.
User-Scoped Custom Dimensions
These are parameters associated with the user across all their events, typically sent via gtag('set', 'user_properties', { plan_type: 'pro' }).
API field name format: customUser:parameter_name
Example: customUser:plan_type
Item-Scoped Custom Dimensions (Ecommerce)
These apply to individual items within ecommerce events.
API field name format: customItem:parameter_name
Example: customItem:product_color
The display name you set in GA4 Admin is irrelevant to the API. The API requires the raw parameter name with the appropriate scope prefix. Always.
Custom Dimension Scopes and Their API Field Names
Here is the full mapping for reference:
| Scope | Example Parameter | GA4 UI Display Name | Data API Field Name |
|---|---|---|---|
| Event | article_category | Article Category | customEvent:article_category |
| User | plan_type | Plan Type | customUser:plan_type |
| Item (ecommerce) | product_color | Product Color | customItem:product_color |
| Session | session_source | Session Source | customSession:session_source (limited availability) |
The session scope is newer and has limited support in some API versions. Stick to event and user scopes for the most reliable results.
One additional note: the parameter name is case-sensitive. customEvent:Article_Category and customEvent:article_category are different fields. GA4 normalizes parameters to lowercase in most implementations, but if you are sending mixed-case parameter names, you must match the case exactly in your API request.
Custom Metrics: The Same Problem, Same Fix
Everything above applies equally to custom metrics. Custom metrics are numeric parameters you send with events (e.g., read_time_seconds, cart_value_usd) and register in GA4 Admin as custom metrics.
API field name format for custom metrics: customEvent:parameter_name
Yes. The same customEvent: prefix, even though you registered it as a metric. The GA4 Data API uses the same prefix convention for both custom dimensions and custom metrics at the event scope.
Example request including a custom metric:
{
"metrics": [
{ "name": "sessions" },
{ "name": "customEvent:read_time_seconds" }
],
"dimensions": [
{ "name": "customEvent:article_category" }
]
}One distinction: custom metrics support expression-based calculations in the API (expression field in the metric object), which custom dimensions do not. If you want to compute an average of a custom metric across sessions, you can use customEvent:read_time_seconds / sessions as a metric expression in some API versions.
Working Apps Script Example
This script queries the GA4 Data API for sessions broken down by a custom event dimension (article_category) and writes results to the active Google Sheet.
function fetchGA4CustomDimensions() {
// Store these in Script Properties for security
const PROPERTY_ID = PropertiesService.getScriptProperties().getProperty('GA4_PROPERTY_ID');
const ACCESS_TOKEN = ScriptApp.getOAuthToken();
const requestBody = {
dateRanges: [
{ startDate: '30daysAgo', endDate: 'today' }
],
dimensions: [
{ name: 'customEvent:article_category' }, // <-- note the prefix
{ name: 'date' }
],
metrics: [
{ name: 'sessions' },
{ name: 'activeUsers' },
{ name: 'customEvent:read_time_seconds' } // custom metric, same prefix
],
orderBys: [
{ dimension: { dimensionName: 'date' }, desc: true }
],
limit: 10000,
keepEmptyRows: false
};
const url = `https://analyticsdata.googleapis.com/v1beta/properties/${PROPERTY_ID}:runReport`;
const options = {
method: 'post',
contentType: 'application/json',
headers: {
Authorization: 'Bearer ' + ACCESS_TOKEN
},
payload: JSON.stringify(requestBody),
muteHttpExceptions: true
};
const response = UrlFetchApp.fetch(url, options);
const result = JSON.parse(response.getContentText());
if (result.error) {
Logger.log('API Error: ' + JSON.stringify(result.error));
return;
}
// Check for sampling
if (result.metadata && result.metadata.samplingSpaceSizes) {
Logger.log('Warning: Response is sampled. Consider reducing date range.');
}
// Write headers
const sheet = SpreadsheetApp.getActiveSheet();
sheet.clearContents();
const headers = ['Article Category', 'Date', 'Sessions', 'Active Users', 'Avg Read Time (s)'];
sheet.getRange(1, 1, 1, headers.length).setValues([headers]);
if (!result.rows || result.rows.length === 0) {
Logger.log('No rows returned. Check that the custom dimension is registered in GA4 Admin and has received data.');
return;
}
// Write data rows
const rows = result.rows.map(row => [
row.dimensionValues[0].value, // article_category
row.dimensionValues[1].value, // date
parseInt(row.metricValues[0].value), // sessions
parseInt(row.metricValues[1].value), // activeUsers
parseFloat(row.metricValues[2].value) // read_time_seconds
]);
sheet.getRange(2, 1, rows.length, headers.length).setValues(rows);
Logger.log(`Written ${rows.length} rows to sheet.`);
}Setup notes:
- Enable the Google Analytics Data API in your Apps Script project (Resources > Advanced Google Services or the Services panel)
- The script uses
ScriptApp.getOAuthToken()which automatically handles OAuth, no separate token management required - Set
GA4_PROPERTY_IDin Script Properties (not the measurement ID. The numeric property ID from GA4 Admin) - The
limit: 10000parameter caps rows per request; for large datasets paginate using theoffsetparameter
Pagination for large result sets:
function fetchAllRows(url, options, requestBody) {
let allRows = [];
let offset = 0;
const limit = 10000;
do {
requestBody.limit = limit;
requestBody.offset = offset;
options.payload = JSON.stringify(requestBody);
const response = UrlFetchApp.fetch(url, options);
const result = JSON.parse(response.getContentText());
if (!result.rows) break;
allRows = allRows.concat(result.rows);
if (result.rows.length < limit) break;
offset += limit;
} while (true);
return allRows;
}Cardinality Limits That Silently Corrupt Data
GA4 standard properties cap each dimension at 500 unique values in reporting. When a custom dimension exceeds this limit, additional values are bucketed into (other) in the UI and API. GA4 360 raises this limit to 150,000 unique values.
The critical issue is that this corruption is silent. Your API response returns data with an (other) row. If your query totals include that row, your aggregates appear correct. But if you are joining this data to another table using the dimension value as a key (a common pattern in Sheets dashboards) the (other) rows either fail to join or introduce significant error.
Common high-cardinality traps:
page_pathwith query parameters includedarticle_id(numeric or UUID-based identifiers)session_idpassed as a custom dimension (never do this)user_idpassed as an event-scoped custom dimension instead of the User-ID featuresearch_queryfrom internal site searchproduct_skuwithout grouping
How to check your cardinality:
In GA4 > Explore > Free Form, add your custom dimension. If the last row in the table says (other), you have a cardinality issue. The percentage of events in (other) tells you how badly the data is corrupted for that dimension.
How to fix it:
- Reduce cardinality at the source. Send category-level values instead of ID-level values. Send
article_categoryinstead ofarticle_id. Sendprice_tierinstead of exact price. - Use BigQuery for high-cardinality dimensions. GA4's BigQuery export does not have the 500-value cardinality limit. If you need exact product SKUs or user-level breakdowns, BigQuery is the correct storage layer, not GA4 reporting.
- Clean URL parameters before tracking. Use GA4's URL parameter exclusion list or clean URLs at the GTM level before they are sent as
page_location.
The 48-Hour API Delay for Custom Events
GA4 processes standard events (session_start, page_view, first_visit) within a few hours of collection. Custom events (and therefore custom dimensions derived from them) are subject to a processing delay that can reach 48 hours.
This delay affects:
- Yesterday's data for any custom event dimension
- The "today" date range in API queries
- Any dashboard showing near-real-time data for custom events
Practical implications:
If your Sheets dashboard shows today's or yesterday's numbers for a custom dimension and they look artificially low, the data is not yet fully processed. This is not a bug in your API call. It is a GA4 processing latency that disproportionately affects custom events.
How to handle it:
- Exclude today and yesterday from custom dimension queries when freshness matters
- Use a 2-day lag (
endDate: '2daysAgo') for custom dimension reports - Add a visible note in your Sheets dashboard: "Custom dimension data reflects data processed as of 48 hours ago"
- For GA4 360 users, the delay is typically shorter but still exists
This delay does not apply to standard GA4 events and built-in dimensions. sessions, activeUsers, pageViews, and standard dimension breakdowns process much faster.
Comparison Table: Dimension Name Formats
| Where You See It | What It Shows | What the API Requires |
|---|---|---|
| GA4 Admin > Custom Definitions | "Article Category" (display name) | customEvent:article_category |
| GA4 Explore | "Article Category" (display name) | N/A (UI handles this) |
| GA4 Looker Studio connector | "Article Category" or customEvent:article_category (varies) | customEvent:article_category |
| GA4 Data API | Not shown: must be constructed | customEvent:article_category |
| GA4 Dimensions Explorer | customEvent:article_category | customEvent:article_category |
| BigQuery export | event_params.key = 'article_category' | Different schema entirely |
The only place that shows you the correct API field name by default is the GA4 Dimensions & Metrics Explorer tool. Every other surface uses display names or abstracts the format away.
Troubleshooting
"My API request returns INVALID_ARGUMENT for the custom dimension." You are using the wrong field name. Check that you are using customEvent: (or customUser:) as the prefix, followed by the exact parameter name you send in your events. The display name you set in GA4 Admin does not work in API calls.
"The custom dimension returns empty or (not set) for all rows." Three possible causes: (1) The dimension has not been registered in GA4 Admin > Custom Definitions. You must explicitly register it there. GA4 does not auto-register event parameters. (2) The data is within the 48-hour processing window and is not yet available. Try querying for data from 3+ days ago. (3) The parameter name in your API call does not match the parameter name in your events. Check for typos and case differences.
"I see data for this dimension in GA4 Explore but not in the API." The Explore UI applies Blended reporting identity by default; your API call may be using Device-based. This affects user-scoped dimensions more than event-scoped ones. Also verify you are querying the same date range and that no filters differ.
"My totals include a large (other) row." You have exceeded the 500-value cardinality limit for this dimension. See the cardinality section above. Reducing the number of unique values sent for this parameter is the only fix within standard GA4.
**"The customEvent: prefix works in the Dimensions Explorer but my third-party tool uses a different format."** Some connectors (including certain Looker Studio GA4 connectors) abstract the prefix and require only the parameter name. Check your connector's documentation. The raw GA4 Data API always requires the full customEvent: prefix.
Bottom Line
The customEvent:parameter_name API format is the single most important undocumented detail for anyone pulling GA4 custom dimensions into Google Sheets. Without it, your queries fail silently or return INVALID_ARGUMENT errors that send you in the wrong direction for hours.
Beyond the naming format: register your dimensions in GA4 Admin before querying, account for the 48-hour processing delay on custom event data, monitor cardinality before it becomes a data quality problem, and use BigQuery for any dimension with more than a few hundred unique values.
Custom metrics follow the same pattern. Custom dimensions of all scopes (event, user, item) follow the same prefix convention, just with the appropriate scope prefix substituted.
Once you have the format right, GA4 custom dimensions in Sheets are genuinely powerful. Getting there requires knowing the one thing most guides skip.
Pull GA4 Custom Dimensions Into Sheets Without the API Headaches
brooked.io handles the customEvent: field name mapping automatically, surfaces cardinality warnings before they corrupt your data, and accounts for the 48-hour custom event processing delay in its refresh scheduling. Connect your GA4 property and get custom dimension data in Sheets without writing or maintaining API code.
Related reading:
- Why Your GA4 Numbers in Google Sheets Don't Match GA4: The 3 Real Reasons (and How to Fix Each)
- How to Sync HubSpot Deals and Pipeline Data to Google Sheets in Real Time
Troubleshooting quick reference
Frequently asked questions
Do I need to register a custom dimension in GA4 Admin before querying it via the API?
Yes. GA4 does not automatically make event parameters available as queryable dimensions. You must go to Admin > Data Display > Custom Definitions, click "Create custom dimension," enter the parameter name exactly as it appears in your events, and set the scope. Until this registration exists, the API returns nothing for that parameter even if data is being collected.
Is the `customEvent:` prefix case-sensitive?
The prefix itself (customEvent) is case-sensitive. CustomEvent: or customevent: will fail. The parameter name after the colon is also case-sensitive and must match the case of the parameter name in your events exactly.
How many custom dimensions can I register in GA4?
Standard GA4 properties support up to 50 event-scoped custom dimensions, 25 user-scoped custom dimensions, and 50 event-scoped custom metrics. GA4 360 properties have higher limits (125 event-scoped, 100 user-scoped). These counts are per property.
Can I query custom dimensions from multiple scopes in the same API request?
Yes. You can mix customEvent:, customUser:, and standard dimensions in a single request. Be aware that combining user-scoped and event-scoped dimensions in the same report can produce unexpected results due to how GA4 joins these scopes: event-scoped dimensions typically have higher row counts and the join may inflate or compress numbers depending on the cardinality of each dimension.
Why does my custom dimension work in GA4 Explore but fail in Apps Script?
Explore uses the display name and handles the API field name internally. Apps Script calls the raw Data API and requires the full customEvent:parameter_name format. This is the most common reason for the discrepancy. Double-check your request body in Apps Script and confirm the field name format.
Ready to connect your data to Google Sheets?
Brooked's free tier covers 100 imports per month with AI Analyst included, no credit card required.
Install Brooked free →

