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.
Integration method
You connect Replit to HubSpot Marketing Hub by creating a private app in your HubSpot account with the required marketing scopes, obtaining a private app access token, and storing it in Replit Secrets. Your server-side Python or Node.js code calls the HubSpot API v3 using the token in an Authorization: Bearer header. The HubSpot API covers contact management, forms, email campaigns, and workflow triggers through a consistent REST interface.
Prerequisites
- 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
Create a HubSpot Private App and Get an Access Token
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.
Pro tip: Grant only the minimum scopes your integration actually needs. If you only need to create contacts, you do not need the marketing-email scope. Minimal permissions reduce the blast radius if the token is ever compromised.
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.
Store HubSpot Credentials in Replit Secrets
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.
Pro tip: Store workflow IDs, list IDs, and form IDs that your integration uses frequently as additional Secrets (HUBSPOT_ONBOARDING_WORKFLOW_ID, HUBSPOT_NEWSLETTER_LIST_ID). This makes it easy to update target IDs without changing code.
Expected result: Two Secrets (HUBSPOT_ACCESS_TOKEN and HUBSPOT_PORTAL_ID) appear in the Replit Secrets pane with masked values.
Manage Contacts and Form Submissions with Python
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.
1import os2import requests3from typing import Optional45ACCESS_TOKEN = os.environ["HUBSPOT_ACCESS_TOKEN"]6PORTAL_ID = os.environ["HUBSPOT_PORTAL_ID"]7BASE_URL = "https://api.hubapi.com"89HEADERS = {10 "Authorization": f"Bearer {ACCESS_TOKEN}",11 "Content-Type": "application/json"12}1314def get_contact_by_email(email: str) -> Optional[dict]:15 """Look up a contact by email address."""16 url = f"{BASE_URL}/crm/v3/objects/contacts/{email}"17 params = {18 "idProperty": "email",19 "properties": "firstname,lastname,email,company,hs_lead_status,lifecyclestage"20 }21 response = requests.get(url, headers=HEADERS, params=params)22 if response.status_code == 404:23 return None24 response.raise_for_status()25 return response.json()2627def create_or_update_contact(28 email: str,29 first_name: str = "",30 last_name: str = "",31 company: str = "",32 extra_properties: dict = None33) -> dict:34 """35 Upsert a contact in HubSpot.36 If the email exists, updates properties. If not, creates a new contact.37 """38 properties = {39 "email": email,40 "firstname": first_name,41 "lastname": last_name,42 "company": company,43 }44 if extra_properties:45 properties.update(extra_properties)4647 # Try to create first48 response = requests.post(49 f"{BASE_URL}/crm/v3/objects/contacts",50 json={"properties": properties},51 headers=HEADERS52 )5354 # If contact exists (409 Conflict), update instead55 if response.status_code == 409:56 existing = get_contact_by_email(email)57 if existing:58 contact_id = existing['id']59 update_response = requests.patch(60 f"{BASE_URL}/crm/v3/objects/contacts/{contact_id}",61 json={"properties": properties},62 headers=HEADERS63 )64 update_response.raise_for_status()65 return update_response.json()66 67 response.raise_for_status()68 return response.json()6970def add_contact_to_list(contact_id: str, list_id: str) -> bool:71 """Add a contact to a HubSpot static list."""72 url = f"{BASE_URL}/contacts/v1/lists/{list_id}/add"73 payload = {"vids": [int(contact_id)]}74 response = requests.post(url, json=payload, headers=HEADERS)75 response.raise_for_status()76 return True7778def enroll_in_workflow(contact_email: str, workflow_id: str) -> bool:79 """Enroll a contact in a HubSpot workflow by email."""80 url = f"{BASE_URL}/automation/v4/enrollment/automations/{workflow_id}/enrollments/contacts"81 payload = {"email": contact_email}82 response = requests.post(url, json=payload, headers=HEADERS)83 response.raise_for_status()84 return True8586def get_marketing_emails(limit: int = 20) -> list:87 """Fetch marketing email campaigns and their stats."""88 url = f"{BASE_URL}/marketing/v3/emails"89 params = {"limit": limit, "orderBy": "-stats.sent"}90 response = requests.get(url, headers=HEADERS, params=params)91 response.raise_for_status()92 return response.json().get('results', [])9394# Example usage95if __name__ == "__main__":96 # Create a test contact97 contact = create_or_update_contact(98 email="test@example.com",99 first_name="Jane",100 last_name="Doe",101 company="Acme Corp",102 extra_properties={"lifecyclestage": "lead"}103 )104 print(f"Contact: {contact['id']} β {contact['properties'].get('email')}")105106 # Fetch email campaigns107 emails = get_marketing_emails(5)108 print(f"\nMarketing emails: {[e.get('name', 'Unnamed') for e in emails]}")Pro tip: HubSpot's API has different rate limits depending on your plan. Free and Starter plans allow 100 requests per 10 seconds. Professional and Enterprise plans allow higher limits. Implement retry logic with exponential backoff when you receive a 429 Too Many Requests response.
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.
Build a Node.js Integration for Lead Capture
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.
1const express = require('express');2const hubspot = require('@hubspot/api-client');34const app = express();5app.use(express.json());67const ACCESS_TOKEN = process.env.HUBSPOT_ACCESS_TOKEN;8const WELCOME_WORKFLOW_ID = process.env.HUBSPOT_ONBOARDING_WORKFLOW_ID || '';910// Initialize HubSpot client11const hubspotClient = new hubspot.Client({ accessToken: ACCESS_TOKEN });1213// Create or update a contact (upsert by email)14app.post('/leads', async (req, res) => {15 const { email, firstName, lastName, company, phone, source } = req.body;16 if (!email) return res.status(400).json({ error: 'email is required' });1718 try {19 const properties = {20 email,21 firstname: firstName || '',22 lastname: lastName || '',23 company: company || '',24 phone: phone || '',25 lead_source: source || 'web-form',26 lifecyclestage: 'lead'27 };2829 let contactId;30 try {31 // Try to create32 const createResponse = await hubspotClient.crm.contacts.basicApi.create({33 properties34 });35 contactId = createResponse.id;36 } catch (createErr) {37 if (createErr.code === 409) {38 // Contact exists β update instead39 const existing = await hubspotClient.crm.contacts.basicApi.getById(40 email, ['id'], undefined, undefined, false, 'email'41 );42 await hubspotClient.crm.contacts.basicApi.update(existing.id, { properties });43 contactId = existing.id;44 } else {45 throw createErr;46 }47 }4849 // Optionally enroll in onboarding workflow50 if (WELCOME_WORKFLOW_ID && contactId) {51 try {52 const axios = require('axios');53 await axios.post(54 `https://api.hubapi.com/automation/v4/enrollment/automations/${WELCOME_WORKFLOW_ID}/enrollments/contacts`,55 { email },56 { headers: { Authorization: `Bearer ${ACCESS_TOKEN}`, 'Content-Type': 'application/json' } }57 );58 } catch (workflowErr) {59 console.warn('Workflow enrollment failed:', workflowErr.message);60 // Non-critical β contact was still created61 }62 }6364 res.status(201).json({ success: true, contactId });65 } catch (err) {66 console.error('HubSpot error:', err.message);67 res.status(500).json({ error: err.message });68 }69});7071// Get recent contacts with lifecycle stage72app.get('/contacts', async (req, res) => {73 try {74 const response = await hubspotClient.crm.contacts.basicApi.getPage(75 parseInt(req.query.limit) || 20,76 undefined,77 ['email', 'firstname', 'lastname', 'lifecyclestage', 'createdate']78 );79 res.json(response.results);80 } catch (err) {81 res.status(500).json({ error: err.message });82 }83});8485// Get marketing email list86app.get('/email-campaigns', async (req, res) => {87 try {88 const axios = require('axios');89 const response = await axios.get(90 'https://api.hubapi.com/marketing/v3/emails',91 {92 headers: { Authorization: `Bearer ${ACCESS_TOKEN}` },93 params: { limit: 20 }94 }95 );96 res.json(response.data.results || []);97 } catch (err) {98 res.status(500).json({ error: err.message });99 }100});101102app.listen(3000, '0.0.0.0', () => {103 console.log('HubSpot Marketing Hub integration server running on port 3000');104});Pro tip: The @hubspot/api-client SDK throws errors with a body property containing the HubSpot API error details. Use err.body?.message or err.response?.body?.message for more descriptive error logging rather than just err.message.
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.
Set Up HubSpot Webhooks and Deploy
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.
1from flask import Flask, request, jsonify2import os3import hmac4import hashlib5import time67app = Flask(__name__)89ACCESS_TOKEN = os.environ["HUBSPOT_ACCESS_TOKEN"]10CLIENT_SECRET = os.environ.get("HUBSPOT_CLIENT_SECRET", "")1112def verify_hubspot_signature(request_body: bytes, timestamp: str, signature: str) -> bool:13 """14 Verify HubSpot webhook signature v3.15 Validates that the request came from HubSpot.16 """17 if not CLIENT_SECRET:18 return True # Skip verification in development19 20 # Check timestamp is within 5 minutes to prevent replay attacks21 if abs(time.time() - int(timestamp) / 1000) > 300:22 return False23 24 # Build the string to sign: method + URI + body + timestamp25 # Note: HubSpot uses the full request URI for v3 signatures26 expected = hmac.new(27 CLIENT_SECRET.encode(),28 request_body + timestamp.encode(),29 hashlib.sha25630 ).hexdigest()31 return hmac.compare_digest(expected, signature)3233@app.route('/hubspot/webhook', methods=['POST'])34def hubspot_webhook():35 # Verify signature36 signature = request.headers.get('X-HubSpot-Signature-v3', '')37 timestamp = request.headers.get('X-HubSpot-Request-Timestamp', '0')38 39 if CLIENT_SECRET and not verify_hubspot_signature(request.data, timestamp, signature):40 return jsonify({'error': 'Invalid signature'}), 40141 42 events = request.json43 if not events:44 return jsonify({'error': 'No events'}), 40045 46 for event in events:47 event_type = event.get('subscriptionType')48 object_id = event.get('objectId')49 portal_id = event.get('portalId')50 51 print(f"HubSpot event: {event_type} for object {object_id} in portal {portal_id}")52 53 if event_type == 'contact.creation':54 # New contact created β trigger welcome workflow, send to CRM, etc.55 print(f"New contact: {object_id}")56 elif event_type == 'contact.propertyChange':57 property_name = event.get('propertyName')58 new_value = event.get('propertyValue')59 print(f"Contact {object_id} property '{property_name}' changed to '{new_value}'")60 61 return jsonify({'received': True}), 2006263if __name__ == '__main__':64 app.run(host='0.0.0.0', port=3000)Pro tip: HubSpot sends webhooks as arrays of events β your endpoint receives a JSON array, not a single event object. Always iterate over the events array, as HubSpot may batch multiple events in a single request during high-volume periods.
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.
Common 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.
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.
Copy this prompt to try it in Replit
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.
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.
Copy this prompt to try it in Replit
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.
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.
Copy this prompt to try it in Replit
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.
1# Python: verify token validity with a simple test request2response = requests.get(3 'https://api.hubapi.com/oauth/v1/access-tokens/' + ACCESS_TOKEN,4 headers={'Authorization': f'Bearer {ACCESS_TOKEN}'}5)6print(response.json()) # Shows token info including scopes and expiryPOST /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.
1# Python: upsert using batch endpoint2def upsert_contacts(contacts: list) -> dict:3 """contacts = [{email, firstname, lastname, ...}, ...]"""4 inputs = [5 {"properties": c, "idProperty": "email"}6 for c in contacts7 ]8 response = requests.post(9 f"{BASE_URL}/crm/v3/objects/contacts/batch/upsert",10 json={"inputs": inputs},11 headers=HEADERS12 )13 response.raise_for_status()14 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.
1# Test your webhook endpoint locally with a sample payload2# Run this in your Replit Shell to simulate a HubSpot webhook:3# curl -X POST http://localhost:3000/hubspot/webhook \4# -H 'Content-Type: application/json' \5# -d '[{"subscriptionType": "contact.creation", "objectId": 12345}]'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.
Alternatives
The HubSpot CRM API focuses on contacts, companies, deals, and pipeline management rather than marketing campaigns and email automation, making it a better fit for sales-focused integrations.
Mailchimp is easier to set up for email-only marketing campaigns and has a simpler API, making it a better choice for projects that only need email list management without the full CRM integration.
Salesforce provides enterprise-grade CRM and marketing automation with more advanced lead routing and scoring capabilities, though it is significantly more complex and expensive than HubSpot.
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.
Talk to an Expert
Our team has built 600+ apps. Get personalized help with your project.
Book a free consultation