// Docs · shadow-api.v1

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.

Adapted and extended from datola.es — hacks y trucos en GA4 (parte II) and the original IKAUE hack.

What 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.

URL anatomy

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."

Surfaces (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.

Keys reference

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

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.

Filter shape

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

{
  "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.

Operator codes (evaluationType)

UI labelCodeComplement
coincide exactamente con8false
contiene3false
empieza por4false
termina con5false
coincide con la regex2false
coincide con la regex parcial11false
no coincide exactamente con8true
no contiene3true
no empieza por4true
no termina con5true
no coincide con la regex2true
no coincide con la regex parcial11true
es (no se han definido datos)12false
no es (no se han definido datos)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.

Comparisons (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:

{
  "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):

_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:

{
  "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.

Limits & 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 API names, not the UI label. Use pagePath, not "Page path".
  • Undocumented contract: Google can change this any day. We re-test it against new GA4 releases when something breaks.

Credits