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
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."
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:
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.
04Keys reference
The Report Builder writes the following keys. They cover ~95% of free-form reports.
Frontend names, not Data API names
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.| 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. | ||
05Filter 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.
06Operator codes (evaluationType)
| UI label | Code | Complement |
|---|---|---|
| matches exactly | 8 | false |
| contains | 3 | false |
| begins with | 4 | false |
| ends with | 5 | false |
| matches regex | 2 | false |
| matches partial regex | 11 | false |
| does not match exactly | 8 | true |
| does not contain | 3 | true |
| does not begin with | 4 | true |
| does not end with | 5 | true |
| does not match regex | 2 | true |
| does not match partial regex | 11 | true |
| is (not set) | 12 | false |
| is not (not set) | 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.
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:
{
"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.
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.
https://analytics.google.com/analytics/web/
#/p<PROPERTY_ID>/assetlibrary/builder/new
?params=<encoded state>Editor or Administrator access required
- 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.._cidsis the ordered card registry: a comma separated list of<cardId>.<templateId>pairs (not a JSON array). The observed template id is182.- The account segment is optional —
#/p<PROPERTY_ID>/…resolves the account for you. Legacya<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.
_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
| cardType | Chart | Requires |
|---|---|---|
| 59 | Scorecard (KPI) | 1 metric |
| 65 | Table | dimension + metric |
| 72 | Line chart | dimension (usually date) + metric |
| 3 | Bar chart (horizontal) | dimension + metric |
| 67 | Column chart (vertical) | dimension + metric |
| 73 | Donut | dimension + metric |
| 66 | Funnel | analysisFeature.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..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.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. |
_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..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.._cids c1.182,c2.182 Ordered card registry: cardId.templateId (comma separated). | ||
| _r.{id}..cardType | 65 | Chart type for that card. |
_r.{id}..cardType 65 Chart type for that card. | ||
| _r.{id}..seldim | ["pagePath"] | Selected dimensions (GA4 UI names, e.g. pagePath, hostname). |
_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}..selmet ["screenPageViews"] Selected metrics (GA4 UI names, e.g. screenPageViews, conversions). | ||
| _r.{id}..innerTitle | Top pages | Card title shown on the canvas. |
_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}..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}..sortKey d-0 Table only. Column the table sorts by. | ||
| _r.{id}..rowsPerPage | 500 | Table only. Page size; startRow sets the offset. |
_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}..dashboardTableMetadata.showSearchRow true Table only. Search row / totals row toggles. | ||
| _r.{id}..dataFilters | [{"fieldName":"country",…}] | Card-scoped filters. |
_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. |
_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(nothostName),conversions(notkeyEvents),landingPage(notlandingPagePlusQueryString). 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.
- 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.
- 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.
- Explorer: documented dimensions, metrics, sorting, titles and visibility keys.
- Encoding: established the params key/value chain and single-encoding rules.
13Credits & resources
- ga4builder.com by Steve Lamar — report URL builder using the shadow API.
- GA4SPY — the surface you're using right now.