Google Analytics 4 Data API
Query GA4 properties for arbitrary reporting data via curl.
Prerequisites
- Google OAuth skill must be set up (
.claude/skills/google-oauth/) - Scope:
analytics.readonly - Token retrieval:
TOKEN=$(node ~/.claude/skills/google-oauth/scripts/token-store.js get analytics.readonly) - If token retrieval fails (non-zero exit), trigger the OAuth flow per the google-oauth skill
Workflow
Step 1: Discover Property IDs (mandatory before any report)
TOKEN=$(node ~/.claude/skills/google-oauth/scripts/token-store.js get analytics.readonly)
curl -s -H "Authorization: Bearer $TOKEN" \
"https://analyticsadmin.googleapis.com/v1beta/accountSummaries"
Response:
{
"accountSummaries": [{
"displayName": "My Account",
"account": "accounts/123456",
"propertySummaries": [{
"property": "properties/987654321",
"displayName": "My Website"
}]
}]
}
Extract the property value (e.g. properties/987654321). The numeric part (987654321) is used in Data API URL paths. Supports pagination via pageSize and pageToken query params.
Step 2: Use the property ID in report calls
API Endpoints
Base URL: https://analyticsdata.googleapis.com/v1beta/
| Method | Endpoint | Description |
|---|---|---|
| POST | properties/ID:runReport |
Standard report |
| POST | properties/ID:batchRunReports |
Multiple reports in batch |
| POST | properties/ID:runRealtimeReport |
Realtime data (last 30min) |
| POST | properties/ID:runPivotReport |
Pivot table report |
| POST | properties/ID:batchRunPivotReports |
Batch pivot reports |
| POST | properties/ID:checkCompatibility |
Check dimension/metric compatibility |
| GET | properties/ID/metadata |
Available dimensions & metrics metadata |
runReport Request Body
{
"dimensions": [{"name": "date"}],
"metrics": [{"name": "sessions"}],
"dateRanges": [{"startDate": "28daysAgo", "endDate": "today"}],
"dimensionFilter": "<FilterExpression>",
"metricFilter": "<FilterExpression>",
"orderBys": ["<OrderBy>"],
"limit": "10000",
"offset": "0",
"currencyCode": "USD",
"keepEmptyRows": false,
"returnPropertyQuota": false,
"metricAggregations": ["TOTAL", "MINIMUM", "MAXIMUM"],
"comparisons": ["<Comparison>"]
}
limit: string, default 10000, max 250000- Date formats: relative (
today,yesterday,NdaysAgo) or absoluteYYYY-MM-DD - Multiple dateRanges supported for comparison
FilterExpression
Union type — exactly one of these four fields:
andGroup:{"expressions": [FilterExpression, ...]}orGroup:{"expressions": [FilterExpression, ...]}notExpression:FilterExpressionfilter: primitive filter (see below)
Primitive filter (alongside fieldName):
| Type | Fields | Notes |
|---|---|---|
stringFilter |
{matchType, value, caseSensitive} |
matchType: EXACT, BEGINS_WITH, ENDS_WITH, CONTAINS, FULL_REGEXP, PARTIAL_REGEXP |
inListFilter |
{values[], caseSensitive} |
Match any in list |
numericFilter |
{operation, value} |
operation: EQUAL, LESS_THAN, LESS_THAN_OR_EQUAL, GREATER_THAN, GREATER_THAN_OR_EQUAL; value: {int64Value} or {doubleValue} |
betweenFilter |
{fromValue, toValue} |
NumericValue objects |
emptyFilter |
{} |
Matches "(not set)" values |
OrderBy
{"desc": true, "metric": {"metricName": "sessions"}}
{"desc": false, "dimension": {"dimensionName": "date", "orderType": "NUMERIC"}}
orderType enum: ALPHANUMERIC, CASE_INSENSITIVE_ALPHANUMERIC, NUMERIC
Realtime Reports
Uses minuteRanges instead of dateRanges:
{"minuteRanges": [{"startMinutesAgo": 29, "endMinutesAgo": 0}]}
- Max 2 minute ranges
- Standard properties: up to 29 min ago; GA 360: up to 59 min
- Same filter/orderBy/limit structure as runReport
Response Shape
{
"dimensionHeaders": [{"name": "date"}],
"metricHeaders": [{"name": "sessions", "type": "TYPE_INTEGER"}],
"rows": [{"dimensionValues": [{"value": "20260301"}], "metricValues": [{"value": "1234"}]}],
"rowCount": 100,
"metadata": {}
}
Common Dimensions
| Dimension | Description |
|---|---|
| date | YYYYMMDD format |
| dateHour | YYYYMMDDHH |
| country, city, region, continent | Geographic |
| deviceCategory | desktop, mobile, tablet |
| browser, operatingSystem | Tech |
| pagePath, pageTitle, landingPage | Content |
| sessionSource, sessionMedium | Traffic source |
| sessionDefaultChannelGrouping | Channel |
| sessionCampaignName | Campaign |
| eventName | Event name |
| hostname | Site hostname |
| newVsReturning | new or returning |
| platform | web, iOS, Android |
| language | User language |
Common Metrics
| Metric | Description |
|---|---|
| sessions | Total sessions |
| totalUsers, newUsers, activeUsers | User counts |
| screenPageViews | Page views |
| engagedSessions | Sessions with engagement |
| bounceRate | Float 0-1 |
| averageSessionDuration | Seconds (float) |
| engagementRate | Float 0-1 |
| sessionsPerUser | Float |
| eventCount | Total events |
| conversions | Key events |
| totalRevenue | Revenue (currency) |
| ecommercePurchases | Purchase count |
| userEngagementDuration | Total engagement seconds |
Curl Examples
1. List all properties
TOKEN=$(node ~/.claude/skills/google-oauth/scripts/token-store.js get analytics.readonly)
curl -s -H "Authorization: Bearer $TOKEN" \
"https://analyticsadmin.googleapis.com/v1beta/accountSummaries"
2. Traffic overview by date (last 7 days)
curl -s -X POST \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"dimensions": [{"name": "date"}],
"metrics": [{"name": "sessions"}, {"name": "totalUsers"}, {"name": "screenPageViews"}],
"dateRanges": [{"startDate": "7daysAgo", "endDate": "today"}],
"orderBys": [{"dimension": {"dimensionName": "date"}, "desc": false}]
}' \
"https://analyticsdata.googleapis.com/v1beta/properties/PROPERTY_ID:runReport"
3. Top pages
curl -s -X POST \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"dimensions": [{"name": "pagePath"}],
"metrics": [{"name": "screenPageViews"}],
"dateRanges": [{"startDate": "28daysAgo", "endDate": "today"}],
"orderBys": [{"metric": {"metricName": "screenPageViews"}, "desc": true}],
"limit": "20"
}' \
"https://analyticsdata.googleapis.com/v1beta/properties/PROPERTY_ID:runReport"
4. Traffic by source filtered to US
curl -s -X POST \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"dimensions": [{"name": "sessionSource"}, {"name": "sessionMedium"}],
"metrics": [{"name": "sessions"}, {"name": "totalUsers"}],
"dateRanges": [{"startDate": "28daysAgo", "endDate": "today"}],
"dimensionFilter": {
"filter": {
"fieldName": "country",
"stringFilter": {"matchType": "EXACT", "value": "United States"}
}
}
}' \
"https://analyticsdata.googleapis.com/v1beta/properties/PROPERTY_ID:runReport"
5. Realtime active users by country
curl -s -X POST \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"dimensions": [{"name": "country"}],
"metrics": [{"name": "activeUsers"}],
"minuteRanges": [{"startMinutesAgo": 29, "endMinutesAgo": 0}]
}' \
"https://analyticsdata.googleapis.com/v1beta/properties/PROPERTY_ID:runRealtimeReport"
Tips & Gotchas
- Property ID is always
properties/XXXXXXXXin the URL path - The
datedimension returnsYYYYMMDD(no dashes) bounceRateis 0-1, not a percentage- Max 9 dimensions and 10 metrics per report
limitis a string, not an integer- Empty reports:
rowskey will be absent from the response - Use the
properties/ID/metadataGET endpoint to discover all available dimensions/metrics for a property - API quota: 10,000 requests/day/property by default