The Ashfort Intelligence pixel
A small (~4 KB gzipped), asynchronous script that records page views and the events you choose, so Ashfort Intelligence can show company-level interest. It never blocks rendering, never reads form fields, and ships no secrets to the browser.
Install
Create a website in the dashboard, add the domains allowed to send data, then copy the snippet from its setup screen — it contains your real site ID and this deployment's script host. Paste it inside <head> on every page:
<script async src="https://ashfortintelligence.com/pixel.js" data-site-id="YOUR_SITE_ID"></script>To call the API before the script finishes loading, add the optional one-line queue stub first: window.ashfort=window.ashfort||function(){(ashfort.q=ashfort.q||[]).push(arguments)};
Script attributes
data-site-id(required) — public site ID from the setup screen (af_…).data-consent—required(wait forashfort("consent","granted")) orgcm(follow Google Consent Mode). Omit when your review concludes consent isn't needed. Must match the website's setting — the server enforces it.data-endpoint— collector URL, only needed when the script is served from a different host than the app (e.g. a CDN).data-storage="none"— never write to localStorage/sessionStorage; IDs live only for the page view (repeat-visit signals are lost).data-spa="false"— disable automatic route-change tracking.data-auto-pageview="false"— send page views manually withashfort("page").data-debug— log what the pixel does to the console.
Custom events
Use events for actions that signal buying intent. Names are 1–64 characters of letters, numbers and _ . : -. Up to 20 flat properties (string, number, boolean) are kept; keys or values that look like personal, credential or payment data are dropped on both the client and the server.
// Track a custom event (name: letters, numbers, _ . : -)
ashfort("track", "demo_request", { plan: "growth" });
ashfort("track", "pricing_interest");
// No-code: add data attributes to any button, link or form
<button data-ashfort-event="demo_request" data-ashfort-prop-plan="growth">Book a demo</button>
<form data-ashfort-event="contact_sales">…</form> <!-- field values are never read -->
// Consent (for data-consent="required")
ashfort("consent", "granted"); // or "denied" — clears the pixel's storageEvent names listed under Settings → Scoring (defaults: demo_request, contact_sales, trial_signup, pricing_interest, plan_selected) feed the intent score.
Consent mode
With data-consent="required", events are held in memory — nothing is sent and nothing is stored — until you grant consent. Wire it to your consent banner:
<script async src="https://ashfortintelligence.com/pixel.js" data-site-id="YOUR_SITE_ID" data-consent="required"></script>
<script>
// OneTrust: "C0002" = performance/analytics category
function OptanonWrapper() {
var ok = (window.OnetrustActiveGroups || "").indexOf("C0002") > -1;
ashfort("consent", ok ? "granted" : "denied");
}
// Cookiebot
window.addEventListener("CookiebotOnConsentReady", function () {
ashfort("consent", Cookiebot.consent.statistics ? "granted" : "denied");
});
</script>- Call
ashfort("consent", …)on every page load — the pixel does not persist consent itself. "denied"clears the pixel's storage and drops queued events.data-consent="gcm"readsanalytics_storagefrom Google Consent Mode v2 (gtag("consent", …)/ your CMP's GCM integration). If any region-scoped default deniesanalytics_storage, the pixel can't tell whether the visitor is in that region, so it waits for a consent update instead of following a general "granted" default.- Global Privacy Control and Do Not Track are always treated as opt-outs.
Single-page apps
The pixel listens to history.pushState, replaceState and popstate, and records a page view when the path changes (query-only changes are ignored). Works with React Router, Next.js, Vue Router, SvelteKit and similar.
First-party identify (optional)
If people sign in to your site or app, you may attach activity to someone already known to you — only with a lawful basis, a matching privacy notice, and after enabling Accept first-party identify calls on the website. It is disabled by default and rejected by the server otherwise.
ashfort("identify", {
id: "user_8412", // your stable user id (required)
companyDomain: "acme.com", // optional; else derived from a business email
email: "ana@acme.com", // optional
name: "Ana Ortiz", // optional
phone: "+44 20 7946 0000", // optional
lawfulBasis: "contract" // consent | contract | legitimate_interests
});Ashfort Intelligence never infers identity from anonymous traffic and never enriches people with third-party profile data.
Known contacts & the Contacts API
Ashfort Intelligence distinguishes anonymous visits (at most matched to a company by a configured enrichment provider) from known contacts — people your organization already knows. A person's name, email or phone only ever comes from a source you control, and every contact is labelled with that source:
- Website identify —
ashfort("identify", …)on a site where you enabled it (above). - Contacts API — your backend sends people who submitted one of your consented forms. The pixel never reads form fields.
- Added by your team — entered in the dashboard with a lawful basis.
Create a key under Settings → API keys (server-side only; calls from a browser are refused):
curl -X POST https://YOUR_APP_HOST/api/v1/contacts \
-H "Authorization: Bearer $ASHFORT_API_KEY" -H "Content-Type: application/json" \
-d '{"externalId":"form-123","name":"Ana Ortiz","email":"ana@acme.com",
"lawfulBasis":"consent","consentedAt":"2026-09-23T10:15:00Z",
"sourceDetail":"Demo request form","visitorId":"<from ashfort.getVisitorId()>"}'
curl -X DELETE https://YOUR_APP_HOST/api/v1/contacts/form-123 -H "Authorization: Bearer $ASHFORT_API_KEY"lawfulBasis is required. To connect a submission to earlier visits, read ashfort.getVisitorId() in the page (it returns null until consent allows storage) and send it along with your form to your backend. Deleting a contact also deletes the activity linked to it.
Optional fields, all supplied by you and shown with their source on Raw traffic: jobTitle and employer; doNotContact (once true, later calls can't clear it); and marketingOptIn — send true only when the person opted in to marketing (accepting your terms is not an opt-in). With self-reported demographics enabled with a stated purpose under Settings → Privacy & data, you may also send ageRange and gender as the person reported them; they are shown only in aggregate. Anything else — income, net worth, homeownership, marital or family status, personal phones, home addresses — is ignored and listed back as ignoredFields.
Raw traffic & CSV export
Raw traffic (in the sidebar and the header) lists every event the collector actually received for your organization, one row per event, newest first, with search, filters and a CSV export of the filtered rows. It is an audit view: every cell is filled only from an authorized source, and a dash means nothing was supplied.
| Column | Filled from |
|---|---|
| Received, event, page, referrer, campaign, website | The pixel |
| Company, domain, match provider · confidence · time | A configured enrichment provider, a business domain you supplied, or an authorized identity provider's employer — recorded per visit |
| Name, business email, business phone, employer, job title, person source | Only a known contact from an authorized source (your identify calls, the Contacts API, your team, or a licensed identity provider you contracted and authorized); each field shows its own source |
| Age range, gender | Never per person. Shown only as aggregates on Overview when self-reported demographics are enabled with a stated purpose |
| Network | Coarse country from your hosting provider's header; device and browser from the user agent. No city or address is derived from an IP |
Exports use the same filters, include a *_source column for every person field plus do_not_contact and marketing_opt_in, are capped at 50,000 rows, neutralise spreadsheet formulas and are written to the organization's audit log. The export endpoint requires a signed-in member of the organization; there is no API-key access to it.
Traffic sources & geography
Each session is attributed to a channel from its landing page view: UTM medium first (paid, email, social), then the referrer (organic search, social, email, other referral), otherwise Direct. Tag campaigns with standard utm_* parameters; they are the only query parameters the pixel keeps.
Geography is country level only. It comes from your hosting provider's header (Vercel x-vercel-ip-country, Cloudflare cf-ipcountry) or, when the operator configures one, a local MaxMind GeoLite2 Country database. City and coordinates are never stored, and country storage can be switched off per organization.
Age & gender
Ashfort Intelligence does not infer a visitor's age or gender, and does not buy it from data brokers. Two opt-in sources can populate the aggregate Audience demographics panel:
- Google Analytics 4: connect your property under Settings → Integrations. Google provides age brackets and gender only with Google Signals enabled, anonymised and thresholded.
- Self-reported: if people give you an age range or gender themselves (for example on a consented form), and you enable it under Settings → Privacy & data, send it with identify:
ashfort("identify", {
id: "user_8412",
ageRange: "25-34", // or a whole-number age; stored only as a range, under-18 discarded
gender: "female" // female | male | non_binary | other | prefer_not_to_say
});Values are never shown for an individual. Charts need at least 10 people in the selected range and hide any group smaller than 5. Custom event properties named like age or gender are always dropped.
Household and personal attributes — income, net worth, homeownership, marital or family status, personal phone numbers and home addresses — are not accepted as lead fields and are never inferred. Identity providers can't supply age or gender either.
Identity resolution (licensed providers)
A pixel records activity; on its own it never reveals who someone is. If your organization contracts a licensed identity-resolution provider, an admin can add it under Settings → Integrations — with the contract reference and date, the intended use, the lawful basis and an attestation that the contract authorizes matching your website visitors for that use. Then switch it on per website. Test traffic is never sent, each browser is looked up at most once a day, and the pixel sends nothing for visitors with Global Privacy Control or without required consent.
Your provider (or a small middleware) implements this contract:
POST <your endpoint>
Authorization: Bearer <provider API key>
{ "requestId": "…", "ip": "198.51.100.23", "userAgent": "…",
"page": { "host": "www.example.com", "path": "/pricing" },
"occurredAt": "2026-09-23T14:05:00Z", "intendedUse": "<your stated use>" }
→ { "matched": true, "confidence": 0.86, "reference": "…",
"person": { "name": "…", "businessEmail": "…", "businessPhone": "…",
"jobTitle": "…", "employer": { "name": "…", "domain": "…" } } }Only those business fields are kept (free-mail addresses aren't business email); anything else is discarded and counted. Matches below your minimum confidence are discarded. Errors and timeouts are logged and shown; nothing is created. Match rate, field coverage, fields per match and latency are shown only from at least 20 of your own lookups, with the method.
Disclosures & permissions
Settings → Privacy & data generates notice text from your live configuration — what the pixel collects, why, which providers receive it, retention, and how visitors exercise their choices. Each website can also publish a public /disclosure/<site id> page to link from your privacy notice. Keep acceptance of your terms separate from tracking permission and marketing opt-ins; a website visit is never permission to contact someone.
AI intelligence
The AI intelligence panel on Overview summarises the selected range and traffic view: visitors, sessions, matched companies, top accounts and signals, channels, pages and pipeline. It is generated from aggregates and company-level facts only — no visitor IDs, contact details or per-person traits are ever sent to a model.
- Rule-based insights are always available and restate the numbers on the page.
- When the operator sets
ANTHROPIC_API_KEY, Claude (INSIGHTS_MODEL, defaultclaude-opus-5) writes the summary from the same aggregates, with a structured output schema and instructions not to invent figures, identify people or promise uplift. Without a key the panel is labelled AI not configured. - Results are cached per organization, range and view for six hours; Refresh regenerates on demand (20 per hour per organization, audit-logged). The footer shows when it was generated and how many events it covers.
- With no events in the view, the panel shows setup guidance instead of insights.
Testing & debugging
- Sandbox: on the website setup screen, “Open sandbox test” loads the real script with a signed one-hour token and shows the collector's responses.
- Live site: open any page with
?ashfort_test=1. The pixel sends a page view and apixel_testevent flagged as test and logs the server's reply in the console. The setup screen updates within seconds. - Debug logging: add
?ashfort_debug=1or thedata-debugattribute. - Common errors:
origin_not_allowed(add the domain to the website),consent_required(grant consent or change the site's consent mode),site_paused,monthly_limit_reached.
CMS & tag managers
- Google Tag Manager: Custom HTML tag on All Pages; set “Additional consent checks” to
analytics_storageif you use Consent Mode. The setup screen generates a GTM-safe loader. - WordPress: a header-code plugin (e.g. WPCode) or a
wp_headhook in a child theme. - Shopify:
layout/theme.liquidbefore</head>; optionally forward the Customer Privacy API decision toashfort("consent", …). - Webflow: Site settings → Custom code → Head code.
- Squarespace: Settings → Advanced → Code Injection → Header. Wix: Settings → Custom code → Head, all pages.
- Ad blockers may block third-party analytics hosts. Serving the script and collector from your own subdomain via a reverse proxy (set
PIXEL_SCRIPT_URL/COLLECT_URL) is supported; it does not bypass consent — the same rules apply.
Data collected
- Page path with query strings removed (except UTM tags) and ID-, token- and email-like segments redacted.
- External referrer (origin + path), UTM campaign tags.
- Random first-party visitor ID (localStorage
_ashfort_vid) and session ID (sessionStorage_ashfort_sid, 30-minute inactivity). - Device type and browser family (the full user agent is not stored), optional two-letter country from your CDN.
- The request IP is used in memory to look up a company through a configured, licensed provider and is never stored; lookups are cached against a keyed hash.
Never collected: form values, passwords, payment details, page content, keystrokes, sensitive traits. Retention is configurable per organization; see privacy.
Intent scoring
Each account's score is the sum of these signals (count × weight, capped), clamped to 100. Weights, caps, paths and event names are adjustable per organization. Defaults:
| Signal | Weight | Cap |
|---|---|---|
| Page views — Every page view from the account. | 1 | 15 |
| Repeat visits — Each session after the first. | 6 | 18 |
| High-value pages — Views of paths you mark as buying research. | 5 | 20 |
| Pricing interest — Pricing page views or pricing events. | 10 | 20 |
| Demo / sales requests — Custom events you map to demo or contact requests. | 30 | 30 |
| Multiple visitors — Each additional anonymous browser from the same account. | 5 | 15 |
| Recent activity — Active within the recency window. | 10 | 10 |
Accounts at or above the threshold (default 50) are marked qualified and suggested as leads.
Security model
- The site ID is public by design. The collector validates it, checks the request
Originagainst the website's allowed domains, enforces the consent mode, rate-limits per IP and per site, validates and size-limits payloads, and filters bots and automation. - Origin checks stop other websites from sending data from visitors' browsers; like any public analytics endpoint, they cannot stop a determined non-browser client from sending fabricated events, which is why rate limits and plan limits also apply.
- Responses contain no tenant data. Dashboard and API access is session-based with server-side tenant checks on every read and write.