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
| Path | Method | Purpose | Body limit |
|---|---|---|---|
/v1/r | POST | Recording packets (binary, gzip) | 1 MB |
/v1/r/css | POST | Which stylesheet hashes of a recording the server still needs | 64 KB |
/v1/hb/{pk} | GET | Live heartbeat (answers how often to send frames) | – |
/v1/l | POST | Live frames (viewport band, clicks) | 2 KB |
/v1/snap | POST | Owner page capture (menu open, quiz step) | 4 MB |
/v1/snap/session | POST | Starts an owner capture with its one-time token | 64 KB |
/v1/privacy/{pk} | GET | Public privacy page | – |
/v1/privacy/{pk}/erase, /v1/privacy/{pk}/export | POST | Privacy 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
| Recipe | Status |
|---|---|
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 |
| Nginx | widen the location pattern (below) and raise client_max_body_size to 4m |
| Cloudflare Worker, Netlify | add the paths to the allowlist; pass request.body through unchanged |
| WordPress | add 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 subdomain | everything 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.