// Docs · shadow-api.v2.1

The GA4 Shadow API

A pragmatic field guide to the undocumented URL contract that GA4 Explorations and free-form reports read on startup — the same contract the Report Builder and Comparisons use under the hood.

No API key3 surfacesURL onlyUndocumented

01What it is

When you open a GA4 Exploration or Free-form report, the app hydrates the report's initial state from a key-value string after #/...?params= in the URL. There is no official documentation for this contract — but it has been stable since Explorations launched, and it's what makes deep-linking a fully configured report possible without any backend integration. We call it the shadow API.

We never call GA4 on your behalf. The link opens inside your own browser session and uses your existing GA4 permissions on the property you target.

02URL anatomy

shadow-api
https://analytics.google.com/analytics/web/
  #/p<PROPERTY_ID>/<SURFACE>
    ?params=<encoded payload>
    [&r=new-report]
  • p<PROPERTY_ID> — numeric GA4 property the report opens against. The user must have access to it.
  • <SURFACE> — one of assetlibrary/explorer/new, reports/reportinghub or reports/dashboard (see next section).
  • params= — the payload. Multiple keys are joined with the literal three-character sequence %26 (the URL-encoded form of &), not a real ampersand — that's what keeps the whole payload inside the hash fragment.
  • r=new-report — explorer only. Tells GA4 to treat this as a brand-new report instead of loading a saved one.

Inside params, every key starts with _u.. (user / view state — e.g. comparisons, navigation mode) or _r.. (report — the actual chart definition). A dotted path like_r.explorerCard..metrics means "set metrics on the explorerCard of the report."

03Surfaces (three URL flavours)

The shadow API is the same contract on three different GA4 surfaces. Pick the one that matches what you want the user to land on:

Surface
Explorer
/assetlibrary/explorer/new

Free-form report with dimensions, metrics, sort, filters and comparisons. Emits the full _r.explorerCard..* chain.

Surface
Reporting hub
/reports/reportinghub

Standard reports view. Ideal for comparison-only links: a single _u..comparisons key + no explorer state.

Surface
Dashboard
/reports/dashboard

The default reports dashboard. Same payload contract as reportinghub; different landing screen.

The Reporting hub flavour is what GA4 itself emits when you save a comparison from the top bar. See Comparisons below for the payload shape.

04Keys reference

The Report Builder writes the following keys. They cover ~95% of free-form reports.

Frontend names, not Data API names

Every fieldName, seldim and selmet value is the GA4 frontend name:hostname, pagePath, landingPage, conversions — nothostName, pagePathPlusQueryString or keyEvents. This is the single most common cause of a URL that opens empty.
KeyValueNotes
_u..nav
"maui"
Tells the explorer to use the free-form navigation surface.
_r.explorerCard..dimensions
["dim1","dim2"]
Dimensions added to the report.
_r.explorerCard..seldim
["dim1"]
Dimensions currently selected (subset of the above).
_r.explorerCard..ddimensions
["dim"]
Optional "detail" dimensions for the secondary card.
_r.explorerCard..metrics
["m1","m2"]
Metrics added to the report.
_r.explorerCard..selmet
["m1"]
Metrics currently selected (subset of the above).
_r.explorerCard..sortKey
"metricName"
Default sort column.
_r.explorerCard..isAscending
true | false
Sort direction.
_r.explorerCard.primaryCard..isHidden
true | false
Hide the primary chart.
_r.explorerCard.secondaryCard..isHidden
true | false
Hide the table card.
_r..dataFilters
[Filter, …]
Report-level filters (JSON array, see below).
_u..comparisons
[Comparison, …]
Comparison segments (JSON array, see below).
_r..title
"My report"
Visible report title.
_r..defaultReportTitle
"My report"
Fallback title if the user clears the visible one.

05Filter shape

Each filter in _r..dataFilters is a JSON object:

shadow-api
{
  "type": 1,
  "fieldName": "pagePath",
  "evaluationType": 3,
  "expressionList": ["checkout"],
  "complement": false,
  "isCaseSensitive": true,
  "expression": ""
}
  • type: 1 — dimension/metric filter (the only type the explorer surfaces).
  • fieldName — the GA4 API name, e.g. pagePath, eventName, sessionSource.
  • evaluationType — operator code (see next section).
  • expressionList — array of values. Multiple values joined with , behave as OR. Empty for null-style operators.
  • complement — flip the operator. Negative operators ("does not contain", "does not match regex") are encoded as the positive operator + complement: true.
  • isCaseSensitive — most string operators support both modes.

06Operator codes (evaluationType)

UI labelCodeComplement
matches exactly8false
contains3false
begins with4false
ends with5false
matches regex2false
matches partial regex11false
does not match exactly8true
does not contain3true
does not begin with4true
does not end with5true
does not match regex2true
does not match partial regex11true
is (not set)12false
is not (not set)12true

Codes 3, 4, 5 and 8 are the everyday string operators. Code 2 is full regex, code 11 is the partial-match regex GA4 uses when you don't anchor the pattern. Code 12 is the special "no data" filter and ignores expressionList.

07Comparisons (separate from the report payload)

Comparisons live under _u..comparisons — the user/view state, not the report definition. That's why a link with just _u..comparisons and no _r..keys still works on the Reporting hub surface.

The compact shape used by the Report Builder — a name and a list of equality conditions joined as AND:

shadow-api
{
  "name": "Mobile + Spain",
  "filters": [
    { "fieldName": "deviceCategory", "expressionList": ["mobile"], "isCaseSensitive": false },
    { "fieldName": "country", "expressionList": ["Spain"], "isCaseSensitive": false }
  ]
}

The full shape GA4 itself emits when you save a comparison from the top bar (adds evaluationType, complement, isEnabled):

shadow-api
_u..comparisons=[
  {
    "name": "country matches All values containing Spain",
    "filters": [
      {
        "fieldName": "country",
        "expressionList": ["Spain"],
        "evaluationType": 3,
        "isCaseSensitive": true,
        "complement": false
      }
    ]
  }
]

Comparisons can also reference GA4's built-in "All users" segment through savedComparisonId + systemDefinedSavedComparisonType:

shadow-api
{
  "savedComparisonId": "7034674712",
  "name": "Todos los usuarios",
  "isEnabled": true,
  "filters": [],
  "systemDefinedSavedComparisonType": 8,
  "isSystemDefined": true
}

Comparisons render as additional series in the report, side-by-side with the baseline. They're scoped to the report only; they don't create a saved segment in your property.

08Dashboards (Asset Library builder)

The 2026 GA4 dashboard builder is a second, richer flavour of the same shadow contract. It lives on the Asset Library builder surface and hydrates an entire canvas of cards from one URL.

shadow-api
https://analytics.google.com/analytics/web/
  #/p<PROPERTY_ID>/assetlibrary/builder/new
    ?params=<encoded state>

Editor or Administrator access required

Anyone can open the link, but GA4 only lets users with Editor or Administrator access on the property save the resulting report or dashboard.
  • Unlike the explorer flavour, the whole inner parameter string is encoded once with encodeURIComponent, and inner keys are separated by a real &.
  • The minimum viable state is _u..nav=maui.
  • _u.* holds global state (navigation, dates, comparison). _r.* holds the report/card state.
  • _r.._cids is the ordered card registry: a comma separated list of <cardId>.<templateId> pairs (not a JSON array). The observed template id is 182.
  • The account segment is optional — #/p<PROPERTY_ID>/… resolves the account for you. Legacy a<ACCOUNT>p<PROPERTY> links keep working.
  • Arrays and objects are JSON.stringify-ed before they go into a value. Never encode twice.
  • Editor or Administrator access on the property is required to save a report or dashboard.
shadow-api
_u..nav=maui
_u.dateOption=last28Days
_r.._cids=c1.182,c2.182
_r.c1..cardType=59
_r.c1..selmet=["activeUsers"]
_r.c1..positioning={"x":2,"y":2,"width":22,"height":14}

09Card types

cardTypeChartRequires
59Scorecard (KPI)1 metric
65Tabledimension + metric
72Line chartdimension (usually date) + metric
3Bar chart (horizontal)dimension + metric
67Column chart (vertical)dimension + metric
73Donutdimension + metric
66FunnelanalysisFeature.funnel

Geo maps and touchpoint segments are not covered — their state has not been reverse-engineered reliably.

10Dashboard keys reference

_u..nav
maui
Required. Puts the app in builder navigation mode.
_u.dateOption
last28Days
Date preset. Use custom together with _u.date00 / _u.date01 (YYYYMMDD).
_u.comparisonOption
precedingPeriod
Date comparison. Adds _u.date10 / _u.date11 when custom.
_r..dataFilters
[{"fieldName":"country",…}]
Dashboard-wide filters. Same JSON shape as the explorer filters above.
_r.._cids
c1.182,c2.182
Ordered card registry: cardId.templateId (comma separated).
_r.{id}..cardType
65
Chart type for that card.
_r.{id}..seldim
["pagePath"]
Selected dimensions (GA4 UI names, e.g. pagePath, hostname).
_r.{id}..selmet
["screenPageViews"]
Selected metrics (GA4 UI names, e.g. screenPageViews, conversions).
_r.{id}..innerTitle
Top pages
Card title shown on the canvas.
_r.{id}..positioning
{"x":2,"y":2,"width":44,"height":28}
Grid position on an approximate 0–100 canvas scale.
_r.{id}..sortKey
d-0
Table only. Column the table sorts by.
_r.{id}..rowsPerPage
500
Table only. Page size; startRow sets the offset.
_r.{id}..dashboardTableMetadata.showSearchRow
true
Table only. Search row / totals row toggles.
_r.{id}..dataFilters
[{"fieldName":"country",…}]
Card-scoped filters.
_r.{id}..analysisFeature
{"funnel":{"sequence":{"step":[…]}}}
Funnel only. Steps carry an event filter, a label and immediatelyFollows.

Build pipeline: model → key/value map → inner parameter string → a single encodeURIComponent. Validate before you serialize: unique card ids, every card has its required fields, every position has x/y/width/height.

11Limits & gotchas

  • Comparisons: 4 max per report (GA4 silently ignores the rest).
  • Dimensions: 5 max in a free-form table.
  • Metrics: 10 max in a free-form table.
  • URL length: Long filter values + many comparisons can blow past browser URL caps (~32k chars in Chrome, less in older browsers). The Report Builder shows the byte count next to the preview so you can keep an eye on it.
  • Property access: The link opens in the signed-in user's GA4 session. They need access to the property; there's no auth bypass.
  • Field names are GA4 UI (frontend) names, not the Data API names and not the UI label. Use hostname (not hostName), conversions (not keyEvents), landingPage (not landingPagePlusQueryString). GA4SPY lists all three names for every field.
  • Undocumented contract: Google can change this any day. We re-test it against new GA4 releases when something breaks.

12API changelog

Observed changes to GA4's undocumented URL contract and the options supported by this reference. Entries describe tested behaviour, not an official Google release history.

v2.1
31 Aug 2026
Current
  • Dashboard: documented Asset Library builder URLs, card registry and positioning.
  • Cards: added scorecard, table, line, bar, column, donut and funnel types.
  • Filters: documented global and card-scoped dashboard filters.
  • Comparisons: added compact and full payload shapes plus saved comparisons.
v2.0
18 Aug 2026
Expanded
  • Surfaces: separated Explorer, Reporting hub and Dashboard URL flavours.
  • Filters: mapped evaluationType codes, complement and case sensitivity.
  • Comparisons: confirmed _u..comparisons as view state outside the report payload.
v1.0
Initial research
Baseline
  • Explorer: documented dimensions, metrics, sorting, titles and visibility keys.
  • Encoding: established the params key/value chain and single-encoding rules.

13Credits & resources