Integration vCX with GHL
✅ Recommended Architecture
1. Push vCX Conversations into GHL
-
Create or update a Contact using
clientIdas the unique identifier -
Create a Conversation under that contact
-
Add each message as a Message object inside the conversation
All of this is supported viaPOST /conversations/{id}/messagesand related endpoints .
2. Pull GHL Replies into vCX
Set up a webhook subscription in GHL to listen for:
-
message.incoming -
conversation.updated
These webhooks will fire whenever a GHL user (or bot) replies. You can map the GHL
conversationId to your conversationId using metadata or a lookup table.OAuth 2.0 is now required for all new integrations, so you’ll need to register your app in GHL’s Developer Portal .
🔑 Key GHL API Docs You’ll Need
| Task | Endpoint | Notes |
|---|---|---|
| Create/Update Contact | POST /contacts |
Use clientId as external ID |
| Create Conversation | POST /conversations |
Link to contact |
| Send Message | POST /conversations/{id}/messages |
Includes text, type, timestamp |
| Listen for Replies | Webhook: message.incoming |
Use to sync back to vCX |
All endpoints are documented at:
🔗 https://highlevel.stoplight.io/docs/integrations
🔗 https://highlevel.stoplight.io/docs/integrations
vCX ↔ GoHighLevel – Two-Way Chat Sync (Node.js)
0. Prerequisites
- Node ≥ 18
- A GHL developer account (https://developers.gohighlevel.com)
- Your app registered in the portal with scopes:
contacts.write conversations.write conversations.read locations.read - Redirect URI set to
https://yourdomain.com/auth/callback - Environment variables:
GHL_CLIENT_ID=xxxxxxxx GHL_CLIENT_SECRET=xxxxxxxx GHL_REDIRECT_URI=https://yourdomain.com/auth/callback
1. Install dependencies
package.json (excerpt)
{
"type": "module",
"dependencies": {
"axios": "^1.6.0",
"dotenv": "^16.3.1",
"express": "^4.18.2"
}
}
npm install
2. Minimal Express server skeleton
server.js (top)
import 'dotenv/config';
import express from 'express';
import axios from 'axios';
import crypto from 'crypto';
const app = express();
app.use(express.json());
const PORT = process.env.PORT || 3000;
/* --- In-memory maps for demo purposes --- */
const tokenStore = new Map(); // locationId -> {access_token, refresh_token, expires_at}
const conversationMap = new Map(); // vcxConversationId -> ghlConversationId
app.listen(PORT, () => console.log(`Listening on :${PORT}`));
3. OAuth 2.0 – Authorization URL
GET /install
app.get('/install', (req, res) => {
const state = crypto.randomUUID();
const url = `https://marketplace.gohighlevel.com/oauth/chooselocation?response_type=code&client_id=${process.env.GHL_CLIENT_ID}&redirect_uri=${encodeURIComponent(process.env.GHL_REDIRECT_URI)}&scope=contacts.write%20conversations.write%20conversations.read%20locations.read&state=${state}`;
res.redirect(url);
});
4. OAuth 2.0 – Exchange code for tokens
GET /auth/callback
app.get('/auth/callback', async (req, res) => {
const { code, locationId } = req.query;
const { data } = await axios.post('https://services.leadconnectorhq.com/oauth/token', {
client_id: process.env.GHL_CLIENT_ID,
client_secret: process.env.GHL_CLIENT_SECRET,
grant_type: 'authorization_code',
code,
redirect_uri: process.env.GHL_REDIRECT_URI
});
tokenStore.set(locationId, {
access_token: data.access_token,
refresh_token: data.refresh_token,
expires_at: Date.now() + data.expires_in * 1000
});
res.send(`Sub-account ${locationId} connected.`);
});
5. Helper – get valid access token (auto-refresh)
async function getToken(locationId) {
let t = tokenStore.get(locationId);
if (!t) throw new Error('Location not authorized');
if (Date.now() > t.expires_at - 60_000) {
const { data } = await axios.post('https://services.leadconnectorhq.com/oauth/token', {
client_id: process.env.GHL_CLIENT_ID,
client_secret: process.env.GHL_CLIENT_SECRET,
grant_type: 'refresh_token',
refresh_token: t.refresh_token
});
t = {
access_token: data.access_token,
refresh_token: data.refresh_token,
expires_at: Date.now() + data.expires_in * 1000
};
tokenStore.set(locationId, t);
}
return t.access_token;
}
6. Upsert Contact (by vCX clientId)
POST /contact
app.post('/contact', async (req, res) => {
const { locationId, clientId, email, phone, name } = req.body;
const token = await getToken(locationId);
// Search by externalId first
const search = await axios.get(`https://rest.gohighlevel.com/v1/contacts/?query=${clientId}&locationId=${locationId}`, {
headers: { Authorization: `Bearer ${token}` }
});
let contactId = search.data.contacts?.[0]?.id;
if (!contactId) {
const { data } = await axios.post('https://rest.gohighlevel.com/v1/contacts/', {
locationId,
name,
email,
phone,
source: 'vCX',
tags: ['vCX'],
customFields: [{ id: 'clientId', value: clientId }]
}, { headers: { Authorization: `Bearer ${token}` } });
contactId = data.contact.id;
}
res.json({ contactId });
});
7. Create Conversation & Push Messages
POST /push-message
app.post('/push-message', async (req, res) => {
const { locationId, clientId, vcxConversationId, fromUser, body, timestamp } = req.body;
const token = await getToken(locationId);
// 1. Ensure contact
const { contactId } = (await axios.post(`http://localhost:${PORT}/contact`, {
locationId, clientId, email: `${clientId}@example.com`, name: clientId
})).data;
// 2. Ensure conversation
let ghlConvId = conversationMap.get(vcxConversationId);
if (!ghlConvId) {
const { data } = await axios.post('https://rest.gohighlevel.com/v1/conversations/', {
locationId,
contactId,
type: 'chat'
}, { headers: { Authorization: `Bearer ${token}` } });
ghlConvId = data.conversation.id;
conversationMap.set(vcxConversationId, ghlConvId);
}
// 3. Push message
await axios.post(`https://rest.gohighlevel.com/v1/conversations/${ghlConvId}/messages`, {
type: fromUser ? 'Inbound' : 'Outbound',
message: body,
dateAdded: new Date(timestamp).toISOString()
}, { headers: { Authorization: `Bearer ${token}` } });
res.sendStatus(200);
});
8. Receive GHL Replies via Webhook
POST /webhook
app.post('/webhook', (req, res) => {
const { type, locationId, conversationId, message } = req.body;
if (type !== 'message.incoming') return res.sendStatus(200);
// Reverse lookup
let vcxConvId;
for (const [vId, gId] of conversationMap.entries()) {
if (gId === conversationId) vcxConvId = vId;
}
if (!vcxConvId) return res.sendStatus(200);
// TODO: forward to vCX backend
console.log('Forward to vCX:', { vcxConversationId: vcxConvId, fromUser: false, body: message, timestamp: Date.now() });
res.sendStatus(200);
});
Register this URL in GHL → Settings → API → Webhooks.
9. Quick test with cURL
# 1. Start your server
node server.js
# 2. Install the app (open in browser)
open http://localhost:3000/install
# 3. Push a message
curl -X POST http://localhost:3000/push-message \
-H "Content-Type: application/json" \
-d '{"locationId":"LOC_ID","clientId":"c_abc123","vcxConversationId":"conv_456","fromUser":true,"body":"Hello from vCX","timestamp":1710000000000}'
10. Production checklist
- Use persistent storage (Redis/Postgres) instead of in-memory maps.
- Verify webhook signatures (GHL sends headers
X-Signature). - Rate-limit token refresh.
- Handle pagination when searching contacts.
- Wrap axios calls in retries with exponential backoff.
Last updated 2024-07-20
No Comments