Integration vCX with GHL
✅ Recommended Architecture
1. Push vCX Conversations into GHL
-
Create or update a Contact using
clientId
as 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}/messages
and 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