Custom HTML sites
This guide covers any site where you control the HTML: hand built sites, static site generators, React, Vue and other single page apps, and server rendered frameworks. The platform guides build on it.
1. Add the script to every page
Copy the tag from Sites, your site, Install and put it in the <head> of every page, including landing pages without forms (the landing page is where the source information is):
<script src="https://cdn.sourcream.ai/t/YOUR_SITE_KEY.js" async></script>
The script is about 6 KB gzipped and loads async, so it never blocks rendering. On pages with a Typeform embed, write defer instead of async, so the script runs before Typeform reads its hidden fields (Typeform and Gravity Forms). Your site settings are built into the script, so changes you save in Sourcream reach visitors within about 10 minutes (the script is cached briefly at the edge and in browsers).
If your site sends a Content Security Policy, allow Sourcream:
script-src https://cdn.sourcream.ai; connect-src https://cdn.sourcream.ai
With the first-party proxy, 'self' already covers both.
2. Forms
With the default settings there is nothing else to do. On every page the script:
- finds every
<form>(the Form selector setting), skipping search forms, forms withmethod="get"and anything matching the Exclude selector (by default[data-sourcream-ignore], form[role="search"]); - fills any input whose
namematches a Sourcream field (first_source,last_medium,sc_visitor_idand so on, after any renames in Field names); - adds the missing ones as hidden inputs when Auto inject hidden fields is on (the default);
- watches for forms added later (pop ups, multi step forms, single page app routes) and fills those too.
The values travel with the form submission to wherever the form posts: your own backend, a form service, Zapier, a CRM form handler.
To control placement yourself, paste the hidden fields from the Install tab inside the form and turn Auto inject off:
<input type="hidden" name="first_source">
<input type="hidden" name="first_medium">
<input type="hidden" name="first_campaign">
<input type="hidden" name="last_source">
<input type="hidden" name="last_medium">
<input type="hidden" name="last_campaign">
<input type="hidden" name="sc_visitor_id">
Two other ways to mark where a value goes, useful when a form builder controls the input names:
<input type="hidden" name="field_17" data-sourcream="first_source">
<div class="gfield sc-last_source"><input type="hidden" name="input_4"></div>
An element with data-sourcream="<field>", or with the class sc-<field> on itself or on a Gravity Forms style .gfield wrapper, is filled with that field's value.
To skip a form, add data-sourcream-ignore to it.
3. Lead capture
When a form is submitted, the script refreshes the values and sends the lead to Sourcream in the background (navigator.sendBeacon), without delaying or changing the form's own submission. Sourcream then:
- looks up the visitor's durable record (first touch, last touch, journey) on our servers;
- stores the lead, counted as one tracked event;
- delivers it to every connected integration for that site.
With Capture mode set to Email (the default), the beacon includes the email address (the first type="email" input, or a field whose name contains "email"), and name, first name, last name, phone and company from fields with common names. CRMs need the email to find or create the contact. With Capture mode set to None, no form contents leave the browser; match CRM records by visitor ID instead (see the CRM guides).
Forms that never fire a submit event (some script driven forms send data from a button click), or a conversion that is not a form at all, can report the lead themselves after it succeeds:
window.Sourcream && Sourcream.lead({ email: '[email protected]', name: 'Jo Citizen', company: 'Example Pty Ltd' });
Contact details passed to lead() are only sent when Capture mode is Email.
HubSpot embedded forms that report submissions through HubSpot's onFormSubmitted callback are captured automatically.
4. JavaScript API
The script defines window.Sourcream on every page, even when something goes wrong inside it.
| Call | What it does |
|---|---|
Sourcream.read() |
{ first, last, visitorId }. first and last hold source, medium, campaign, and when present term, content, campaignId, clickIds, landingPage, referrer, ts (epoch ms). |
Sourcream.ready(fn) |
Calls fn once the values are final, after the server has confirmed the visitor. Read them with Sourcream.read(). With consent mode set to Required or Google Consent Mode, it waits until the visitor consents (Consent). |
Sourcream.fill(root?) |
Fills forms now, inside root if given. Use after rendering a form the script could not see. |
Sourcream.lead(contact?) |
Sends a lead now, as described above. |
Sourcream.consent(granted) |
Grants or withdraws consent. See Consent. |
Sourcream.visitorId() |
The visitor ID, the same value as the sc_visitor_id field. |
Sourcream.version |
Script version. |
Once values are final the script also pushes { event: 'sourcream_ready', sourcream: { first_source: ..., ... } } to window.dataLayer for Google Tag Manager (GA4 through Google Tag Manager). Like ready(), the push waits for consent: it is not made while the script runs in memory only mode.
5. What is stored in the browser
All first-party, SameSite=Lax, Secure on https, path /:
| Cookie | Lifetime | Holds |
|---|---|---|
_sc_ft |
180 days | First touch (JSON, URL encoded) |
_sc_lt |
180 days | Last non-direct touch (JSON, URL encoded) |
_sc_vid |
180 days | Visitor ID |
_sc_s |
30 minutes | Session marker, used to decide when to contact our servers |
_sc_id |
180 days | Server set, HttpOnly visitor ID. First-party proxy only. |
The script contacts our servers on a new session or a new non-direct visit, not on every page view. The answer carries the canonical first and last touch from our servers, and the script rewrites its cookies from it. That is how a first touch survives Safari deleting script cookies after 7 days.
6. Site settings
Under Sites, your site, Settings:
| Setting | Use it for |
|---|---|
| Own domains | Other hostnames you own (booking subdomain, landing page domain, staging host). Visits between them are internal, not referrals, and the server accepts data from them. Subdomains of the primary domain are already included. Subdomains and builder hosts such as *.webflow.io are free; any other domain here (acme.com next to acme.com.au, a landing page domain) counts as one more domain on your plan (details). |
| Cookie domain | Set to your apex (for example acme.com.au) when visitors move between subdomains, so one visitor carries one attribution. Never set on shared hosts such as *.webflow.io. |
| fbclid medium | cpc (default), paid_social, or social if most of your Facebook traffic is organic. |
| Field set | Core or extended (Business plan and above). |
| Field names | Rename fields to match your forms or CRM. |
| Auto inject hidden fields | Add missing hidden inputs to forms. |
| Form selector, Exclude selector | Which forms to fill. |
| Consent mode | None, Required, or Google Consent Mode. |
| Capture mode | Email or None. |
| Ignore referrers | Extra hosts to treat as direct, such as a payment provider or booking engine that sends visitors back to you. |
| Read legacy LeadAttr cookies | For sites moving from the LeadAttr script: imports existing lead_attr_first and lead_attr_last cookies and keeps window.LeadAttr.read() working. |
| Allow localhost | Accept data from localhost and 127.0.0.1 while you test. |
7. Test it
- Open a private window on your live site with
?utm_source=test&utm_medium=test&utm_campaign=install. - In the console,
Sourcream.read()returnstest / test / installas both first and last touch. - Inspect a form: the hidden fields hold the same values.
- Submit the form. Within a few seconds the lead appears under Leads with that source.
- Open another page from a Google search result and check the last touch becomes
google / organicwhile the first staystest.
Troubleshooting
Sourcream is not defined: the script did not load. Check the tag is in the published page, the key is right, and no Content Security Policy or consent tool blocks it.- Everything is
(direct)while testing: you arrived from your own site or typed the address. Use a UTM tagged link in a fresh private window. - Leads do not appear: look for a failed request to
/lin the Network tab. A 403 means the page's domain is not the site's domain or one of its own domains (add staging hosts there; turn on Allow localhost for local testing). If the account has no active plan, the script is served as an empty placeholder (/* sourcream: inactive */) and nothing is sent. - Hidden fields stay empty: the form is inside an iframe (a script on your page cannot reach it), or the inputs are rendered after the form selector stopped matching. Call
Sourcream.fill()after rendering.
Questions: [email protected]