DEV Community

Cover image for How to Add HubSpot CRM Integration to Your Chatbot
Chatboq
Chatboq

Posted on

How to Add HubSpot CRM Integration to Your Chatbot

Chatbots have become essential tools for customer engagement, but their true power emerges when they're connected to your CRM. By integrating your chatbot with HubSpot CRM, you can automatically capture leads, log conversations, and create a seamless flow of customer data from initial contact to conversion.

In this tutorial, we'll build a practical integration that sends chatbot conversation data to HubSpot, creates or updates contacts, and tracks interactions. You'll learn how to work with HubSpot's APIs, handle authentication securely, and implement best practices for production environments.

Why Integrate Your Chatbot with HubSpot CRM?

Before diving into code, let's understand the value proposition:

Automatic lead capture: **Every chat interaction can create or update a contact in HubSpot
**Conversation tracking:
Store chat transcripts as engagement activities
Better context: Sales teams see the full conversation history before reaching out
Workflow automation: Trigger HubSpot workflows based on chatbot interactions
Data centralization: All customer touchpoints in one place
Understanding how to integrate live chat with your CRM is crucial for maximizing the value of both systems and creating a unified view of customer interactions.

Understanding HubSpot's API Structure

HubSpot provides several APIs relevant to chatbot integration:

Contacts API: Create and update contact records
Engagements API: Log activities like notes, calls, and meetings
Properties API: Manage custom contact properties
Timeline API: Add custom events to contact timelines

For our chatbot integration, we'll primarily use the Contacts API and a custom property to store conversation data.

Architecture Overview

Illustration showing the architecture of a chatbot integrated with HubSpot CRM through a backend service, visualizing data flow between chatbot, API layer, and CRM system.

Our integration follows a three-tier architecture:
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ Chatbot │────────▶│ Backend │────────▶│ HubSpot │
│ (Frontend) │ │ (Node.js) │ │ CRM │
└─────────────┘ └─────────────┘ └─────────────┘

Why not call HubSpot directly from the chatbot?

Your API token would be exposed in the browser
CORS restrictions make direct API calls difficult
You need server-side validation and error handling
Rate limiting is easier to manage from a backend
Prerequisites

Before starting, make sure you have:

A HubSpot account (free tier works fine)
Node.js installed (v14 or higher)
Basic understanding of REST APIs
A chatbot implementation (we'll use a simple example)

Tools We'll Use

Express.js: Backend server
Axios: HTTP client for API calls
dotenv: Environment variable management
@hubspot/api-client: Official HubSpot Node.js SDK (optional but recommended)

Step 1: Setting Up HubSpot Private App

HubSpot private apps provide secure API access without OAuth complexity.

Create a Private App

Navigate to Settings → Integrations → Private Apps
Click Create a private app
Name it something like "Chatbot Integration"

Go to the Scopes tab and select:
crm.objects.contacts.write
crm.objects.contacts.read
crm.schemas.contacts.write (if using custom properties)

Click Create app and copy the access token
Important: Store this token securely. You won't be able to see it again.
Create Custom Contact Properties (Optional)
If you want to store conversation transcripts:
Go to Settings → Properties → Contact properties
Click Create property
Set:
Label: "Last Chat Transcript"
Field type: Multiple line text
Internal name: last_chat_transcript
Save the property

Step 2: Setting Up the Backend

Create a new Node.js project:
mkdir chatbot-hubspot-integration
cd chatbot-hubspot-integration
npm init -y
npm install express axios dotenv cors body-parser

Create a .env file in the project root:
HUBSPOT_ACCESS_TOKEN=your_access_token_here
PORT=3000

Never commit this file to version control. Add it to .gitignore:
echo ".env" >> .gitignore

Step 3: Building the Integration Backend

Create server.js:
require('dotenv').config();
const express = require('express');
const axios = require('axios');
const cors = require('cors');
const bodyParser = require('body-parser');

const app = express();
const PORT = process.env.PORT || 3000;

// Middleware
app.use(cors());
app.use(bodyParser.json());

// HubSpot API configuration
const HUBSPOT_API_BASE = 'https://api.hubapi.com';
const HUBSPOT_TOKEN = process.env.HUBSPOT_ACCESS_TOKEN;

// Validate environment variables
if (!HUBSPOT_TOKEN) {
console.error('ERROR: HUBSPOT_ACCESS_TOKEN is not set in .env file');
process.exit(1);
}

// Headers for HubSpot API requests
const getHubSpotHeaders = () => ({
'Authorization': Bearer ${HUBSPOT_TOKEN},
'Content-Type': 'application/json'
});

app.listen(PORT, () => {
console.log(Server running on port ${PORT});
});

Step 4: Implementing Contact Creation/Update

Add this function to handle contact operations:
/**

  • Create or update a contact in HubSpot
  • @param {string} email - Contact email
  • @param {Object} properties - Additional contact properties
  • @returns {Promise} HubSpot contact object
    */
    async function createOrUpdateContact(email, properties = {}) {
    try {
    // First, try to find existing contact by email
    const searchUrl = ${HUBSPOT_API_BASE}/crm/v3/objects/contacts/search;

    const searchPayload = {
    filterGroups: [{
    filters: [{
    propertyName: 'email',
    operator: 'EQ',
    value: email
    }]
    }]
    };

    const searchResponse = await axios.post(
    searchUrl,
    searchPayload,
    { headers: getHubSpotHeaders() }
    );

    // Contact exists - update it
    if (searchResponse.data.results.length > 0) {
    const contactId = searchResponse.data.results[0].id;
    const updateUrl = ${HUBSPOT_API_BASE}/crm/v3/objects/contacts/${contactId};

    const updateResponse = await axios.patch(
    updateUrl,
    { properties },
    { headers: getHubSpotHeaders() }
    );

    return {
    success: true,
    action: 'updated',
    contact: updateResponse.data
    };
    }

    // Contact doesn't exist - create new one
    const createUrl = ${HUBSPOT_API_BASE}/crm/v3/objects/contacts;
    const createPayload = {
    properties: {
    email,
    ...properties
    }
    };

    const createResponse = await axios.post(
    createUrl,
    createPayload,
    { headers: getHubSpotHeaders() }
    );

    return {
    success: true,
    action: 'created',
    contact: createResponse.data
    };

} catch (error) {
console.error('HubSpot API Error:', error.response?.data || error.message);

return {
  success: false,
  error: error.response?.data?.message || error.message
};

}
}

Step 5: Creating the Chatbot Endpoint
Add an endpoint to receive chatbot data:
/**

  • Endpoint to receive chatbot conversation data
    */
    app.post('/api/chatbot/conversation', async (req, res) => {
    try {
    const { email, name, transcript, metadata } = req.body;

    // Validate required fields
    if (!email) {
    return res.status(400).json({
    success: false,
    error: 'Email is required'
    });
    }

    // Validate email format
    const emailRegex = /^[^\s@]+@[^\s@]+.[^\s@]+$/;
    if (!emailRegex.test(email)) {
    return res.status(400).json({
    success: false,
    error: 'Invalid email format'
    });
    }

    // Prepare contact properties
    const properties = {};

    if (name) {
    // Split name into first and last name
    const nameParts = name.trim().split(' ');
    properties.firstname = nameParts[0];
    if (nameParts.length > 1) {
    properties.lastname = nameParts.slice(1).join(' ');
    }
    }

    // Add conversation transcript if provided
    if (transcript) {
    properties.last_chat_transcript = transcript;
    }

    // Add metadata as custom properties if needed
    if (metadata?.source) {
    properties.lead_source = metadata.source;
    }

    // Create or update contact
    const result = await createOrUpdateContact(email, properties);

    if (result.success) {
    return res.status(200).json({
    success: true,
    action: result.action,
    contactId: result.contact.id,
    message: Contact ${result.action} successfully
    });
    } else {
    return res.status(500).json({
    success: false,
    error: result.error
    });
    }

} catch (error) {
console.error('Server Error:', error);
return res.status(500).json({
success: false,
error: 'Internal server error'
});
}
});

Step 6: Building a Simple Chatbot Frontend

Create index.html:
<!DOCTYPE html>




Chatbot with HubSpot Integration
<br> body {<br> font-family: Arial, sans-serif;<br> max-width: 400px;<br> margin: 50px auto;<br> padding: 20px;<br> }<br> #chat-container {<br> border: 1px solid #ccc;<br> height: 400px;<br> overflow-y: auto;<br> padding: 15px;<br> margin-bottom: 15px;<br> background: #f9f9f9;<br> }<br> .message {<br> margin: 10px 0;<br> padding: 8px 12px;<br> border-radius: 8px;<br> max-width: 80%;<br> }<br> .user {<br> background: #007bff;<br> color: white;<br> margin-left: auto;<br> text-align: right;<br> }<br> .bot {<br> background: #e9ecef;<br> color: #333;<br> }<br> #user-input {<br> width: 70%;<br> padding: 10px;<br> border: 1px solid #ccc;<br> border-radius: 4px;<br> }<br> button {<br> padding: 10px 20px;<br> background: #007bff;<br> color: white;<br> border: none;<br> border-radius: 4px;<br> cursor: pointer;<br> }<br> button:hover {<br> background: #0056b3;<br> }<br> .info-form {<br> margin-bottom: 20px;<br> padding: 15px;<br> background: #fff3cd;<br> border-radius: 4px;<br> }<br> .info-form input {<br> width: 100%;<br> padding: 8px;<br> margin: 5px 0;<br> border: 1px solid #ccc;<br> border-radius: 4px;<br> }<br>


Customer Support Chat


Please provide your details to start:




Start Chat



Send
End Chat

<br> const API_URL = &#39;<a href="http://localhost:3000">http://localhost:3000</a>&#39;;<br> let userName = &#39;&#39;;<br> let userEmail = &#39;&#39;;<br> let conversationHistory = [];</p> <div class="highlight"><pre class="highlight plaintext"><code>function startChat() { userName = document.getElementById('user-name').value.trim(); userEmail = document.getElementById('user-email').value.trim(); if (!userName || !userEmail) { alert('Please enter both name and email'); return; } // Validate email format const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; if (!emailRegex.test(userEmail)) { alert('Please enter a valid email address'); return; } document.getElementById('info-section').style.display = 'none'; document.getElementById('chat-section').style.display = 'block'; addMessage('bot', `Hi ${userName}! How can I help you today?`); conversationHistory.push({ role: 'bot', message: `Hi ${userName}! How can I help you today?`, timestamp: new Date().toISOString() }); } function addMessage(sender, text) { const container = document.getElementById('chat-container'); const messageDiv = document.createElement('div'); messageDiv.className = `message ${sender}`; messageDiv.textContent = text; container.appendChild(messageDiv); container.scrollTop = container.scrollHeight; } function sendMessage() { const input = document.getElementById('user-input'); const message = input.value.trim(); if (!message) return; addMessage('user', message); conversationHistory.push({ role: 'user', message: message, timestamp: new Date().toISOString() }); input.value = ''; // Simple bot response (in production, this would be more sophisticated) setTimeout(() =&gt; { const response = getBotResponse(message); addMessage('bot', response); conversationHistory.push({ role: 'bot', message: response, timestamp: new Date().toISOString() }); }, 500); } function getBotResponse(message) { const lowerMessage = message.toLowerCase(); if (lowerMessage.includes('price') || lowerMessage.includes('cost')) { return 'Our pricing starts at $29/month. Would you like to schedule a demo?'; } else if (lowerMessage.includes('demo') || lowerMessage.includes('trial')) { return 'Great! I can help you set up a demo. A team member will reach out soon.'; } else if (lowerMessage.includes('feature')) { return 'We offer integrations, analytics, and 24/7 support. What specific feature interests you?'; } else { return 'Thanks for your question! Let me connect you with someone who can help.'; } } async function endChat() { // Format transcript const transcript = conversationHistory .map(entry =&gt; `[${entry.role.toUpperCase()}]: ${entry.message}`) .join('\n'); try { const response = await fetch(`${API_URL}/api/chatbot/conversation`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ email: userEmail, name: userName, transcript: transcript, metadata: { source: 'website_chat', chatDuration: conversationHistory.length, endedAt: new Date().toISOString() } }) }); const data = await response.json(); if (data.success) { addMessage('bot', 'Thank you for chatting! Your conversation has been saved.'); setTimeout(() =&gt; { alert('Chat ended. Your information has been saved to our CRM.'); location.reload(); }, 2000); } else { console.error('Error saving to HubSpot:', data.error); alert('Chat ended, but there was an error saving your information.'); } } catch (error) { console.error('Network error:', error); alert('Chat ended, but there was a connection error.'); } } // Allow Enter key to send messages document.getElementById('user-input').addEventListener('keypress', (e) =&gt; { if (e.key === 'Enter') sendMessage(); }); </code></pre></div> <p>

Step 7: Testing the Integration

Start your server:
node server.js

Open index.html in your browser and test the flow:
Enter a name and email
Have a conversation
Click "End Chat"
Check HubSpot CRM for the new contact
You should see:
A new contact with the email you provided
First and last name populated
The conversation transcript in the custom property

Security Best Practices

1. Never Expose API Tokens

// ❌ NEVER do this
const token = 'pat-na1-xxxxx';

// ✅ Always use environment variables
const token = process.env.HUBSPOT_ACCESS_TOKEN;

2. Implement Rate Limiting

HubSpot has API rate limits. Install and use express-rate-limit:
npm install express-rate-limit

const rateLimit = require('express-rate-limit');

const limiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100, // limit each IP to 100 requests per windowMs
message: 'Too many requests from this IP, please try again later.'
});

app.use('/api/', limiter);

3. Validate All Input

function validateContactData(data) {
const { email, name, transcript } = data;

// Email validation
if (!email || !/^[^\s@]+@[^\s@]+.[^\s@]+$/.test(email)) {
return { valid: false, error: 'Invalid email' };
}

// Name validation (optional but recommended)
if (name && name.length > 100) {
return { valid: false, error: 'Name too long' };
}

// Transcript size limit
if (transcript && transcript.length > 65536) {
return { valid: false, error: 'Transcript too large' };
}

return { valid: true };
}

4. Handle API Errors Gracefully

async function safeHubSpotCall(apiFunction) {
try {
return await apiFunction();
} catch (error) {
// Check for specific HubSpot errors
if (error.response?.status === 429) {
console.error('Rate limit exceeded');
// Implement exponential backoff
await new Promise(resolve => setTimeout(resolve, 5000));
return safeHubSpotCall(apiFunction);
}

if (error.response?.status === 401) {
  console.error('Invalid API token');
  // Alert administrators
}

throw error;

}
}

Common Pitfalls and Solutions

1. Duplicate Contacts

Problem: Creating multiple contacts for the same email.
Solution: Always search before creating:
// The createOrUpdateContact function we built handles this
// by searching first, then creating or updating

2. Lost Conversations During Server Restart

Problem: In-memory conversation data is lost if the server restarts.
Solution: Use a database or session storage:
// Example with a simple file-based storage
const fs = require('fs').promises;

async function saveConversation(sessionId, data) {
await fs.writeFile(
./sessions/${sessionId}.json,
JSON.stringify(data)
);
}

3. API Token in Client Code

Problem: Exposing your HubSpot token in frontend JavaScript.
Solution: Never call HubSpot directly from the browser. Always use a backend proxy.

4. Property Name Mismatches

Problem: Using incorrect property names causes silent failures.
Solution: List available properties programmatically:
async function getContactProperties() {
const url = ${HUBSPOT_API_BASE}/crm/v3/properties/contacts;
const response = await axios.get(url, { headers: getHubSpotHeaders() });
return response.data.results.map(prop => prop.name);
}

Advanced Features

Adding Engagement Tracking
Create a note in HubSpot for each conversation:
async function createEngagementNote(contactId, transcript) {
const url = ${HUBSPOT_API_BASE}/crm/v3/objects/notes;

const payload = {
properties: {
hs_timestamp: Date.now(),
hs_note_body: transcript,
hubspot_owner_id: null
},
associations: [{
to: { id: contactId },
types: [{
associationCategory: 'HUBSPOT_DEFINED',
associationTypeId: 202 // Note to Contact
}]
}]
};

const response = await axios.post(
url,
payload,
{ headers: getHubSpotHeaders() }
);

return response.data;
}

Update the conversation endpoint:
// After creating/updating contact
if (result.success && transcript) {
await createEngagementNote(result.contact.id, transcript);
}

Triggering HubSpot Workflows
Set a specific property value to trigger workflows:
// In your contact properties
properties.chatbot_interaction = 'completed';
properties.lead_status = 'new';

Then create a workflow in HubSpot that triggers when chatbot_interaction equals completed.

Real-World Use Cases

1. Lead Qualification
function analyzeChatIntent(transcript) {
const highIntentKeywords = ['demo', 'pricing', 'buy', 'purchase', 'trial'];
const hasHighIntent = highIntentKeywords.some(keyword =>
transcript.toLowerCase().includes(keyword)
);

return hasHighIntent ? 'hot_lead' : 'warm_lead';
}

// Add to contact properties
properties.lead_temperature = analyzeChatIntent(transcript);

This automated lead qualification process helps sales teams prioritize follow-ups. If you're looking to prevent leads from slipping through the cracks, consider implementing strategies to stop missing leads with chatbots that capture and qualify prospects 24/7.

2. Customer Support Ticket Creation

async function createSupportTicket(contactId, issue) {
const url = ${HUBSPOT_API_BASE}/crm/v3/objects/tickets;

const payload = {
properties: {
hs_pipeline: '0',
hs_pipeline_stage: '1',
hs_ticket_priority: 'MEDIUM',
subject: 'Chat Support Request',
content: issue
},
associations: [{
to: { id: contactId },
types: [{
associationCategory: 'HUBSPOT_DEFINED',
associationTypeId: 16 // Ticket to Contact
}]
}]
};

return await axios.post(url, payload, { headers: getHubSpotHeaders() });
}
**

  1. Abandoned Chat Recovery**

Track when users start but don't complete a chat:
app.post('/api/chatbot/abandoned', async (req, res) => {
const { email, partialTranscript, abandonedAt } = req.body;

const properties = {
chat_abandoned: 'true',
last_chat_transcript: partialTranscript,
abandoned_timestamp: abandonedAt
};

await createOrUpdateContact(email, properties);

// This can trigger a follow-up workflow in HubSpot
res.json({ success: true });
});

Monitoring and Debugging
Add logging middleware to track API calls:
const morgan = require('morgan');
app.use(morgan('combined'));

// Custom logging for HubSpot calls
function logHubSpotCall(endpoint, method, success) {
console.log([HubSpot API] ${method} ${endpoint} - ${success ? 'SUCCESS' : 'FAILED'});
}

Add health check endpoint:
app.get('/api/health', async (req, res) => {
try {
// Test HubSpot connection
const url = ${HUBSPOT_API_BASE}/crm/v3/objects/contacts?limit=1;
await axios.get(url, { headers: getHubSpotHeaders() });

res.json({
  status: 'healthy',
  hubspot: 'connected',
  timestamp: new Date().toISOString()
});

} catch (error) {
res.status(500).json({
status: 'unhealthy',
hubspot: 'disconnected',
error: error.message
});
}
});

Effective monitoring is essential for maintaining reliable integrations. For broader insights on tracking performance, explore metrics for live chat success to understand which KPIs matter most for your chatbot and CRM integration.

Conclusion

You now have a working chatbot integration with HubSpot CRM that can:
Automatically create and update contacts
Store conversation transcripts
Track lead sources and metadata
Handle errors gracefully

Protect sensitive API credentials

This integration forms the foundation for more advanced features like automated lead scoring, workflow triggers, and personalized follow-ups. The key is keeping your API tokens secure, validating all inputs, and handling HubSpot's rate limits appropriately.
Remember to test thoroughly in HubSpot's sandbox environment before deploying to production, and always monitor your API usage to stay within rate limits.
What specific chatbot-to-CRM integration challenges have you encountered? I'd love to hear about your use cases in the comments.

Top comments (0)