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