Skip to main content

Integration vCX with GHL

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 via POST /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

Table
Copy
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

 

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