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 ofassetlibrary/explorer/new,reports/reportinghuborreports/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:
Free-form report with dimensions, metrics, sort, filters and comparisons. Emits the full _r.explorerCard..* chain.
Standard reports view. Ideal for comparison-only links: a single _u..comparisons key + no explorer state.
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.
| Key | Value | Notes |
|---|---|---|
| _u..nav | "maui" | Tells the explorer to use the free-form navigation surface. |
_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..dimensions ["dim1","dim2"] Dimensions added to the report. | ||
| _r.explorerCard..seldim | ["dim1"] | Dimensions currently selected (subset of the above). |
_r.explorerCard..seldim ["dim1"] Dimensions currently selected (subset of the above). | ||
| _r.explorerCard..ddimensions | ["dim"] | Optional "detail" dimensions for the secondary card. |
_r.explorerCard..ddimensions ["dim"] Optional "detail" dimensions for the secondary card. | ||
| _r.explorerCard..metrics | ["m1","m2"] | Metrics added to the report. |
_r.explorerCard..metrics ["m1","m2"] Metrics added to the report. | ||
| _r.explorerCard..selmet | ["m1"] | Metrics currently selected (subset of the above). |
_r.explorerCard..selmet ["m1"] Metrics currently selected (subset of the above). | ||
| _r.explorerCard..sortKey | "metricName" | Default sort column. |
_r.explorerCard..sortKey "metricName" Default sort column. | ||
| _r.explorerCard..isAscending | true | false | Sort direction. |
_r.explorerCard..isAscending true | false Sort direction. | ||
| _r.explorerCard.primaryCard..isHidden | true | false | Hide the primary chart. |
_r.explorerCard.primaryCard..isHidden true | false Hide the primary chart. | ||
| _r.explorerCard.secondaryCard..isHidden | true | false | Hide the table card. |
_r.explorerCard.secondaryCard..isHidden true | false Hide the table card. | ||
| _r..dataFilters | [Filter, …] | Report-level filters (JSON array, see below). |
_r..dataFilters [Filter, …] Report-level filters (JSON array, see below). | ||
| _u..comparisons | [Comparison, …] | Comparison segments (JSON array, see below). |
_u..comparisons [Comparison, …] Comparison segments (JSON array, see below). | ||
| _r..title | "My report" | Visible report title. |
_r..title "My report" Visible report title. | ||
| _r..defaultReportTitle | "My report" | Fallback title if the user clears the visible one. |
_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 label | Code | Complement |
|---|---|---|
| coincide exactamente con | 8 | false |
| contiene | 3 | false |
| empieza por | 4 | false |
| termina con | 5 | false |
| coincide con la regex | 2 | false |
| coincide con la regex parcial | 11 | false |
| no coincide exactamente con | 8 | true |
| no contiene | 3 | true |
| no empieza por | 4 | true |
| no termina con | 5 | true |
| no coincide con la regex | 2 | true |
| no coincide con la regex parcial | 11 | true |
| es (no se han definido datos) | 12 | false |
| no es (no se han definido datos) | 12 | true |
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
- Original IKAUE hack that uncovered the contract.
- datola.es — hacks y trucos en GA4 (parte II) — the article this doc expands.
- GA4SPY — the surface you're using right now.