Docs

Identify and leads

A lead is a person linked to a session – the bridge between anonymous analytics and your CRM. 3x offers two ways to create leads, and both keep personal data minimal.

identify() in the browser

import { identify, getIds } from "@3x-engine/sdk";

form.addEventListener("submit", async () => {
  await identify({
    email: form.email.value,
    firstName: form.firstName.value,
    form: "contact",
    traits: { plan: "pro", employees: "11_50" },
  });
});
  • E-mail, phone, first name, last name and external id are normalized and hashed with SHA-256 in the browser (WebCrypto). Only hashes travel; the API never sees plaintext from identify().
  • Phone numbers: digits with country code; 00 prefixes and (0) trunks are handled; national numbers get the project country's calling code, and are dropped when the project has no country.
  • E-mail: lowercased; an additional Google variant (dots removed for Gmail) is hashed for Google Ads.
  • traits: up to 20 non-personal attributes; values that look like e-mail addresses or phone numbers are dropped.
  • Sent only with consent unless the project sets tracking.identify.requireConsent = false (decide with your DPO).
  • One lead row per session and identity: calling identify() repeatedly updates the same row.

The session gets identified = true, goals of kind lead fire, the relevance model adds its identified points, and conversion definitions with a lead trigger record a conversion (subject to the web guards).

Leads from your server

When the form posts to your backend anyway, create the lead there with a secret key – this is the most robust path and the only one that can store plaintext:

import { createX3Server } from "@3x-engine/sdk/server";

const x3 = createX3Server({ apiUrl: process.env.X3_API_URL!, apiKey: process.env.X3_SECRET_KEY! });

await x3.leads.create({
  sessionId,                      // from getIds() in the browser
  form: "contact",
  email, firstName, lastName, company,
  message,                        // stored only when the project stores plaintext
  answers: { "pricing-quiz": { team_size: "11_50" } },
  summary: "Pro plan, 11–50 seats, wants a demo",
  spam: honeypotFilled,           // spam leads are kept but excluded everywhere by default
  conversion: { definition: "lead", eventId },
});
FieldNotes
email, phone, firstName, lastName, externalIdalways hashed; plaintext e-mail, names and company kept only with settings.pii.storePlaintext
company, messageplaintext only with storePlaintext; message is never returned by MCP
answers{questionnaire: {question: option}}
score, grade, summaryyour qualification
spamhoneypot result; excluded from lists, MCP, goals, identified and lead triggers unless include_spam=true
attributionfor leads without a session
conversionalso record a conversion of this definition

Requires the scope leads:write. The request is idempotent with an Idempotency-Key header.

Reading and erasing leads

  • Dashboard Leads (members with the personal-data permission), CSV export.
  • GET /v1/projects/{project}/leads and /leads.csv (leads:read), MCP leads_list (never phone numbers or free text).
  • DELETE /v1/projects/{project}/leads/{id} erases a lead and the hashes on its conversions.
  • settings.pii.leadsRetentionDays deletes leads automatically after N days.