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
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 = '<a href="http://localhost:3000">http://localhost:3000</a>';<br>
let userName = '';<br>
let userEmail = '';<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(() => {
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 => `[${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(() => {
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) => {
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() });
}
**
- 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)