First-Party Domain Setup
Serve Zeotap event collection from your own domain so visitor identity survives Safari’s Intelligent Tracking Prevention (ITP) and similar browser protections. Without this setup, the SDK’s anonymous ID lives in storage the browser may erase after as little as 7 days — with it, the ID is a long-lived cookie set by a server response on your own domain, which browsers do not cap.
Why this matters
- Safari ITP caps script-written storage at 7 days. Cookies written by JavaScript and
localStorageentries are deleted after 7 days without a site visit (24 hours in some cases). The SDK’s anonymous ID rotates, and the same visitor looks like a new person every week. - CNAME cloaking no longer works. Pointing a subdomain at a third-party collection endpoint via DNS CNAME is detected by ITP, and cookies set that way get the same 7-day cap.
- Cookies set by a first-party server response are exempt. When your own server (or a proxy you operate on your domain) sets the cookie via a
Set-Cookieresponse header, it persists for its full lifetime — across all major browsers.
Zeotap uses this: you run a small proxy on your domain that forwards SDK traffic to the Zeotap events endpoint, and the response stamps the visitor’s anonymous ID as a first-party, HttpOnly cookie ({namespace}_fp_id, e.g. zeotap_fp_id) with a 400-day lifetime. Every later request carries that cookie, and Zeotap keeps the visitor’s identity stable — even after the browser has wiped everything JavaScript could write.
Prerequisites
- A website served over HTTPS on your own domain
- The ability to add a reverse-proxy route to your web server, CDN, or application framework
- An event source write key (Streams > Event Sources)
Step 1: Mount a proxy on your domain
Add a route on your main domain — we recommend the path /cdp/ — that forwards requests to https://events.zeotap.com. The proxy must:
| Requirement | Detail |
|---|---|
| Forward all methods | GET and POST (events, batches, cookie sync) |
| Strip the mount prefix | /cdp/v1/batch → /v1/batch |
| Preserve the query string | Write keys and beacon parameters travel there |
| Forward request headers | At minimum Content-Type, X-Write-Key, Cookie |
| Set the upstream host | Host: events.zeotap.com |
| Append the client IP | X-Forwarded-For — keeps geo enrichment accurate |
| Set the protocol | X-Forwarded-Proto: https |
Pass Set-Cookie through | Do not strip or rewrite response cookies |
| Disable caching | Event responses are per-visitor and must never be cached |
The proxy must run on infrastructure you operate on your registrable domain. A bare DNS CNAME pointing at Zeotap does not work — browsers detect it and apply the same 7-day cap.
nginx
location /cdp/ {
proxy_pass https://events.zeotap.com/;
proxy_set_header Host events.zeotap.com;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_ssl_server_name on;
proxy_buffering off;
}The trailing slash on proxy_pass strips the /cdp/ prefix. nginx passes Set-Cookie through by default — do not add proxy_hide_header Set-Cookie or proxy_cookie_domain rewriting.
Cloudflare Worker
If your site is on Cloudflare, a Worker route (www.your-site.com/cdp/*) can proxy without touching your origin server:
export default {
async fetch(request) {
const url = new URL(request.url);
url.hostname = "events.zeotap.com";
url.pathname = url.pathname.replace(/^\/cdp/, "");
const upstream = new Request(url, request);
upstream.headers.set("X-Forwarded-Proto", "https");
// Cloudflare sets CF-Connecting-IP / X-Forwarded-For automatically.
return fetch(upstream, { redirect: "manual" });
},
};Disable caching for the route (Cache Rules → Bypass cache on /cdp/*).
Next.js
Use a route handler that proxies explicitly — its behavior is identical whether you self-host or deploy to a platform like Vercel:
// app/cdp/[...path]/route.ts
const UPSTREAM = "https://events.zeotap.com";
async function proxy(req: Request, path: string[]) {
const url = new URL(req.url);
const upstream = `${UPSTREAM}/${path.join("/")}${url.search}`;
const headers = new Headers(req.headers);
headers.set("Host", "events.zeotap.com");
headers.set("X-Forwarded-Proto", "https");
const res = await fetch(upstream, {
method: req.method,
headers,
body: req.method === "POST" ? req.body : undefined,
// @ts-expect-error duplex is required for streaming bodies
duplex: "half",
});
return new Response(res.body, {
status: res.status,
headers: res.headers, // includes Set-Cookie
});
}
export async function GET(req: Request, ctx: { params: Promise<{ path: string[] }> }) {
return proxy(req, (await ctx.params).path);
}
export async function POST(req: Request, ctx: { params: Promise<{ path: string[] }> }) {
return proxy(req, (await ctx.params).path);
}next.config.js rewrites can also work, but hosted platforms may route external rewrites through their own edge proxy where caching and cookie handling vary — if you use a rewrite, include the verification step below to confirm Set-Cookie reaches the browser.
Step 2: Point the SDK at your proxy
Load the SDK with apiHost set to your proxy path and enable firstPartyCookies:
zeotap.load("YOUR_WRITE_KEY", {
apiHost: "/cdp", // same-origin path — no CORS involved
firstPartyCookies: true,
});apiHost accepts a relative path ("/cdp") or an absolute URL ("https://www.your-site.com/cdp"). With firstPartyCookies: true the SDK announces its namespace on every request, adopts the canonical anonymous ID echoed by the server, and keeps its local storage in sync — so getAnonymousId() style integrations and the durable cookie always agree.
Using a dedicated subdomain instead
If you prefer a subdomain (cdp.your-site.com) over a path, the proxy setup is the same, but the SDK must send cookies cross-origin:
zeotap.load("YOUR_WRITE_KEY", {
apiHost: "https://cdp.your-site.com",
firstPartyCookies: true,
credentials: "include", // required for a cross-origin subdomain
});The subdomain must point at your proxy infrastructure, not directly at Zeotap. The path form is simpler and is recommended.
Step 3: Verify
- Open your site with the browser DevTools Network tab open.
- Find the
POSTto/cdp/v1/batch. The response should include aSet-Cookie: zeotap_fp_id=...; Max-Age=34560000; HttpOnlyheader (visible under the response headers; the cookie itself appears under Application → Cookies on your domain). - Confirm the response body echoes
"anonymousId"and that its value matches the cookie. - In the console, run
document.cookie—zeotap_fp_idmust not appear (it isHttpOnlyby design; JavaScript-visible copies remain in the SDK’s own storage). - Clear Local Storage and site cookies visible to JavaScript, reload, and confirm the next
/cdp/v1/batchresponse carries the sameanonymousId— the durable cookie has re-established the identity. - Check Debugging to confirm events are arriving with a stable
anonymousId.
How identity is reconciled
- Requests carrying the
{namespace}_fp_idcookie use its value as the canonical anonymous ID. If the SDK sent a different ID (for example after the browser wiped its storage), the original is preserved on the event atcontext.priorAnonymousIdfor identity stitching. - Requests without the cookie adopt the SDK’s anonymous ID (or receive a new one) and get the cookie stamped in the response.
reset()(logout) rotates the server cookie: queued events are delivered under the old identity first, then the next request establishes a fresh ID.
Limitations
- The cookie is scoped to the exact host serving the proxy (host-only). Visitors moving between
www.your-site.comand other subdomains keep continuity through the SDK’s own cross-subdomain cookie, but the durable server cookie is re-established per host. - Geo enrichment reflects the address your proxy reports in
X-Forwarded-For. If the proxy omits it, location data degrades to your proxy’s own address. - Ad-blockers that block by URL pattern may still block
/cdp/paths that appear on public blocklists; choosing a site-specific path name reduces this.
Next Steps
- Browser SDK — Full SDK reference including
firstPartyCookies - Event sources — Create and manage write keys
- Debugging — Verify events are arriving
- Consent management — Configure consent categories