HubSpot Trial Attribution

HubSpot Trial Attribution #

Classification: Internal-Only

HubSpot attributes a contact’s traffic source (paid, organic, email, …) from the hubspotutk visitor cookie its tracking script sets on www.gotofu.com. That history only attaches to a contact when the visitor converts through a HubSpot form. The trial CTA sends visitors to Clerk’s hosted accounts.gotofu.com/sign-up, where no script can run, so without this integration every trial contact showed up unattributed (ENG-7499).

How it works #

  1. www.gotofu.com sets hubspotutk for the whole .gotofu.com domain.
  2. The user signs up on the Clerk portal and lands on app.gotofu.com/onboarding.
  3. The webapp loads the same HubSpot tracking script (HubSpotTracking in apps/webapp/src/shared/components/hubspot-tracking.tsx), so the session continues into the app.
  4. On the first onboarding visit of a self-serve variant (full or prefilled), useHubSpotTrialAttribution POSTs to /api/onboarding/hubspot-attribution.
  5. The route reads hubspotutk from the request cookies, looks up the user’s email in Clerk and submits HubSpot’s hidden trial-signup form through the Forms API with context.hutk. HubSpot then merges the visitor history into the new contact.
  6. On success the route stamps hubspot_trial_attributed_at on the user’s Clerk publicMetadata, so the form is never submitted twice for one user.

Sales-led onboarding (entities-only) and the OAuth return leg (integrations-only) never submit.

Cookiebot runs in auto-blocking mode. The HubSpot script is tagged data-cookieconsent="marketing" everywhere except /onboarding and /sign-up, where it is ignore. That exemption is the agreed policy for the trial flow: the source has to be tied to the account before the user has answered the banner. After onboarding the marketing category governs it again, and the user can change it under Account → Cookie Consent.

Cookiebot only blocks scripts that have not loaded yet, so a “reject” after onboarding is also pushed into HubSpot directly: HubSpotTracking listens to the Cookiebot events and, when marketing is refused, queues doNotTrack and revokeCookieConsent (which deletes hubspotutk and the other HubSpot cookies). Opting back in queues doNotTrack with track: true.

The attribution route honours an earlier refusal too: if the request carries Cookiebot’s CookieConsent cookie with marketing:false, it answers { status: "skipped", reason: "marketing_consent_declined" } and never calls HubSpot.

Configuration #

Variable Where Purpose
NEXT_PUBLIC_HUBSPOT_PORTAL_ID webapp build Portal whose tracking script to load. Unset disables the script.
HUBSPOT_PORTAL_ID webapp runtime Portal for the Forms API submission.
HUBSPOT_TRIAL_SIGNUP_FORM_ID webapp runtime GUID of the hidden trial-signup form.
HUBSPOT_FORMS_API_BASE_URL webapp runtime, optional Defaults to https://api-eu1.hsforms.com (EU portal).

If either runtime variable is unset the route answers { status: "disabled" } and logs a warning, so a non-production environment never writes to the marketing portal.

Troubleshooting #

  • Contact created but unattributed: the response logged hasVisitorCookie: false. The cookie was missing on the request, usually because Cookiebot on www.gotofu.com blocked HubSpot before the user clicked the CTA, or the user came straight to the signup URL.
  • HubSpot trial signup submission failed in the logs: the status and HubSpot’s body are attached. A 400 with INVALID_EMAIL or a missing required field points at the form definition in HubSpot.
  • Nothing submitted: check the user’s Clerk publicMetadata for hubspot_trial_attributed_at; a stamped user is skipped by design. A marketing_consent_declined response means the browser already carried a Cookiebot refusal, also by design.