Skip to main content
Jan 17, 2026 Paul Sullivan

HubSpot Integration Guide: Connect Product Data to Revenue Teams

If you're a product manager or product marketer at a SaaS company, you've probably had this conversation: "We have thousands of signups in our product, but sales has no idea which ones are actually qualified." Or maybe it's the reverse: "Sales keeps asking us which users hit their activation moment, but that data lives in Segment and they're looking at HubSpot."

TLDR

Connect your SaaS product to HubSpot using APIs (to push/pull data) and webhooks (for real-time triggers), then layer on intelligence frameworks like ARISE PLG to transform raw product usage into actionable revenue signals across your entire GTM team. The integration is tactical; the operating system is strategic.

The gap between product data and GTM systems isn't just annoying. It's expensive. Every qualified user who doesn't get the right outreach at the right time is revenue you're leaving on the table. Every sales rep who wastes time on cold trial users instead of hot expansion opportunities is money you're burning.

This is why connecting your product to HubSpot matters. Not as a vanity integration, but as operational infrastructure that makes your PLG motion actually work at scale.

Understanding Your Integration Options: API vs Webhooks

Before you start building, you need to understand the two primary methods for connecting your SaaS platform to HubSpot. Most teams end up using both, but for different purposes.

HubSpot's API is what you use when your product needs to actively push data into HubSpot or pull information out. This is a request-response architecture. Your product says, "here's a new user who just activated their account", and HubSpot says, "got it, record created." Or your product asks, "what's the current deal stage for this company?" and HubSpot responds with the data.

Webhooks work in the opposite direction. HubSpot notices something happened (a deal moved to "Closed Won," a contact property changed, a form was submitted) and immediately pings your product's endpoint with that information. Your product doesn't have to keep asking "did anything change?" because HubSpot tells you the moment it happens.

The best integrations use both. A webhook tells your product that something important happened in HubSpot, and your product uses the API to fetch additional context or update related records. This hybrid approach gives you real-time responsiveness without constantly polling HubSpot's API and hitting rate limits.

Step One: Setting Up Authentication

The first technical decision you'll make is whether to build a private app or a public app. This isn't about your company's business model; it's about who will use the integration.

Private apps are the right choice if you're only connecting your product to your own HubSpot portal. You generate an access token, store it securely in your environment variables, and use it for every API call. This is straightforward and works well for internal integrations.

Public apps are required if you're building something that multiple customers will install, or if you plan to list your integration in HubSpot's marketplace. This requires implementing OAuth 2.0, which adds complexity but gives you the ability to scale across multiple HubSpot portals.

Here's what the OAuth handshake looks like in Node.js:

const axios = require('axios');
const crypto = require('crypto');

const CLIENT_ID = process.env.HUBSPOT_CLIENT_ID;
const CLIENT_SECRET = process.env.HUBSPOT_CLIENT_SECRET;
const REDIRECT_URI = 'https://yoursaas.com/oauth/callback';
const SCOPES = 'crm.objects.contacts.write crm.objects.companies.read';

// Step 1: Generate authorization URL
function getAuthorizationUrl() {
  const state = crypto.randomBytes(16).toString('hex');
  const authUrl = `https://app.hubspot.com/oauth/authorize?client_id=${CLIENT_ID}&redirect_uri=${REDIRECT_URI}&scope=${SCOPES}&state=${state}`;
  
  return { authUrl, state };
}

// Step 2: Exchange authorization code for access token
async function exchangeCodeForToken(code) {
  try {
    const response = await axios.post('https://api.hubapi.com/oauth/v1/token', {
      grant_type: 'authorization_code',
      client_id: CLIENT_ID,
      client_secret: CLIENT_SECRET,
      redirect_uri: REDIRECT_URI,
      code: code
    }, {
      headers: { 'Content-Type': 'application/x-www-form-urlencoded' }
    });

    return {
      accessToken: response.data.access_token,
      refreshToken: response.data.refresh_token,
      expiresIn: response.data.expires_in
    };
  } catch (error) {
    console.error('Token exchange failed:', error.response?.data || error.message);
    throw error;
  }
}

// Step 3: Refresh access token when it expires
async function refreshAccessToken(refreshToken) {
  try {
    const response = await axios.post('https://api.hubapi.com/oauth/v1/token', {
      grant_type: 'refresh_token',
      client_id: CLIENT_ID,
      client_secret: CLIENT_SECRET,
      refresh_token: refreshToken
    }, {
      headers: { 'Content-Type': 'application/x-www-form-urlencoded' }
    });

    return {
      accessToken: response.data.access_token,
      refreshToken: response.data.refresh_token,
      expiresIn: response.data.expires_in
    };
  } catch (error) {
    console.error('Token refresh failed:', error.response?.data || error.message);
    throw error;
  }
}

module.exports = { getAuthorizationUrl, exchangeCodeForToken, refreshAccessToken };

 

And the Python equivalent:

import os
import secrets
import requests
from typing import Dict, Tuple

CLIENT_ID = os.getenv('HUBSPOT_CLIENT_ID')
CLIENT_SECRET = os.getenv('HUBSPOT_CLIENT_SECRET')
REDIRECT_URI = 'https://yoursaas.com/oauth/callback'
SCOPES = 'crm.objects.contacts.write crm.objects.companies.read'

def get_authorization_url() -> Tuple[str, str]:
    """Generate authorization URL and state token"""
    state = secrets.token_hex(16)
    auth_url = (
        f"https://app.hubspot.com/oauth/authorize?"
        f"client_id={CLIENT_ID}&"
        f"redirect_uri={REDIRECT_URI}&"
        f"scope={SCOPES}&"
        f"state={state}"
    )
    return auth_url, state

def exchange_code_for_token(code: str) -> Dict[str, any]:
    """Exchange authorization code for access token"""
    try:
        response = requests.post(
            'https://api.hubapi.com/oauth/v1/token',
            data={
                'grant_type': 'authorization_code',
                'client_id': CLIENT_ID,
                'client_secret': CLIENT_SECRET,
                'redirect_uri': REDIRECT_URI,
                'code': code
            },
            headers={'Content-Type': 'application/x-www-form-urlencoded'}
        )
        response.raise_for_status()
        
        data = response.json()
        return {
            'access_token': data['access_token'],
            'refresh_token': data['refresh_token'],
            'expires_in': data['expires_in']
        }
    except requests.exceptions.RequestException as e:
        print(f'Token exchange failed: {e}')
        raise

def refresh_access_token(refresh_token: str) -> Dict[str, any]:
    """Refresh expired access token"""
    try:
        response = requests.post(
            'https://api.hubapi.com/oauth/v1/token',
            data={
                'grant_type': 'refresh_token',
                'client_id': CLIENT_ID,
                'client_secret': CLIENT_SECRET,
                'refresh_token': refresh_token
            },
            headers={'Content-Type': 'application/x-www-form-urlencoded'}
        )
        response.raise_for_status()
        
        data = response.json()
        return {
            'access_token': data['access_token'],
            'refresh_token': data['refresh_token'],
            'expires_in': data['expires_in']
        }
    except requests.exceptions.RequestException as e:
        print(f'Token refresh failed: {e}')
        raise

 

The critical thing to understand about OAuth tokens is that access tokens expire. You need to store the refresh token securely and use it to get new access tokens before the old ones expire. Most production implementations check token expiry before each API call and automatically refresh if needed.

Step Two: Syncing Product Usage to HubSpot

Once you have authentication working, the next step is pushing product data into HubSpot. The most common pattern is syncing user activation events, feature usage, and product-qualified lead (PQL) scores.

Here's how you create or update a contact in HubSpot with product usage data:

async function syncUserToHubSpot(user, accessToken) {
  const endpoint = 'https://api.hubapi.com/crm/v3/objects/contacts';
  
  const contactData = {
    properties: {
      email: user.email,
      firstname: user.firstName,
      lastname: user.lastName,
      product_activation_date: user.activationDate,
      product_usage_score: user.usageScore,
      last_active_date: user.lastActiveDate,
      trial_end_date: user.trialEndDate,
      key_features_used: user.featuresUsed.join(';'),
      pql_score: calculatePQLScore(user)
    }
  };

  try {
    const response = await axios.post(endpoint, contactData, {
      headers: {
        'Authorization': `Bearer ${accessToken}`,
        'Content-Type': 'application/json'
      }
    });
    
    return response.data;
  } catch (error) {
    if (error.response?.status === 409) {
      // Contact already exists, update instead
      return updateContactInHubSpot(user.email, contactData.properties, accessToken);
    }
    throw error;
  }
}

async function updateContactInHubSpot(email, properties, accessToken) {
  const searchEndpoint = 'https://api.hubapi.com/crm/v3/objects/contacts/search';
  
  // First find the contact by email
  const searchResponse = await axios.post(searchEndpoint, {
    filterGroups: [{
      filters: [{
        propertyName: 'email',
        operator: 'EQ',
        value: email
      }]
    }]
  }, {
    headers: {
      'Authorization': `Bearer ${accessToken}`,
      'Content-Type': 'application/json'
    }
  });

  if (searchResponse.data.results.length === 0) {
    throw new Error('Contact not found');
  }

  const contactId = searchResponse.data.results[0].id;
  const updateEndpoint = `https://api.hubapi.com/crm/v3/objects/contacts/${contactId}`;
  
  const response = await axios.patch(updateEndpoint, { properties }, {
    headers: {
      'Authorization': `Bearer ${accessToken}`,
      'Content-Type': 'application/json'
    }
  });

  return response.data;
}

function calculatePQLScore(user) {
  let score = 0;
  
  if (user.activationComplete) score += 30;
  if (user.invitedTeamMembers > 0) score += 20;
  if (user.featuresUsed.includes('core_feature')) score += 25;
  if (user.weeklyActiveUsers > 3) score += 25;
  
  return score;
}

 

This pattern shows the reality of working with HubSpot's API: you often need to search for existing records before updating them. The API returns a 409 conflict error if you try to create a contact that already exists, so most production code checks for existence first.

Step Three: Listening for HubSpot Changes with Webhooks

While pushing product data into HubSpot is important, listening for changes in HubSpot is equally valuable. When a sales rep marks a deal as "Closed Won," your product might want to automatically upgrade that user's account. When a contact's lifecycle stage changes to "Customer," you might want to trigger an onboarding workflow in your product.

Here's how you set up a webhook endpoint to receive events from HubSpot:

const express = require('express');
const crypto = require('crypto');

const app = express();
app.use(express.json());

const WEBHOOK_SECRET = process.env.HUBSPOT_WEBHOOK_SECRET;

// Middleware to verify webhook signature
function verifyHubSpotSignature(req, res, next) {
  const signature = req.headers['x-hubspot-signature-v3'];
  const requestBody = JSON.stringify(req.body);
  
  const hash = crypto
    .createHmac('sha256', WEBHOOK_SECRET)
    .update(req.headers['x-hubspot-request-timestamp'] + requestBody)
    .digest('base64');

  if (hash !== signature) {
    return res.status(401).json({ error: 'Invalid signature' });
  }

  next();
}

// Webhook endpoint
app.post('/webhooks/hubspot', verifyHubSpotSignature, async (req, res) => {
  // Respond immediately to acknowledge receipt
  res.status(200).send();

  // Process webhook asynchronously
  try {
    for (const event of req.body) {
      await processWebhookEvent(event);
    }
  } catch (error) {
    console.error('Webhook processing error:', error);
  }
});

async function processWebhookEvent(event) {
  const { subscriptionType, objectId, propertyName, propertyValue } = event;

  switch (subscriptionType) {
    case 'deal.propertyChange':
      if (propertyName === 'dealstage' && propertyValue === 'closedwon') {
        await upgradeUserAccount(objectId);
      }
      break;

    case 'contact.propertyChange':
      if (propertyName === 'lifecyclestage' && propertyValue === 'customer') {
        await triggerOnboardingWorkflow(objectId);
      }
      break;

    case 'contact.creation':
      await enrichNewContact(objectId);
      break;

    default:
      console.log(`Unhandled event type: ${subscriptionType}`);
  }
}

async function upgradeUserAccount(dealId) {
  // Fetch deal details from HubSpot
  // Find associated contact
  // Upgrade their product account
  console.log(`Upgrading account for deal ${dealId}`);
}

async function triggerOnboardingWorkflow(contactId) {
  // Fetch contact details
  // Send onboarding email sequence
  // Enable feature flags in product
  console.log(`Triggering onboarding for contact ${contactId}`);
}

async function enrichNewContact(contactId) {
  // Pull product usage data
  // Update HubSpot contact with enrichment
  console.log(`Enriching new contact ${contactId}`);
}

app.listen(3000, () => {
  console.log('Webhook server running on port 3000');
});

 

The signature verification step is crucial. Without it, anyone could send fake webhook payloads to your endpoint and trigger actions in your product. HubSpot signs each webhook with a secret key that you configure in your developer app settings, and you must verify that signature before processing any events.

From Product Data to Revenue Intelligence

Getting the technical integration working is the first step. The strategic opportunity is what happens next.

Most product teams build HubSpot integrations to solve a specific tactical problem: "Sales needs to see trial signups" or "We want to trigger emails when users activate." But the real value emerges when you connect product behavior to the entire revenue lifecycle.

This is where ARISE's PLG Intelligence framework becomes relevant. Instead of just syncing data points, you're building an operating system that tells your entire GTM organisation exactly which product users represent real revenue opportunities.

The ARISE PLG Intelligence object tracks activation clarity (who hit value and who's stuck), real PQLs (usage, limits, invites, pricing intent scored and routed), live product pulse (feature adoption, power users, spikes and drops), trial-to-paid insight (who will convert, when trials end, which nudges work), and revenue plus churn signals (MRR, LTV, expansion and churn risk driven by in-product behavior).

This transforms your product from a data silo into a system that tells sales exactly who to upgrade, customer success exactly who to save, and marketing exactly who to leave alone. But PLG Intelligence doesn't operate in isolation.

Connecting PLG Intelligence to the Broader ARISE OS Engine

Once you have product data flowing into HubSpot through your integration, you can connect it to the other intelligence layers in the ARISE OS engine. This is where tactical integration becomes strategic infrastructure.

Competitive Intelligence shows you which competitors your highest-usage product users are also evaluating. When a power user suddenly drops their activity score, and your Competitive Intelligence object shows their company recently started looking at alternative solutions, that's an expansion opportunity turning into a churn risk. Sales needs that signal immediately.

Customer Intelligence validates what users actually do against what they said they would do during the sales process. You told the customer they'd get value from Feature X within 30 days. Customer Intelligence tracks whether that happened, and if it didn't, what their actual usage pattern reveals about their real goals, fears and triggers.

Battlecards arm your sales team with exactly what to say when a product-qualified lead mentions they're also evaluating a competitor. The PQL score from your integration tells sales this user is hot. The Battlecard tells them how to close based on which competitor is in play and which product features the user has already adopted.

GTM Strategy Planner connects product adoption data to your value proposition and positioning. If customers who adopt Feature A have 3x higher retention than customers who adopt Feature B, that's not just a product insight, it's a GTM insight. Your positioning should lead with Feature A. Your sales process should prioritise getting customers to that activation moment. Your customer success team should build its own onboarding around it.

This is the difference between having product data in HubSpot versus having an intelligence engine. The integration gives you visibility. The engine gives you action.

Making This Real

If you're a product manager or product marketer reading this, you probably recognise the tactical value of connecting your product to HubSpot. You might already have a basic integration that syncs signup data or pushes trial start events.

The strategic question is whether that integration serves as infrastructure for intelligence or just generates more noise in your CRM. Most teams have plenty of data in HubSpot. What they lack is the connective tissue that turns product signals into revenue actions.

The technical patterns in this article give you the foundation to build reliable integrations that scale. The ARISE OS framework gives you the architecture to turn those integrations into a competitive advantage. Your product already knows which users are qualified. The question is whether your GTM organisation can act on that knowledge before your competitors do.

If you're building PLG motion and need to connect product intelligence to revenue execution, the integration is just the beginning. The operating system is what makes it work.

For more help in integrating your PLG strategy across the product and CRM, as well as the supporting services, contact our team now.

Book time with Arise →

Published by Paul Sullivan January 17, 2026
Paul Sullivan