Proxy paths for recordings, heatmaps and live

Heatmaps need nothing new: their signals travel inside the normal event batches (/v1/e). Recordings, the live view, owner page captures and the visitor privacy center use further paths. The SDK sends them to the same host as everything else – your proxy path when data-host points at it – so a proxy that only forwards the four classic families silently breaks these features (recordings never arrive, the live board stays empty, the privacy panel reports an error).

The extra path families

PathMethodPurposeBody limit
/v1/rPOSTRecording packets (binary, gzip)1 MB
/v1/r/cssPOSTWhich stylesheet hashes of a recording the server still needs64 KB
/v1/hb/{pk}GETLive heartbeat (answers how often to send frames)–
/v1/lPOSTLive frames (viewport band, clicks)2 KB
/v1/snapPOSTOwner page capture (menu open, quiz step)4 MB
/v1/snap/sessionPOSTStarts an owner capture with its one-time token64 KB
/v1/privacy/{pk}GETPublic privacy page–
/v1/privacy/{pk}/erase, /v1/privacy/{pk}/exportPOSTPrivacy center requests (device proof in the body)64 KB

Forward them exactly like /v1/e: strip cookie and authorization, keep user-agent, origin, content-type and the body as bytes (recording packets are binary – do not decode them as text), add x-3x-client-ip, x-3x-client-country and your x-3x-proxy-key.

The geo header

For the world map the proxy may add the visitor's coarse location from your edge as x-3x-client-geo: CC;region;city;lat;lon (for example CH;ZH;Zürich;47.37;8.54; empty fields stay empty). The API trusts it only together with a valid proxy key, uses it for the live view and the coarsened place of the session, and never stores an IP. Without it the location is the country from x-3x-client-country.

The Next.js handler (createX3ProxyHandler from @x3growth/sdk/next/proxy) sets the header itself. For a custom route the same helper is exported: visitorGeo(request.headers) returns the header value from Vercel's x-vercel-ip-* headers (else Cloudflare's), or null when the edge sends no country.

Per recipe

RecipeStatus
Next.js route handler (createX3ProxyHandler)forwards every path above, reads the body of /v1/r as bytes and adds x-3x-client-geo from Vercel or Cloudflare headers – nothing to do
Nginxwiden the location pattern (below) and raise client_max_body_size to 4m
Cloudflare Worker, Netlifyadd the paths to the allowlist; pass request.body through unchanged
WordPressadd the paths to the route pattern; recordings need binary bodies, which the WordPress REST API does not pass reliably – point data-host at the API host or use a CNAME for recordings
CNAME subdomaineverything works, no change

Nginx pattern with the extra families:

location ~ ^/n3(/v1/(?:e|collect|cb|c/[^/]+|s/[A-Za-z0-9@._-]+|r|r/css|hb/[^/]+|l|snap|snap/session|privacy/[^/]+(?:/erase|/export)?))$ {
  client_max_body_size 4m;
  # … the rest of the recipe unchanged
}

Allowlist for a Worker or edge function:

const ALLOWED = /^\/v1\/(?:e|collect|cb|c\/[^/]+|s\/[A-Za-z0-9@._-]+|r|r\/css|hb\/[^/]+|l|snap|snap\/session|privacy\/[^/]+(?:\/erase|\/export)?)$/;

Check

From a page of your site, with recordings switched on and consent given, the network panel shows POST /n3/v1/r with status 200 and the answer ok, and with the live board open GET /n3/v1/hb/<key> answers {"hz":1,…}. Einstellungen › Aufnahmen & Heatmaps shows the detected proxy and SDK version in the setup diagnostics.