Webflow is a popular no-code website builder for marketing teams. HubSpot is a CRM and marketing hub. Native integration looks simple: install the HubSpot Tracking Code on the Webflow site, connect Webflow Forms to HubSpot — leads flow into the CRM. The problem is in the details: the native integration via the standard HubSpot embed loses UTM attribution, does not work with custom Webflow forms, and does not support progressive profiling. Here is exactly what breaks and how to build it correctly.
How the native integration works (and what is wrong with it)
Step 1: Add the HubSpot Tracking Code to <head> in Webflow. This sets the hsq cookie and begins session tracking.
Step 2: Webflow Forms -> Integrations -> connect HubSpot. When a form is submitted, Webflow POSTs to its own backend -> Webflow passes data to HubSpot through the built-in integration.
What is lost:
1. UTM parameters and source attribution. The native Webflow-HubSpot integration does not pass UTM parameters to HubSpot Contact Properties. A contact is created, but hs_analytics_source, utm_source, utm_campaign — are empty or show “Direct Traffic.” The marketing team does not know which campaign the lead came from.
2. Custom Webflow forms. The native integration only works with standard Webflow Form elements. A custom JS form (React, Alpine.js, custom fetch) — cannot be connected through the native integration.
3. Progressive profiling. HubSpot Progressive Profiling fills in different fields on repeat visits. The native Webflow integration does not support this: it always requests the same fields.
4. File upload in forms. Webflow Forms supports file upload. The native HubSpot integration does not pass files — only text fields.
The correct architecture: HubSpot Forms API + JS SDK
Instead of the native integration — submit data directly to the HubSpot Forms API on submit:
// Correct approach: custom submit handler
// Connect HubSpot Tracking Code in <head> - required for cookie
const HUBSPOT_PORTAL_ID = "YOUR_PORTAL_ID";
const HUBSPOT_FORM_GUID = "YOUR_FORM_GUID"; // from HubSpot -> Marketing -> Forms -> Details
async function submitToHubSpot(formData) {
// Get UTM from URL
const params = new URLSearchParams(window.location.search);
const utmSource = params.get("utm_source") || "";
const utmMedium = params.get("utm_medium") || "";
const utmCampaign = params.get("utm_campaign") || "";
const utmContent = params.get("utm_content") || "";
const utmTerm = params.get("utm_term") || "";
// Get HubSpot cookie for identity resolution
const getCookie = (name) => {
const match = document.cookie.match(new RegExp("(^| )" + name + "=([^;]+)"));
return match ? match[2] : "";
};
const hutk = getCookie("hubspotutk");
const payload = {
portalId: HUBSPOT_PORTAL_ID,
formGuid: HUBSPOT_FORM_GUID,
fields: [
{ name: "email", value: formData.email },
{ name: "firstname", value: formData.firstName },
{ name: "lastname", value: formData.lastName },
{ name: "company", value: formData.company || "" },
{ name: "phone", value: formData.phone || "" },
// UTM parameters as HubSpot contact properties
{ name: "utm_source", value: utmSource },
{ name: "utm_medium", value: utmMedium },
{ name: "utm_campaign", value: utmCampaign },
],
context: {
hutk: hutk, // for session stitching
pageUri: window.location.href,
pageName: document.title,
},
legalConsentOptions: {
// GDPR consent (if required)
consent: {
consentToProcess: true,
text: "I consent to the processing of my personal data.",
},
},
};
const resp = await fetch(
`https://api.hsforms.com/submissions/v3/integration/submit/${HUBSPOT_PORTAL_ID}/${HUBSPOT_FORM_GUID}`,
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload),
}
);
if (!resp.ok) {
const err = await resp.json();
throw new Error(JSON.stringify(err));
}
return await resp.json();
}
// Bind to Webflow form submit
document.querySelector("#contact-form").addEventListener("submit", async (e) => {
e.preventDefault();
const fd = new FormData(e.target);
try {
await submitToHubSpot({
email: fd.get("email"),
firstName: fd.get("first-name"),
lastName: fd.get("last-name"),
company: fd.get("company"),
phone: fd.get("phone"),
});
// Show success state
document.querySelector("#form-success").style.display = "block";
e.target.style.display = "none";
} catch (err) {
console.error("HubSpot submit error:", err);
}
});
UTM persistence: preserving parameters across pages
UTM parameters are lost on navigation if not saved. The correct approach — save to sessionStorage on the first visit:
// Save UTM when loading the first page
(function saveUtm() {
const params = new URLSearchParams(window.location.search);
["utm_source","utm_medium","utm_campaign","utm_content","utm_term"].forEach(k => {
if (params.get(k)) {
sessionStorage.setItem(k, params.get(k));
}
});
})();
// On submit - read from sessionStorage
function getStoredUtm() {
return {
utmSource: sessionStorage.getItem("utm_source") || "",
utmMedium: sessionStorage.getItem("utm_medium") || "",
utmCampaign: sessionStorage.getItem("utm_campaign") || "",
};
}
Server-side option: Webflow Webhooks + Python
For a more reliable architecture (especially when server-side logic is needed):
# Webflow Webhook -> your Python endpoint -> HubSpot API
import requests
HUBSPOT_TOKEN = "your_private_app_token"
@app.route("/webhooks/webflow-form", methods=["POST"])
def webflow_form():
data = request.json
# data contains form fields from Webflow
# POST to HubSpot Contacts API with upsert by email
resp = requests.post(
"https://api.hubapi.com/crm/v3/objects/contacts/upsert",
headers={"Authorization": f"Bearer {HUBSPOT_TOKEN}"},
json={
"properties": {
"email": data.get("email"),
"firstname": data.get("firstName"),
"lastname": data.get("lastName"),
"utm_source": data.get("utm_source", ""),
"utm_campaign": data.get("utm_campaign", ""),
},
"idProperty": "email",
},
)
resp.raise_for_status()
return "", 200
Server-side advantage: UTM is passed explicitly in the payload, no dependency on browser cookies.
Approach comparison
| Approach | UTM attribution | Custom forms | Progressive Profiling | Complexity |
|---|---|---|---|---|
| Native integration | No | No | No | Minimal |
| HubSpot Forms API (JS) | Yes | Yes | Yes (via HubSpot form settings) | Medium |
| Server-side webhook | Yes | Yes | No | Higher |
Real-world case
SaaS (US, Webflow + HubSpot, 200 leads/month via forms):
- Problem: 80% of HubSpot contacts had no source. The marketing team could not attribute conversions to campaigns. $15k/month on Google Ads — without ROI data.
- Native integration: passed name and email, but not UTM. HubSpot Source = “Direct Traffic” for all forms.
- After Forms API with UTM persistence: every lead has
utm_source,utm_campaign. Google Ads campaign ROI is visible directly in HubSpot Reports. Finding: the Brand campaign (30% of budget) had a 3x better close rate than Non-Brand.
Who this is relevant for
- Teams using Webflow + HubSpot where marketing complains about “Direct Traffic” in sources
- SaaS with custom Webflow forms (React, Alpine.js) that do not work with native integration
- Companies with EU audiences where GDPR consent in forms is critical — the native integration does not correctly pass consent data
- Marketing teams running paid ads that need attribution all the way to closed deals
Frequently asked questions
Does the HubSpot Tracking Code still need to be installed?
Yes, even with direct Forms API. HubSpot Tracking Code creates the hubspotutk cookie for session stitching — it links an anonymous session to a contact. Without it, context.hutk will be empty and HubSpot cannot associate page views with form submissions.
Does HubSpot Forms API require a Private App token or is Portal ID sufficient?
Forms API v3 (/submissions/v3/integration/submit/) does not require authorization — only Portal ID and Form GUID. This is a public endpoint (the form is filled in by the client in a browser). Private App token is required for the Contacts API (server-side).
Webflow Logics vs custom JS — which is better?
Webflow Logics (no-code automations) — a new Webflow product. It has a HubSpot connector, but with the same limitations as the native integration: no UTM, limited field mapping. For full control — custom JS or server-side webhook.
Can a file upload from Webflow be passed to HubSpot?
HubSpot Forms API does not support file upload directly. Workaround: upload the file to the HubSpot Files API (POST /filemanager/api/v3/files/upload), get the URL -> pass the URL as a text field in the form. Requires a server-side proxy.
Summary
- Native Webflow + HubSpot integration: works only for basic forms without UTM and custom logic
- The correct path: HubSpot Forms API v3 + UTM persistence in sessionStorage
hubspotutkcookie is required for session stitching — install HubSpot Tracking Code- Server-side option (Webflow Webhook -> Python -> HubSpot Contacts API) — for reliability and complex business logic
- UTM attribution through to a closed deal — impossible without the correct integration
If you have HubSpot + Webflow and leads arrive without a source — describe your form structure and current integration. Exceltic.dev will configure Forms API with complete UTM attribution.