
Guide
How to pull GA4 reports with the Data API
Query the GA4 Data API with runReport: auth, dimensions and metrics, realtime, quotas, and the keyEvents fallback older properties need. With working requests.
Published August 12, 2026
The GA4 Data API is one endpoint: POST a JSON body of dimensions, metrics, and date ranges to properties/<id>:runReport and get rows back. Auth is a service account with Viewer access on the property — no IAM roles, no OAuth screens. Five small requests cover everything a site dashboard needs.
What do you need before the first request?
- A service account with the Analytics Data API enabled and Viewer access on the GA4 property — the full console walkthrough is in the service account guide.
- The numeric property id (GA4 Admin → Property details). Not the
G-XXXXXXXXXXmeasurement id — that identifies a stream for collection; the API wants the property. - A bearer token for scope
…/auth/analytics.readonly.
What does a runReport request look like?
POST https://analyticsdata.googleapis.com/v1beta/
properties/123456789:runReport
Authorization: Bearer <token>
{
"dateRanges": [{ "startDate": "28daysAgo", "endDate": "today" }],
"dimensions": [{ "name": "sessionSourceMedium" }],
"metrics": [{ "name": "sessions" }],
"orderBys": [{ "metric": { "metricName": "sessions" }, "desc": true }],
"limit": 8
}The response is columnar: dimensionHeaders, metricHeaders, then rows of string values in the same order. Everything else is variations on this body — different names from the dimensions and metrics reference, filters, or ordering. The runReport reference documents the full request shape.
Which five reports cover a small site's needs?
The dashboard behind this post runs exactly five requests in parallel, and hasn't needed a sixth:
| Question | Endpoint | Dimensions × metrics |
|---|---|---|
| Anyone here now? | runRealtimeReport | eventName × activeUsers, eventCount |
| How is the month? | runReport | (none) × activeUsers, sessions, newUsers, eventCount, keyEvents |
| Where from? | runReport | sessionSourceMedium × sessions, top 8 |
| Landing where? | runReport | landingPagePlusQueryString × sessions, top 8 |
| Doing what? | runReport | eventName × eventCount, top 10 |
One production wrinkle worth stealing: keyEvents is the current name for what used to be conversions, and properties configured before the rename can reject it. The totals request carries a fallback:
// keyEvents replaced "conversions" — older properties reject it.
const totals = await runReport({
metrics: ["activeUsers", "sessions", "newUsers", "eventCount", "keyEvents"],
}).catch(() =>
runReport({
metrics: ["activeUsers", "sessions", "newUsers", "eventCount"],
})
)How do you stay inside the API quotas?
The quota system charges core tokens per request, budgeted per property per hour and per day — generous for deliberate use, hostile to accidental loops. The design answer is to make every Google call deliberate:
The same pull-once-then-read-locally instinct scales down to the command line — the CLI build note applies it to Search Console data with agents as the consumer.
Questions
Is the GA4 Data API free?
Yes, within quotas. Standard properties get a daily and hourly budget of core tokens per property, and each request spends tokens in proportion to its complexity. A cached small-site dashboard uses a rounding error of the budget; an uncached one can burn through it.
What is the difference between runReport and runRealtimeReport?
runReport queries processed historical data and accepts date ranges, most dimensions, and most metrics. runRealtimeReport covers roughly the last 30 minutes with a restricted dimension set — active users and event names, not acquisition detail. Dashboards need both: one for the trend, one for the pulse.
Why don't API numbers match the GA4 interface exactly?
Three usual causes: data thresholding withholding rows on low-volume demographics, intraday data still processing (allow 24–48 hours for final numbers), and the interface applying its own default filters. Compare like for like — same dates, same scope — before assuming the request is wrong.
Related notes
- Service account access to GA4 and Search Console
- How to use the Google Search Console API
- GA4 without gtag: a first-party collector
More on systems integration
- Own your analytics stack, end to end
- The DataForSEO API: SEO data by the cent
- What is server-side tagging?
- How to migrate systems without losing data
Related service: Systems Integration
Want this kind of engineering on your project?
Tall Karol takes on fractional and project-based engagements for startups and agencies.
Book a working session