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
| Provider | Credentials you supply (CRM_CREDENTIAL_FIELDS) |
|---|---|
| HubSpot | accessToken — a private-app access token |
| Salesforce | instanceUrl, clientId, clientSecret — a connected app |
| Pipedrive | apiToken, companyDomain |
| Zoho | clientId, clientSecret, refreshToken, accountsDomain, apiDomain |
| Webhook | url, 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
- HubSpot — create a private app in your HubSpot account with contact read/write scopes and paste its access token.
- Salesforce — create a connected app, then supply your instance URL along with its consumer key (
clientId) and secret. - Pipedrive — copy your personal/company API token and your company domain (e.g.
acmefromacme.pipedrive.com). - Zoho — register a self-client / server app, generate a refresh token, and supply it with your client id/secret and your account's data-center domains (
accountsDomain,apiDomain). - Webhook — point ClickStream at any HTTPS endpoint you control; each batch is POSTed as JSON and signed with your shared
secretso you can verify authenticity.
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:
- Base contact fields —
email(required; a record with no plaintext email is skipped, since CRMs dedupe on email),firstName,lastName,company, andphone. Phone is only sent when a consented, unmasked first-party phone exists (TCPA-blocked phones are dropped). - Behavioral properties (optional) — visit count, session count, first-seen, and last-seen, when the integration enables
includeBehavioralProperties. Neutral keys only. - The five
clickstream_*identity custom fields (optional) — for CRM-side identity resolution, written only when you both opt in (includeIdentityKeys) and accept the data-processing addendum (dpaAccepted):clickstream_id,clickstream_person_id,clickstream_hem,clickstream_hem_md5,clickstream_merged_ids(CRM_IDENTITY_PROPERTY_KEYS,integrations.ts:300). These are your own tenant's keys — cross-tenant per-site hashes (tenant_hem/hmac_*) are hard-banned regardless of any flag.
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):
| Plan | Auto sync | Monthly auto cap |
|---|---|---|
| Hobby / Free | — | 0 (unavailable) |
| Growth | Manual only | 0 auto |
| Scale | Yes | 10,000 |
| Network | Yes | 100,000 |
| Enterprise (custom) | Yes | 1,000,000 |
Manual vs auto sync
- Manual export is available on Growth and above (
CRM_MANUAL_EXPORT_PLANS). Run the whole eligible set on demand withPOST /api/sites/[id]/integrations/[integrationId]/export-run. - Auto sync requires a non-zero auto cap, i.e. Scale and above. Enable
autoExporton the item and the incremental sync runs on thePOST /api/cron/crm-exportschedule, exporting only what changed since the last watermark. - Per-visitor send pushes a single identified person on demand via
POST /api/visitors/[id]/crm-export(a manual run, so Growth+). Manual pushes still count toward the same monthly budget.
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
- Optional enrichment — how ClickStream attaches visitor detail to an ID
- Signals API — reading identity status in page code
- Privacy & compliance — consent, do-not-sell, and DPA model