CRM integrations

ClickStream connects to your CRM in two independent directions. Export pushes identified visitors out to your CRM as contacts. Identity matching pulls your own contacts in to recognize returning customers among your first-party visitors — a free first-party feature that never calls a data vendor. Both are configured per site.

Where you set this up. CRM connections live in the per-site integrations panel: open Sites, select a site, then the integrations panel (apps/dashboard/src/components/site-config/integrations-panel.tsx). Credentials are write-only — accepted when you save, encrypted at rest with the per-site key, and never echoed back. Clients only ever see whether a field is configured plus a short masked hint.

The five providers

ProviderCredentials you supply (CRM_CREDENTIAL_FIELDS)
HubSpotaccessToken — a private-app access token
SalesforceinstanceUrl, clientId, clientSecret — a connected app
PipedriveapiToken, companyDomain
ZohoclientId, clientSecret, refreshToken, accountsDomain, apiDomain
Webhookurl, secret — records POSTed and signed with an HMAC

Field names are from packages/shared-types/src/integrations.ts:358. Every value is AES-256-GCM encrypted per site; only the last few characters of one non-secret field per provider are ever rendered as a "configured" hint (CRM_CREDENTIAL_HINT_FIELD, integrations.ts:371).

Connecting each provider

Webhook requests carry X-ClickStream-Signature: sha256=<HMAC-SHA256(secret, exact raw body)> and a stable Idempotency-Key: clickstream-crm-<hex HMAC>. Verify the signature before parsing the JSON, then persist and deduplicate the idempotency key before applying the batch. ClickStream reuses the same key when a timeout, rate limit, or server error makes a retry necessary, so receivers can provide exactly-once effects despite at-least-once delivery.

What gets sent

Records are built from a fixed whitelist (apps/dashboard/src/lib/crm/engine.ts). Every exported contact carries:

sensitive.* enrichment fields are never sent. Income, net worth, street address, precise lat/long, and raw hashed identifiers are stripped by a record-safety scan on the way out (CRM_RECORD_FORBIDDEN_KEY_TOKENS, integrations.ts:319). Visitors flagged do_not_sell or processing_restricted are excluded before any record is built. You may additionally map a small allowlist of DEFAULT-tier enrichment fields (name, company, job title, industry, city/state/zip, age, gender, marital status, home ownership) into CRM property names of your choice (CRM_MAPPABLE_ENRICHMENT_FIELDS, integrations.ts:337).

Plan gates and caps

Cost is protected in code — there are no CRM-specific Stripe meters. Auto-exported records are capped monthly per plan (CRM_EXPORT_MONTHLY_CAPS, integrations.ts:388):

PlanAuto syncMonthly auto cap
Hobby / Free0 (unavailable)
GrowthManual only0 auto
ScaleYes10,000
NetworkYes100,000
Enterprise (custom)Yes1,000,000

Manual vs auto sync

An inactive or past-due subscription degrades to the free tier and loses the feature — the gate fails closed.

CRM identity matching (free)

Separately from export, you can connect your CRM (or upload a CSV) so ClickStream recognizes your existing customers among your first-party visitors. This runs entirely inside your own per-tenant identity graph (HMAC-isolated), matching on email and phone. It never calls a third-party enrichment vendor and is never billed against the enrichment add-on — it is a free first-party feature (apps/dashboard/src/lib/crm/match-access.ts).

Identity matching is gated to paid tiers, Growth and above (CRM_MATCH_PLANS), with its own per-plan monthly synced-contact cap (CRM_MATCH_MONTHLY_CONTACT_CAPS). The same five API providers work, plus a universal CSV upload path so any CRM can be used. A hard separation invariant keeps this path out of the enrichment billing code entirely.

See also