Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
78 changes: 78 additions & 0 deletions conversions/hubspot/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
# HubSpot + Dub Conversion Tracking Demo

This is an example of how to track [HubSpot](https://www.hubspot.com/) form submissions and meeting bookings as [lead conversion events on Dub](https://dub.co/docs/conversions/leads/introduction).

Useful for tracking SaaS signups, contact-us form submissions, and sales meeting bookings back to the referral link that drove them.

## Files

| File | Description |
| -------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| [`index.html`](./index.html) | Embeds a **HubSpot Form** on the page. When the form is ready, the `dub_id` click ID is read from the cookie set by `@dub/analytics` and written into a hidden form field so HubSpot forwards it to Dub on submission. |
| [`meeting.html`](./meeting.html) | Embeds the **HubSpot Meetings (scheduler) widget**. It listens for the `meetingBookSucceeded` `postMessage` from the iframe and then calls `dubAnalytics.trackLead()` with the booked contact's details. |

## How it works

Both flows rely on the [`@dub/analytics` client-side script](https://dub.co/docs/sdks/client-side/introduction) setting a `dub_id` cookie when a visitor lands on your site via a Dub referral link.

- **Forms flow (`index.html`)**: listens for HubSpot's `hs-form-event:on-ready` event and populates a hidden field (with property name `dub_id`) on the form. Dub's HubSpot integration picks up the click ID from the submission and attributes it as a lead.
- **Meetings flow (`meeting.html`)**: listens to `window` messages from `https://meetings.hubspot.com`, and when a meeting is successfully booked, calls `dubAnalytics.trackLead()` with the contact's name and email in `deferred` mode.

## Changes needed to make it dynamic

The files contain hardcoded IDs/domains for the demo. Replace the following before using them in your own project:

### `index.html`

1. **HubSpot portal ID** – update the script src and `data-portal-id` with your own portal ID:
```html
<script
src="https://js.hsforms.net/forms/embed/<YOUR_PORTAL_ID>.js"
defer
></script>
... data-portal-id="<YOUR_PORTAL_ID>"</YOUR_PORTAL_ID>
```
2. **HubSpot form ID** – replace `data-form-id` with the ID of the form you created in HubSpot:
```html
data-form-id="<YOUR_FORM_ID>"</YOUR_FORM_ID>
```
3. **Hidden `dub_id` field** – create a hidden field on your HubSpot form whose internal property name is `dub_id`, then update the `setFieldValue` call to match its object/property path (e.g. `0-1/dub_id`, `0-2/dub_id`, …):
```js
HubSpotFormsV4.getForms()[0].setFieldValue(
"<OBJECT_TYPE_ID>/dub_id",
clickId
);
```
4. **Dub analytics `data-domains`** – replace `acme.link` with your own short link domain configured in Dub:
```html
data-domains='{"refer":"<YOUR_DUB_DOMAIN>"}'</YOUR_DUB_DOMAIN>
```

### `meeting.html`

1. **Dub publishable key** – replace the placeholder with your workspace's publishable key from [Dub → Settings → API Keys](https://app.dub.co/settings/tokens):
```js
s.setAttribute("data-publishable-key", "<YOUR_DUB_PUBLISHABLE_KEY>");
```
2. **Dub analytics `data-domains`** – replace `dub.sh` with your own short link domain:
```js
s.setAttribute("data-domains", '{"refer":"<YOUR_DUB_DOMAIN>"}');
```
3. **Remove `data-api-host`** – this line points to a local Dub instance and should be removed in production so the script talks to Dub's hosted API:
```js
s.setAttribute("data-api-host", "http://localhost:8888/api"); // remove this in production
```
4. **HubSpot meeting link** – replace the scheduler slug with your own meeting link:
```html
<div
class="meetings-iframe-container"
data-src="https://meetings.hubspot.com/<YOUR_MEETING_SLUG>?embed=true"
></div>
```

## Learn more

- [Dub Conversions – Leads](https://dub.co/docs/conversions/leads/introduction)
- [`@dub/analytics` client-side SDK](https://dub.co/docs/sdks/client-side/introduction)
- [HubSpot Forms embed](https://developers.hubspot.com/docs/cms/building-blocks/forms)
- [HubSpot Meetings embed](https://knowledge.hubspot.com/meetings-tool/embed-the-meetings-tool-on-an-external-page)
55 changes: 55 additions & 0 deletions conversions/hubspot/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Contact Us</title>
</head>
<body>
<h1>Contact us</h1>
<!-- Load HubSpot forms library -->
<script src="https://js.hsforms.net/forms/embed/47839131.js" defer></script>

<!-- Load Dub analytics script -->
<script
src="https://www.dubcdn.com/analytics/script.js"
defer
data-domains='{"refer":"acme.link"}'
></script>

<div
class="hs-form-frame"
data-region="na1"
data-form-id="07b7409d-cf6b-4d9f-9926-76d55b591e88"
data-portal-id="47839131"
></div>

<script>
// Read cookie value
function getCookie(name) {
const value = `; ${document.cookie}`;
const parts = value.split(`; ${name}=`);

if (parts.length === 2) {
return parts.pop().split(";").shift();
}

return null;
}

// Listen for the form ready event
window.addEventListener("hs-form-event:on-ready", (event) => {
const clickId = getCookie("dub_id");

if (!clickId) {
console.debug("clickId not found. Skipping lead tracking.");
return;
}

// Add the clickId to the form
// Note: Make sure you have a hidden field with connected property "dub_id"
HubSpotFormsV4.getForms()[0].setFieldValue("0-1/dub_id", clickId);
});
</script>
</body>
</html>
103 changes: 103 additions & 0 deletions conversions/hubspot/meeting.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Contact Us</title>
</head>
<body>
<h1>Contact us</h1>

<!-- Load Dub analytics script -->
<script>
!(function (c, n) {
c[n] =
c[n] ||
function () {
(c[n].q = c[n].q || []).push(arguments);
};
["trackClick", "trackLead", "trackSale"].forEach(
(t) => (c[n][t] = (...a) => c[n](t, ...a))
);
var s = document.createElement("script");
s.defer = 1;
s.src = "https://dubcdn.com/analytics/script.conversion-tracking.js";
s.setAttribute(
"data-publishable-key",
"dub_pk_tLnpzXvJAEik0UtzuzdmMJSn"
); // Replace with your publishable key
s.setAttribute("data-domains", '{"refer":"dub.sh"}');
s.setAttribute("data-api-host", "http://localhost:8888/api");
Comment thread
devkiran marked this conversation as resolved.
document.head.appendChild(s);
})(window, "dubAnalytics");
</script>

<div
class="meetings-iframe-container"
data-src="https://meetings.hubspot.com/kiran61?embed=true"
></div>

<!-- Load Meetings Embed Script -->
<script
type="text/javascript"
src="https://static.hsappstatic.net/MeetingsEmbed/ex/MeetingsEmbedCode.js"
></script>

<script>
// Read cookie value
function getCookie(name) {
const value = `; ${document.cookie}`;
const parts = value.split(`; ${name}=`);

if (parts.length === 2) {
return parts.pop().split(";").shift();
}

return null;
}
Comment thread
devkiran marked this conversation as resolved.

// Listen for the message event
window.addEventListener("message", function (event) {
// Check if the message is from the scheduling widget
if (event.origin === "https://meetings.hubspot.com") {
const clickId = getCookie("dub_id");

if (!clickId) {
console.debug("clickId not found. Skipping lead tracking.");
return;
}

// Get the data from the event
const data = event.data;

if (data.meetingBookSucceeded) {
// Get the scheduled contact
const contact =
data.meetingsPayload.bookingResponse.postResponse.contact;

if (!contact) {
console.debug("contact not found. Skipping lead tracking.");
return;
}

console.debug("contact found", contact);

// Track the lead with the scheduled contact
const customerName = [contact.firstName, contact.lastName]
.filter(Boolean)
.join(" ");

dubAnalytics.trackLead({
clickId,
mode: "deferred",
eventName: "Meeting scheduled",
customerExternalId: contact.email,
customerName: customerName,
customerEmail: contact.email,
});
}
}
});
</script>
</body>
</html>