POST /api/collect receives the consent-gated session, lead and contact click events the snippet sends (ADR-021, ADR-041). Schema: /schemas/collect.v1.json.
Request
- Body: JSON, sent as
text/plainsonavigator.sendBeacon/fetch(keepalive)need no CORS preflight. Maximum 8 KB and 10 events per request. - No authentication: the site ID is a public identifier. Abuse reduction only: the
Origin/Refererhost must match a registered domain (same rule as the config endpoint), the site must be enabled (active, subscription servable), the per-sitecollectflags must allow the event type, and requests are limited to 300/min per IP (TRACELEAD_COLLECT_RATE_LIMIT). A coarse user-agent screen drops obvious crawlers; the user agent is not stored.
{
"schemaVersion": 1,
"siteId": "tl_k3j9x2m8n4p7q1r5s6t0",
"snippetVersion": "1.1.0",
"events": [
{
"type": "pageview",
"vid": "9f1c…32 hex…", "sid": "0a7e…32 hex…",
"ts": "2026-10-09T14:02:11Z",
"path": "/pricing",
"touch": {
"channel": "paid_search", "source": "google", "medium": "cpc",
"utm_campaign": "spring-sale", "gclid": "…",
"landing_page": "/pricing?utm_source=google&utm_medium=cpc",
"first_touch_at": "2026-10-09T13:58:40Z"
}
},
{ "type": "lead", "vid": "…", "sid": "…", "ts": "…", "path": "/contact", "formId": "contact-form", "touch": { "…": "…" } },
{ "type": "contact", "vid": "…", "sid": "…", "ts": "…", "path": "/contact", "kind": "phone", "touch": { "…": "…" } }
]
}
vid— random visitor id the snippet mints after consent and keeps in first-party storage for the attribution lifetime.sid— random session id, renewed after 30 minutes idle.path— pathname only; any query or fragment is discarded server-side.touch— the flattened first-touch fields (the same map written into hidden inputs). Unknown keys are ignored;gclid/gbraid/wbraid/msclkid/ttclid/li_fat_id/twclid/fbclidvalues are only used to derive which click id was present.last_*keys (optional last touch, ADR-026) are ignored: the dashboard is first-touch only.formId— lead events only: the submitted form'sidornameattribute, or the id page code passed totracelead('lead', formId)for a form inside an iframe (≤ 100 chars). Never field values.kind— contact events only (ADR-041):phone,email,smsorwhatsapp, from a click on atel:,mailto:,sms:,wa.meorapi.whatsapp.comlink or fromtracelead('contact', kind). Sent only when the site config hascollect.contacts: true. The link's number, address or message is never read past its scheme or host and never sent; any other key on the event (anhref, anumber) is ignored. See Contact clicks.
Response
Always 204 No Content with Access-Control-Allow-Origin: * and Cache-Control: no-store — for stored events, dropped events, unknown sites, malformed bodies and internal failures alike. The customer page never sees an error and nothing about tenants can be probed. OPTIONS /api/collect answers the same way with Access-Control-Allow-Headers: Content-Type.
What is stored
| Table | Row | Columns |
|---|---|---|
visitor_sessions |
one per (site, sid) |
visitor id, session id, started/last seen, pageview count, entry path, up to 30 pathnames, converted flag, attribution snapshot, snippet version |
lead_events |
one per lead event (duplicates for the same visitor + form within 2 s collapse) | visitor id, session link, time, form id, page path, attribution snapshot, snippet version, and on sites with engagement counters sessions_before_lead (1–10000) and days_to_lead (0–400) |
contact_clicks |
one per (site, sid, kind): repeats in a visit collapse |
visitor id, session id and link, time, kind, page path, attribution snapshot, snippet version. Not a lead: it never marks the session converted or changes a lead count |
Engagement counters (ADR-034): two whole numbers the snippet computes in the browser and sends with lead events only, stored only when the site has them on; values outside the ranges are dropped. A beacon whose touch contains ga_client_id is rejected outright (still 204) and logged as a rejection with no value: the GA client id may be filled into a form but never sent here.
Attribution snapshot: channel, source, medium, utm_campaign, utm_term, utm_content, utm_id, click_id_type, referrer_host, landing_page (sensitive parameters removed again server-side), first_touch_at.
Never stored: names, emails, phone numbers, any form value, full URLs, fragments, IP addresses, user agents, click-id values. Timestamps more than an hour old or five minutes in the future are replaced by the server clock.
Retention
php artisan tracelead:purge runs daily: sessions, leads and contact clicks older than the site's data_retention_days (default 395, 30–730) are deleted, and all collected data of a team soft-deleted for more than TRACELEAD_DELETED_TEAM_RETENTION_DAYS (90) days is removed. After the same period the closed organisation itself is deleted (team, sites, settings, audit trail, subscriptions, report links, webhooks, invitations, API tokens); invoices stay in Stripe. --dry-run reports counts only.
Install health
Successful config fetches from allowed pages set sites.last_config_fetched_at; stored events set last_event_at and last_snippet_version. Both are throttled to one write per site per minute and hold no request data.