# How to Integrate Replit with HubSpot Marketing Hub

- Tool: Replit
- Difficulty: Intermediate
- Time required: 30 minutes
- Last updated: March 2026

## TL;DR

To integrate Replit with HubSpot Marketing Hub, generate a private app access token in HubSpot with marketing scopes, store it in Replit Secrets (lock icon 🔒), and call the HubSpot Marketing API from Python or Node.js server-side code to manage forms, email campaigns, contacts, and marketing workflows. Use Autoscale deployment for web apps that capture leads or trigger marketing events.

## Why Connect Replit to HubSpot Marketing Hub?

HubSpot Marketing Hub is a comprehensive marketing automation platform used by over 190,000 businesses for email campaigns, lead nurturing, form capture, and contact management. Connecting your Replit app to HubSpot enables you to sync leads from any source directly into HubSpot's contact database, trigger automated email workflows based on user behavior in your app, and pull campaign performance data into custom reporting dashboards — all without manual data export and import workflows.

The most common integration patterns are pushing new user registrations from your Replit app into HubSpot as marketing contacts, enrolling contacts in specific nurture workflows based on their in-app actions, and receiving webhooks when contacts interact with marketing emails (opens, clicks, unsubscribes). The HubSpot API v3 provides a clean, well-documented REST interface with consistent response formats across all marketing objects.

HubSpot's private app authentication model (introduced in 2022) replaces the older API key system and provides more granular permission control. Your private app access token is a long-lived bearer token that only has access to the specific HubSpot scopes you grant. Store it in Replit Secrets (lock icon 🔒) and access it with os.environ['HUBSPOT_ACCESS_TOKEN'] in Python or process.env.HUBSPOT_ACCESS_TOKEN in Node.js. Never include it in client-side code or commit it to Git.

## Before you start

- A Replit account with a Python or Node.js project created
- A HubSpot account (Marketing Hub Starter or higher for full API access)
- Super Admin or App Marketplace permissions in your HubSpot account to create private apps
- Basic familiarity with REST APIs and Bearer token authentication
- Node.js 18+ or Python 3.10+ (both available on Replit by default)

## Step-by-step guide

### 1. Create a HubSpot Private App and Get an Access Token

Log in to your HubSpot account and click the settings gear icon in the top navigation bar. In the left sidebar, navigate to Account Management > Integrations > Private Apps. Click 'Create a private app'.

Give your app a descriptive name like 'Replit Marketing Integration'. On the 'Scopes' tab, select the permissions your integration needs. For marketing workflows, select: crm.objects.contacts.read, crm.objects.contacts.write, marketing-email (for email campaign data), automation (for workflow enrollment), and forms (for form submission data). You can always add more scopes later by editing the private app.

Click 'Create app'. A dialog will show your access token — it is a long string starting with 'pat-'. Copy it immediately. This token does not expire but can be rotated at any time from the Private Apps settings page.

Note your HubSpot Portal ID as well. It appears in the upper-right corner of HubSpot next to your account name as a numeric value (e.g., 12345678). You need the Portal ID for some API endpoints and for constructing webhook verification.

**Expected result:** You have a HubSpot private app access token (starting with 'pat-') and your Portal ID copied and ready to store in Replit Secrets.

### 2. Store HubSpot Credentials in Replit Secrets

Open your Replit project and click the lock icon 🔒 in the left sidebar to open the Secrets pane. Add the following secrets:

- Key: HUBSPOT_ACCESS_TOKEN — Value: your private app access token (starting with 'pat-')
- Key: HUBSPOT_PORTAL_ID — Value: your numeric HubSpot Portal ID

Click 'Add Secret' after each one. Replit encrypts these values at rest and injects them as environment variables at runtime. In Python, read these with os.environ['HUBSPOT_ACCESS_TOKEN'] and os.environ['HUBSPOT_PORTAL_ID']. In Node.js, use process.env.HUBSPOT_ACCESS_TOKEN.

If you are also setting up HubSpot webhooks, you will receive a client secret during webhook subscription creation — store this as HUBSPOT_CLIENT_SECRET for signature verification. Do not confuse the private app access token with the OAuth app client secret — they are different credentials used for different authentication flows.

Replit's Secret Scanner will warn you if you accidentally type an access token directly into a source code file, helping prevent accidental credential exposure.

**Expected result:** Two Secrets (HUBSPOT_ACCESS_TOKEN and HUBSPOT_PORTAL_ID) appear in the Replit Secrets pane with masked values.

### 3. Manage Contacts and Form Submissions with Python

The HubSpot API v3 uses Bearer token authentication. All requests include the Authorization: Bearer {token} header. The base URL for most CRM operations is https://api.hubapi.com/. Contact operations use the /crm/v3/objects/contacts/ endpoint.

Contacts in HubSpot are identified by their email address or internal HubSpot contact ID. The upsert pattern (create or update) is particularly useful: if a contact with the given email already exists, HubSpot updates it; otherwise, it creates a new contact. Use the POST /crm/v3/objects/contacts endpoint for individual contacts or the /crm/v3/objects/contacts/batch/ endpoint for bulk operations.

Install the HubSpot Python SDK with pip install hubspot-api-client, or use the requests library directly. The code below uses requests for explicit control over each API call.

```
import os
import requests
from typing import Optional

ACCESS_TOKEN = os.environ["HUBSPOT_ACCESS_TOKEN"]
PORTAL_ID = os.environ["HUBSPOT_PORTAL_ID"]
BASE_URL = "https://api.hubapi.com"

HEADERS = {
    "Authorization": f"Bearer {ACCESS_TOKEN}",
    "Content-Type": "application/json"
}

def get_contact_by_email(email: str) -> Optional[dict]:
    """Look up a contact by email address."""
    url = f"{BASE_URL}/crm/v3/objects/contacts/{email}"
    params = {
        "idProperty": "email",
        "properties": "firstname,lastname,email,company,hs_lead_status,lifecyclestage"
    }
    response = requests.get(url, headers=HEADERS, params=params)
    if response.status_code == 404:
        return None
    response.raise_for_status()
    return response.json()

def create_or_update_contact(
    email: str,
    first_name: str = "",
    last_name: str = "",
    company: str = "",
    extra_properties: dict = None
) -> dict:
    """
    Upsert a contact in HubSpot.
    If the email exists, updates properties. If not, creates a new contact.
    """
    properties = {
        "email": email,
        "firstname": first_name,
        "lastname": last_name,
        "company": company,
    }
    if extra_properties:
        properties.update(extra_properties)

    # Try to create first
    response = requests.post(
        f"{BASE_URL}/crm/v3/objects/contacts",
        json={"properties": properties},
        headers=HEADERS
    )

    # If contact exists (409 Conflict), update instead
    if response.status_code == 409:
        existing = get_contact_by_email(email)
        if existing:
            contact_id = existing['id']
            update_response = requests.patch(
                f"{BASE_URL}/crm/v3/objects/contacts/{contact_id}",
                json={"properties": properties},
                headers=HEADERS
            )
            update_response.raise_for_status()
            return update_response.json()
    
    response.raise_for_status()
    return response.json()

def add_contact_to_list(contact_id: str, list_id: str) -> bool:
    """Add a contact to a HubSpot static list."""
    url = f"{BASE_URL}/contacts/v1/lists/{list_id}/add"
    payload = {"vids": [int(contact_id)]}
    response = requests.post(url, json=payload, headers=HEADERS)
    response.raise_for_status()
    return True

def enroll_in_workflow(contact_email: str, workflow_id: str) -> bool:
    """Enroll a contact in a HubSpot workflow by email."""
    url = f"{BASE_URL}/automation/v4/enrollment/automations/{workflow_id}/enrollments/contacts"
    payload = {"email": contact_email}
    response = requests.post(url, json=payload, headers=HEADERS)
    response.raise_for_status()
    return True

def get_marketing_emails(limit: int = 20) -> list:
    """Fetch marketing email campaigns and their stats."""
    url = f"{BASE_URL}/marketing/v3/emails"
    params = {"limit": limit, "orderBy": "-stats.sent"}
    response = requests.get(url, headers=HEADERS, params=params)
    response.raise_for_status()
    return response.json().get('results', [])

# Example usage
if __name__ == "__main__":
    # Create a test contact
    contact = create_or_update_contact(
        email="test@example.com",
        first_name="Jane",
        last_name="Doe",
        company="Acme Corp",
        extra_properties={"lifecyclestage": "lead"}
    )
    print(f"Contact: {contact['id']} — {contact['properties'].get('email')}")

    # Fetch email campaigns
    emails = get_marketing_emails(5)
    print(f"\nMarketing emails: {[e.get('name', 'Unnamed') for e in emails]}")
```

**Expected result:** Running the script creates a test contact in HubSpot and prints the contact ID alongside a list of recent marketing email campaign names.

### 4. Build a Node.js Integration for Lead Capture

For Node.js projects, use the @hubspot/api-client npm package (npm install @hubspot/api-client) for a typed SDK experience, or use axios for direct API calls. The Express server below provides endpoints for capturing leads from a web form, enriching contact records with additional properties, and triggering workflow enrollments.

The server exposes a POST /leads endpoint designed to be called from your frontend when a user submits a sign-up or contact form. The endpoint creates or updates the HubSpot contact and optionally enrolls them in a welcome workflow. This pattern keeps all HubSpot API interactions server-side so your access token is never exposed to the browser.

Install dependencies with npm install express @hubspot/api-client. The HubSpot SDK handles pagination and rate limiting automatically for collection endpoints.

```
const express = require('express');
const hubspot = require('@hubspot/api-client');

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

const ACCESS_TOKEN = process.env.HUBSPOT_ACCESS_TOKEN;
const WELCOME_WORKFLOW_ID = process.env.HUBSPOT_ONBOARDING_WORKFLOW_ID || '';

// Initialize HubSpot client
const hubspotClient = new hubspot.Client({ accessToken: ACCESS_TOKEN });

// Create or update a contact (upsert by email)
app.post('/leads', async (req, res) => {
  const { email, firstName, lastName, company, phone, source } = req.body;
  if (!email) return res.status(400).json({ error: 'email is required' });

  try {
    const properties = {
      email,
      firstname: firstName || '',
      lastname: lastName || '',
      company: company || '',
      phone: phone || '',
      lead_source: source || 'web-form',
      lifecyclestage: 'lead'
    };

    let contactId;
    try {
      // Try to create
      const createResponse = await hubspotClient.crm.contacts.basicApi.create({
        properties
      });
      contactId = createResponse.id;
    } catch (createErr) {
      if (createErr.code === 409) {
        // Contact exists — update instead
        const existing = await hubspotClient.crm.contacts.basicApi.getById(
          email, ['id'], undefined, undefined, false, 'email'
        );
        await hubspotClient.crm.contacts.basicApi.update(existing.id, { properties });
        contactId = existing.id;
      } else {
        throw createErr;
      }
    }

    // Optionally enroll in onboarding workflow
    if (WELCOME_WORKFLOW_ID && contactId) {
      try {
        const axios = require('axios');
        await axios.post(
          `https://api.hubapi.com/automation/v4/enrollment/automations/${WELCOME_WORKFLOW_ID}/enrollments/contacts`,
          { email },
          { headers: { Authorization: `Bearer ${ACCESS_TOKEN}`, 'Content-Type': 'application/json' } }
        );
      } catch (workflowErr) {
        console.warn('Workflow enrollment failed:', workflowErr.message);
        // Non-critical — contact was still created
      }
    }

    res.status(201).json({ success: true, contactId });
  } catch (err) {
    console.error('HubSpot error:', err.message);
    res.status(500).json({ error: err.message });
  }
});

// Get recent contacts with lifecycle stage
app.get('/contacts', async (req, res) => {
  try {
    const response = await hubspotClient.crm.contacts.basicApi.getPage(
      parseInt(req.query.limit) || 20,
      undefined,
      ['email', 'firstname', 'lastname', 'lifecyclestage', 'createdate']
    );
    res.json(response.results);
  } catch (err) {
    res.status(500).json({ error: err.message });
  }
});

// Get marketing email list
app.get('/email-campaigns', async (req, res) => {
  try {
    const axios = require('axios');
    const response = await axios.get(
      'https://api.hubapi.com/marketing/v3/emails',
      {
        headers: { Authorization: `Bearer ${ACCESS_TOKEN}` },
        params: { limit: 20 }
      }
    );
    res.json(response.data.results || []);
  } catch (err) {
    res.status(500).json({ error: err.message });
  }
});

app.listen(3000, '0.0.0.0', () => {
  console.log('HubSpot Marketing Hub integration server running on port 3000');
});
```

**Expected result:** The server starts on port 3000. A POST to /leads with an email address creates a contact in HubSpot and returns the contact ID.

### 5. Set Up HubSpot Webhooks and Deploy

HubSpot can send webhook notifications to your Replit app when contacts are created or updated, when deals change stage, or when specific marketing events occur like email opens and clicks. This real-time data flow eliminates the need to poll HubSpot for changes.

To set up webhooks, go to your Private App in HubSpot (Settings > Integrations > Private Apps) and click on your app. Select the 'Webhooks' tab. Click 'Create subscription'. Enter your deployed Replit URL plus the webhook path (e.g., https://your-app.replit.app/hubspot/webhook). Select the event type: contact.creation, contact.propertyChange, deal.stageChange, or email.sent are the most useful for marketing integrations.

HubSpot signs webhook payloads with a v3 signature using your app client secret. Verify this signature on every incoming request to prevent processing fake events. The signature is in the X-HubSpot-Signature-v3 header.

Deploy your Replit app before registering webhooks. Use Autoscale deployment for marketing apps where webhook volume varies. Click 'Deploy' in the Replit toolbar and wait for the deployment to complete before registering the webhook URL.

```
from flask import Flask, request, jsonify
import os
import hmac
import hashlib
import time

app = Flask(__name__)

ACCESS_TOKEN = os.environ["HUBSPOT_ACCESS_TOKEN"]
CLIENT_SECRET = os.environ.get("HUBSPOT_CLIENT_SECRET", "")

def verify_hubspot_signature(request_body: bytes, timestamp: str, signature: str) -> bool:
    """
    Verify HubSpot webhook signature v3.
    Validates that the request came from HubSpot.
    """
    if not CLIENT_SECRET:
        return True  # Skip verification in development
    
    # Check timestamp is within 5 minutes to prevent replay attacks
    if abs(time.time() - int(timestamp) / 1000) > 300:
        return False
    
    # Build the string to sign: method + URI + body + timestamp
    # Note: HubSpot uses the full request URI for v3 signatures
    expected = hmac.new(
        CLIENT_SECRET.encode(),
        request_body + timestamp.encode(),
        hashlib.sha256
    ).hexdigest()
    return hmac.compare_digest(expected, signature)

@app.route('/hubspot/webhook', methods=['POST'])
def hubspot_webhook():
    # Verify signature
    signature = request.headers.get('X-HubSpot-Signature-v3', '')
    timestamp = request.headers.get('X-HubSpot-Request-Timestamp', '0')
    
    if CLIENT_SECRET and not verify_hubspot_signature(request.data, timestamp, signature):
        return jsonify({'error': 'Invalid signature'}), 401
    
    events = request.json
    if not events:
        return jsonify({'error': 'No events'}), 400
    
    for event in events:
        event_type = event.get('subscriptionType')
        object_id = event.get('objectId')
        portal_id = event.get('portalId')
        
        print(f"HubSpot event: {event_type} for object {object_id} in portal {portal_id}")
        
        if event_type == 'contact.creation':
            # New contact created — trigger welcome workflow, send to CRM, etc.
            print(f"New contact: {object_id}")
        elif event_type == 'contact.propertyChange':
            property_name = event.get('propertyName')
            new_value = event.get('propertyValue')
            print(f"Contact {object_id} property '{property_name}' changed to '{new_value}'")
    
    return jsonify({'received': True}), 200

if __name__ == '__main__':
    app.run(host='0.0.0.0', port=3000)
```

**Expected result:** After deployment and webhook registration, new HubSpot contact events appear in your Replit console output, and the webhook subscription shows 'Active' status in HubSpot.

## Best practices

- Store HUBSPOT_ACCESS_TOKEN and HUBSPOT_PORTAL_ID in Replit Secrets (lock icon 🔒) — never in source code or Git history.
- Use a private app access token instead of the legacy HubSpot API key, as the legacy API key is deprecated and no longer supported for new integrations.
- Implement the upsert pattern for contact creation — always handle 409 Conflict responses by updating the existing contact rather than failing.
- Grant only the minimum scopes needed for your integration — if you only sync contacts, you do not need marketing-email or automation scopes.
- Verify HubSpot webhook signatures using your app client secret before processing events to prevent processing forged requests.
- Rate limits on Starter plans are 100 requests per 10 seconds — implement exponential backoff when you receive 429 responses, especially in batch import scenarios.
- Deploy with Autoscale for web apps that capture leads or serve dashboards; use Reserved VM only if you have a high-frequency webhook processor that cannot tolerate cold-start delay.
- Use the batch endpoints (/batch/create, /batch/upsert) when importing more than 10 contacts at a time — they are significantly more efficient than individual create requests.

## Use cases

### Lead Capture and Contact Sync

When a user submits a sign-up form or completes a key action in your Replit app, automatically create or update a HubSpot contact with their details and any relevant properties like plan tier, signup source, or feature usage. The contact appears immediately in HubSpot's contact database and can be enrolled in onboarding email sequences.

Prompt example:

```
Build a Flask endpoint that accepts a lead form POST with email, firstName, lastName, and company fields, creates or updates a HubSpot contact using the HUBSPOT_ACCESS_TOKEN from Replit Secrets, and returns a success response with the contact ID.
```

### Workflow Enrollment Based on App Events

Enroll contacts in specific HubSpot marketing workflows when they reach milestones in your Replit app — for example, enrolling a user in a trial-expiry nurture sequence when their free trial ends, or triggering a win-back campaign when a user has not logged in for 30 days. The workflow handles the email sequence scheduling within HubSpot.

Prompt example:

```
Write a Python function that takes a user's email address and a workflow ID, looks up the contact in HubSpot by email, and enrolls them in the specified workflow using the POST /automation/v4/enrollment/automations/{workflowId}/enrollments/contacts endpoint.
```

### Marketing Email Performance Dashboard

Build a Replit app that pulls HubSpot email campaign statistics — open rates, click rates, bounce rates, and unsubscribe rates — for all campaigns in the past quarter and displays them in a sortable table. This gives your marketing team a consolidated view without requiring everyone to log into HubSpot directly.

Prompt example:

```
Create an Express server with a GET /email-campaigns endpoint that fetches all marketing emails from HubSpot using the Marketing Emails API, retrieves performance statistics for each campaign, and returns a JSON array sorted by open rate for the past 90 days.
```

## Troubleshooting

### API returns 401 Unauthorized — 'You don't have permission to access this resource'

Cause: The private app access token is missing the required scope for the endpoint being called. For example, calling the Marketing Emails API without the marketing-email scope returns a 403, not 401. A 401 typically means the token itself is invalid or was revoked.

Solution: Check your private app settings in HubSpot (Settings > Integrations > Private Apps) to verify the token is active and has the required scopes. Edit the private app to add missing scopes. After adding scopes, HubSpot may require you to rotate the token — generate a new one and update HUBSPOT_ACCESS_TOKEN in Replit Secrets.

```
# Python: verify token validity with a simple test request
response = requests.get(
    'https://api.hubapi.com/oauth/v1/access-tokens/' + ACCESS_TOKEN,
    headers={'Authorization': f'Bearer {ACCESS_TOKEN}'}
)
print(response.json())  # Shows token info including scopes and expiry
```

### POST /crm/v3/objects/contacts returns 409 Conflict

Cause: A contact with this email address already exists in HubSpot. The basic create endpoint does not upsert — it only creates new records.

Solution: Implement an upsert pattern: catch 409 responses, look up the existing contact by email using the idProperty=email query parameter, and then PATCH the contact with the new properties. Alternatively, use the batch upsert endpoint at POST /crm/v3/objects/contacts/batch/upsert which handles create-or-update automatically.

```
# Python: upsert using batch endpoint
def upsert_contacts(contacts: list) -> dict:
    """contacts = [{email, firstname, lastname, ...}, ...]"""
    inputs = [
        {"properties": c, "idProperty": "email"}
        for c in contacts
    ]
    response = requests.post(
        f"{BASE_URL}/crm/v3/objects/contacts/batch/upsert",
        json={"inputs": inputs},
        headers=HEADERS
    )
    response.raise_for_status()
    return response.json()
```

### Workflow enrollment returns 400 — 'Contact is not eligible for enrollment'

Cause: The contact does not meet the workflow's re-enrollment criteria, is already enrolled in this workflow, or the workflow is paused or not set to allow manual enrollment.

Solution: In HubSpot, edit the workflow and check Settings > Enrollment options. Enable 'Re-enrollment' if you want to enroll existing contacts again. Also verify the workflow is turned on (Active status) in the workflow editor. Check that the contact's current properties match any enrollment criteria the workflow requires.

### Webhooks are never received despite the subscription showing Active in HubSpot

Cause: The webhook URL points to a Replit development server that is offline, or the server is returning non-200 responses causing HubSpot to mark the subscription as failing. HubSpot retries failed webhooks but eventually deactivates subscriptions that consistently fail.

Solution: Deploy your Replit app with Autoscale to get a stable URL. Update the webhook subscription URL in HubSpot to point to the deployed URL. Test the endpoint manually with a curl POST to confirm it returns 200. In HubSpot, check the webhook subscription's 'Event log' tab for delivery attempts and error details.

```
# Test your webhook endpoint locally with a sample payload
# Run this in your Replit Shell to simulate a HubSpot webhook:
# curl -X POST http://localhost:3000/hubspot/webhook \
#   -H 'Content-Type: application/json' \
#   -d '[{"subscriptionType": "contact.creation", "objectId": 12345}]'
```

## Frequently asked questions

### How do I store my HubSpot access token in Replit?

Click the lock icon 🔒 in the left sidebar of your Replit project to open the Secrets pane. Add HUBSPOT_ACCESS_TOKEN with your private app access token (starting with 'pat-') and HUBSPOT_PORTAL_ID with your numeric Portal ID. Access them in Python with os.environ['HUBSPOT_ACCESS_TOKEN'] and in Node.js with process.env.HUBSPOT_ACCESS_TOKEN.

### Does the HubSpot private app token expire?

No. HubSpot private app access tokens do not have an expiry date — they remain valid until you manually rotate or revoke them in the private app settings. Unlike OAuth tokens, they do not require a refresh flow. However, if you change the scopes of your private app, HubSpot may require you to generate a new token for the changes to take effect.

### Can I use HubSpot Marketing Hub with Replit on the free tier?

HubSpot's free CRM plan includes basic contact management API access. The Marketing Hub features (email campaigns, workflows, forms API) require Marketing Hub Starter ($20/month) or higher. Replit's free tier supports outbound API calls, but you will need Replit Core for always-on deployments required to receive HubSpot webhooks reliably.

### How do I enroll a contact in a HubSpot workflow from Replit?

Use the POST /automation/v4/enrollment/automations/{workflowId}/enrollments/contacts endpoint with the contact's email address in the request body. The workflow must be Active and have manual enrollment enabled. Find the workflow ID in HubSpot under Automation > Workflows — it appears in the URL as a numeric ID when you edit the workflow.

### How do I find my HubSpot Portal ID?

Your HubSpot Portal ID (also called Hub ID) appears in the top-right corner of your HubSpot account next to your account name as a numeric value. You can also find it in the account settings under Account Management > Account Defaults > Hub ID. Store it as HUBSPOT_PORTAL_ID in Replit Secrets.

### What is the difference between HubSpot private apps and the legacy API key?

Private apps are HubSpot's current authentication standard, replacing the legacy API key system which was deprecated in 2022 and removed in 2023. Private apps use OAuth-style bearer tokens with granular scope control, so each integration only has access to the HubSpot data it actually needs. Legacy API keys had full account access and are no longer supported for new integrations.

---

Source: https://www.rapidevelopers.com/replit-integration/hubspot-marketing-hub
© RapidDev — https://www.rapidevelopers.com/replit-integration/hubspot-marketing-hub
