# GHL

# Integration vCX with GHL

### ✅ Recommended Architecture

#### 1. **Push vCX Conversations into GHL**

<div class="paragraph" id="bkmrk-use-the-https%3A%2F%2Fhigh">Use the [https://highlevel.stoplight.io/docs/integrations/0443d7d1a4bd0-overview](https://highlevel.stoplight.io/docs/integrations/0443d7d1a4bd0-overview) to:</div>- <div class="paragraph">Create or update a **Contact** using `clientId` as the unique identifier</div>
- <div class="paragraph">Create a **Conversation** under that contact</div>
- <div class="paragraph">Add each message as a **Message** object inside the conversation</div>

> <div class="paragraph">All of this is supported via `POST /conversations/{id}/messages` and related endpoints .</div>

#### 2. **Pull GHL Replies into vCX**

<div class="paragraph" id="bkmrk-set-up-a-webhook-sub">Set up a **webhook subscription** in GHL to listen for:</div>- <div class="paragraph">`message.incoming`</div>
- <div class="paragraph">`conversation.updated`</div>

<div class="paragraph" id="bkmrk-these-webhooks-will-">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.</div>> <div class="paragraph">OAuth 2.0 is now required for all new integrations, so you’ll need to register your app in GHL’s [Developer Portal](https://developers.gohighlevel.com/) .</div>

---

### 🔑 Key GHL API Docs You’ll Need

<div class="table markdown-table" data-v-0909cf3c="" data-v-3a4aba44="" id="bkmrk-table-copy-task-endp"><header class="table-actions" data-v-0909cf3c=""><span class="table-title" data-v-0909cf3c="">Table</span><div class="simple-button size-medium" data-v-0909cf3c="" data-v-182d5fe2="" data-v-92afdd37=""><svg aria-hidden="true" class="simple-button-icon iconify" data-v-182d5fe2="" height="16" name="Copy" role="img" viewbox="0 0 1024 1024" width="16" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><path d="M427.04896 379.12576a60.2112 60.2112 0 0 0-60.2112 60.2112v315.51488a60.2112 60.2112 0 0 0 60.2112 60.25216h315.51488a60.2112 60.2112 0 0 0 60.25216-60.2112v-315.55584a60.2112 60.2112 0 0 0-60.2112-60.2112H427.008z m-94.74048-34.48832a133.9392 133.9392 0 0 1 94.74048-39.23968h315.51488a133.98016 133.98016 0 0 1 133.98016 133.9392v315.51488a133.9392 133.9392 0 0 1-133.9392 133.98016H427.008a133.9392 133.9392 0 0 1-133.9392-133.9392v-315.55584c0-35.51232 14.09024-69.632 39.23968-94.69952z" fill="currentColor"></path><path d="M257.14688 233.472a36.16768 36.16768 0 0 0-35.96288 35.96288v364.05248a35.96288 35.96288 0 0 0 18.18624 31.21152 36.864 36.864 0 1 1-36.41344 64.1024A109.64992 109.64992 0 0 1 147.456 633.56928v-364.1344A109.89568 109.89568 0 0 1 257.14688 159.744h364.09344c20.56192 0 38.87104 5.48864 54.51776 16.83456 14.86848 10.77248 24.82176 25.10848 32.31744 38.5024a36.864 36.864 0 0 1-64.47104 35.84c-4.95616-8.97024-8.6016-12.86144-11.14112-14.66368-1.72032-1.2288-4.5056-2.78528-11.22304-2.78528h-364.1344z" fill="currentColor"></path></svg><span data-v-182d5fe2="">Copy</span></div></header><div class="table-container" data-v-0909cf3c=""><table data-v-0909cf3c=""><thead data-v-0909cf3c=""><tr data-v-0909cf3c=""><th align="left" data-v-0909cf3c="">Task</th><th align="left" data-v-0909cf3c="">Endpoint</th><th align="left" class="" data-v-0909cf3c="">Notes</th></tr></thead><tbody data-v-0909cf3c=""><tr data-v-0909cf3c=""><td align="left" class="" data-v-0909cf3c="">Create/Update Contact</td><td align="left" class="" data-v-0909cf3c="">`POST /contacts`</td><td align="left" class="" data-v-0909cf3c="">Use `clientId` as external ID</td></tr><tr data-v-0909cf3c=""><td align="left" class="" data-v-0909cf3c="">Create Conversation</td><td align="left" class="" data-v-0909cf3c="">`POST /conversations`</td><td align="left" class="" data-v-0909cf3c="">Link to contact</td></tr><tr data-v-0909cf3c=""><td align="left" class="" data-v-0909cf3c="">Send Message</td><td align="left" class="" data-v-0909cf3c="">`POST /conversations/{id}/messages`</td><td align="left" class="" data-v-0909cf3c="">Includes text, type, timestamp</td></tr><tr data-v-0909cf3c=""><td align="left" class="" data-v-0909cf3c="">Listen for Replies</td><td align="left" class="" data-v-0909cf3c="">Webhook: `message.incoming`</td><td align="left" class="" data-v-0909cf3c="">Use to sync back to vCX</td></tr></tbody></table>

</div></div><div class="paragraph" id="bkmrk-all-endpoints-are-do">All endpoints are documented at:  
🔗 [https://highlevel.stoplight.io/docs/integrations](https://highlevel.stoplight.io/docs/integrations)</div>#  

# vCX ↔ GoHighLevel – Two-Way Chat Sync (Node.js)

<details id="bkmrk-0.-prerequisites-nod"><summary>0. Prerequisites</summary>

- Node ≥ 18
- A GHL developer account ([https://developers.gohighlevel.com](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
    ```

</details><details id="bkmrk-1.-install-dependenc"><summary>1. Install dependencies</summary>

```
package.json (excerpt)
```

```
{
  "type": "module",
  "dependencies": {
    "axios": "^1.6.0",
    "dotenv": "^16.3.1",
    "express": "^4.18.2"
  }
}
```

```
npm install
```

</details><details id="bkmrk-2.-minimal-express-s"><summary>2. Minimal Express server skeleton</summary>

```
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}`));
```

</details><details id="bkmrk-3.-oauth-2.0-%E2%80%93-autho"><summary>3. OAuth 2.0 – Authorization URL</summary>

```
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);
});
```

</details><details id="bkmrk-4.-oauth-2.0-%E2%80%93-excha"><summary>4. OAuth 2.0 – Exchange code for tokens</summary>

```
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.`);
});
```

</details><details id="bkmrk-5.-helper-%E2%80%93-get-vali"><summary>5. Helper – get valid access token (auto-refresh)</summary>

```
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;
}
```

</details><details id="bkmrk-6.-upsert-contact-%28b"><summary>6. Upsert Contact (by vCX clientId)</summary>

```
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 });
});
```

</details><details id="bkmrk-7.-create-conversati"><summary>7. Create Conversation &amp; Push Messages</summary>

```
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);
});
```

</details><details id="bkmrk-8.-receive-ghl-repli"><summary>8. Receive GHL Replies via Webhook</summary>

```
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.

</details><details id="bkmrk-9.-quick-test-with-c"><summary>9. Quick test with cURL</summary>

```
# 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}'
```

</details><details id="bkmrk-10.-production-check"><summary>10. Production checklist</summary>

- 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.

</details>Last updated 2024-07-20