Ingest protocol
The browser SDK speaks this protocol for you. You only need it to write your own client, to build a proxy, or to understand what you see in the network tab.
Endpoints
| Endpoint | Purpose |
|---|---|
POST /v1/e (alias /v1/collect) | Tracking batch, text/plain JSON (no CORS preflight). Always bodyless: 204 stored or deliberately ignored, 400 malformed, 403 unknown key or origin not allowed, 413 too large, 429 rate limited |
OPTIONS /v1/e | CORS answer for allowed origins (robustness for fetch fallbacks) |
GET /v1/c/{pk} | Client config of the publishable key's project (cached 60 s, public) |
GET /v1/s/{file} | SDK scripts: x3.js, x3.full.js, plugins x3.video.js, x3.sections.js, x3.forms.js, x3.identify.js, x3.consent.js, x3.bookings.js, versioned x3@1.2.3.js. ?k=pk_… inlines the client config |
POST /v1/cb | Conversion beacon for a conversion definition (≤ 4 KB, 4/min/IP, once per session and definition) |
POST /v1/projects/{project}/events | Server-side events with a secret key – see Server helper |
Batch format (wire v2)
{
"v": 2,
"k": "pk_3x_…",
"sid": "3f0c…",
"fresh": true,
"consent": false,
"test": false,
"pvs": [{
"id": "b2d1…", "seq": 3, "age": 41250, "nav": "navigate",
"host": "www.example.com", "path": "/pricing", "variant": null, "lang": "en",
"ref": "www.google.com", "attrFrom": "url",
"attr": { "utm_source": "linkedin", "utm_medium": "paid_social", "utm_campaign": "q4", "li_fat_id": "…" },
"vw": 1280, "vh": 720, "sw": 1280, "tp": false,
"ph": 5400, "sy": 2100, "sp": 52, "marks": { … },
"sec": { "hero": [0, 8200], "pricing": [9400, 12000] },
"eng": 30500, "clk": 4, "iclk": 2, "vids": []
}],
"ev": [{ "pv": "b2d1…", "n": 0, "t": 12000, "e": "cta_click", "p": { "cta": "book-demo" } }],
"cv": 7,
"lib": "x3-js@1.0.0"
}- Pageviews are cumulative. Each send carries the complete current state with a higher
seq; the server applies it only whenseqis newer. Lost or duplicated sends therefore never corrupt data. ageis milliseconds since the pageview started; the server computes the start time from its own clock, so wrong client clocks do not matter.sidis a random id kept in memory (or insessionStorageafter consent).fresh: truetells the server that the id was created on this document load, so it may continue the visitor's session by the daily salted hash instead – see Cookieless sessions.attrholds up to 13 attribution keys (six UTMs, seven click ids). Click ids are kept complete for conversion uploads.- Events (
ev) reference a pageview of the same batch;pv+nis the de-duplication key. id(identify) contains SHA-256 hashes only – plaintext contact data never travels through the public endpoint.ac(_fbp,_fbc,_ttp) is only present after consent.- A batch is at most 16 KB and 1–5 pageviews; the SDK shrinks oversized state.
The exact zod description lives in @3x/core (schemas/wire.ts); the server validates with a
clamping validator that drops what it does not understand instead of rejecting the batch.
Processing order
- Body size (413) → JSON (400) → key format and checksum (400) → key lookup (403).
- Proxy trust: a valid
x-3x-proxy-keylets the request carry the visitor's IP and country inx-3x-client-ip/x-3x-client-country; otherwise the IP comes from the socket or the deployment's trusted headers (Environment). - Origin must be an allowed origin of the project (403).
- Bots (user agent) and zero-viewport clients are acknowledged with 204 and not stored.
- Validation and the event-name cardinality guard; blocked event names are dropped.
- Rate limit per IP (429), database availability, project quotas and daily budgets – all of them answer 204 when they skip writing.
- The visitor hash
sha256(daily salt ‖ ip ‖ ua ‖ project)is computed in memory only; IP and user agent are never stored. Salts are deleted daily. - One transaction writes sessions, pageviews, events, video plays and identify leads, then derives flags, goals and the relevance score.
- In the background: conversion triggers, the event catalog, cleanup and retries.
Debugging
Outside production the API adds an x-3x-debug header with the session, stitch kind and whether
each pageview state was applied. In production use the dashboard's live view, GET
/v1/projects/{project}/events/live or the MCP tool events_tail.