GuidesWeb ComponentsGoHighLevel CRM Integration

GoHighLevel CRM Integration

Send Hairscope lead data into GoHighLevel using their API v2, and trigger automations when new leads arrive.

Prerequisite: You should already have the Hairscope webhook configured and working. See the Webhook Setup Guide first.

Architecture

Hairscope Widget → Your Middleware → GoHighLevel API v2 → CRM Automations

Your middleware receives the Hairscope webhook payload and forwards it to GoHighLevel as a contact. GHL workflows then handle the automation (SMS, email, pipeline assignment, etc.).

1. Get Your GoHighLevel API Credentials

  1. In GoHighLevel, go to Settings → Business Profile → API Keys (or use a Private Integration Token from the Marketplace).
  2. Generate a new API key or create a Private Integration with contacts.write scope.
  3. Copy the token — you’ll use it as a Bearer token in API requests.
  4. Note your Location ID — every contact in GHL belongs to a location (sub-account). Find it in Settings → Business Profile or in the URL.
⚠️

Store the API key server-side only. Never expose it in frontend code.

2. API Endpoint — Create or Upsert Contact

Base URL: https://services.leadconnectorhq.com

EndpointMethodPurpose
/contacts/POSTCreate a new contact
/contacts/upsertPOSTCreate or update (recommended)

Upsert will create a new contact if no match is found, or update the existing one if a contact with the same email/phone already exists. This prevents duplicates.

Required Headers

Authorization: Bearer YOUR_API_KEY
Version: 2021-07-28
Content-Type: application/json

The Version header is required for all GHL API v2 requests.

3. Map Hairscope Payload to GHL Contact

Hairscope sends this payload to your webhook:

{
  "firstName": "John",
  "lastName": "Doe",
  "name": "John Doe",
  "email": "john@example.com",
  "phone": "+44 1234567890",
  "gender": "male",
  "age": "30",
  "clinic": "London North"
}

Map it to the GHL contact schema:

const ghlPayload = {
  locationId: process.env.GHL_LOCATION_ID,  // Required
  firstName: lead.firstName,
  lastName: lead.lastName,
  email: lead.email,
  phone: lead.phone,
  source: 'Hairscope Widget',
  tags: ['Hairscope Lead'],
  customFields: [
    { key: 'gender', field_value: lead.gender },
    { key: 'age', field_value: lead.age },
    { key: 'preferred_clinic', field_value: lead.clinic },
  ],
};

Key points:

  • locationId is required — every contact must belong to a GHL location.
  • tags is an array of strings. Use tags to trigger workflows.
  • customFields uses key (the field key you set up in GHL) and field_value.

4. Full Middleware Example

const express = require('express');
const axios = require('axios');
const app = express();
 
app.use(express.json());
 
const GHL_API_KEY = process.env.GHL_API_KEY;
const GHL_LOCATION_ID = process.env.GHL_LOCATION_ID;
const GHL_BASE_URL = 'https://services.leadconnectorhq.com';
 
app.post('/webhook/hairscope-lead', async (req, res) => {
  const lead = req.body;
 
  try {
    const response = await axios.post(
      `${GHL_BASE_URL}/contacts/upsert`,
      {
        locationId: GHL_LOCATION_ID,
        firstName: lead.firstName,
        lastName: lead.lastName,
        email: lead.email,
        phone: lead.phone,
        source: 'Hairscope Widget',
        tags: ['Hairscope Lead'],
        customFields: [
          { key: 'gender', field_value: lead.gender },
          { key: 'age', field_value: lead.age },
          { key: 'preferred_clinic', field_value: lead.clinic },
        ],
      },
      {
        headers: {
          Authorization: `Bearer ${GHL_API_KEY}`,
          Version: '2021-07-28',
          'Content-Type': 'application/json',
        },
      }
    );
 
    console.log('GHL contact created/updated:', response.data.contact?.id);
    res.status(200).json({ success: true });
  } catch (error) {
    console.error('GHL API error:', error.response?.data || error.message);
    res.status(500).json({ error: 'Failed to sync to GHL' });
  }
});
 
app.listen(3000);

5. Trigger Workflows Automatically

Once a contact is created with the tag Hairscope Lead, trigger workflows automatically.

Option A — “Contact Created” Trigger

  1. Go to Automation → Workflows → Create Workflow.
  2. Add trigger: Contact Created.
  3. Add filter: Tag is Hairscope Lead.
  4. Add your actions.

Option B — “Contact Tag Added” Trigger

  1. Create a workflow with trigger: Contact Tag Added.
  2. Set the tag filter to Hairscope Lead.
  3. Add your automation steps.

Typical Workflow

Trigger: Contact Tag Added → "Hairscope Lead"

Send SMS — "Hi {firstName}, thanks for your hair analysis!"

Send Email — Welcome + consultation booking link

Add to Pipeline — "New Leads" → Stage: "Contacted"

Notify Staff — Internal notification to clinic team

Wait: 24 hours

Send SMS — Follow-up if no booking

6. Custom Fields Setup

Before the customFields mapping works, create the fields in GoHighLevel:

  1. Go to Settings → Custom Fields.
  2. Create fields: gender, age, preferred_clinic.
  3. Note the field key for each — use that in the key property.

7. Phone Number Formatting

GHL requires E.164 international format:

✅  +447123456789
✅  +919876543210
❌  07123456789
❌  9876-543-210

Normalize numbers in your middleware before sending to GHL. Invalid formats may cause duplicate contacts or silent failures.

8. Multi-Clinic Routing

If you have multiple GHL locations (one per clinic), route leads based on the clinic field:

const CLINIC_TO_LOCATION = {
  'London North': 'loc_abc123',
  'London South': 'loc_def456',
  'Manchester': 'loc_ghi789',
};
 
const locationId = CLINIC_TO_LOCATION[lead.clinic] || process.env.GHL_DEFAULT_LOCATION_ID;

9. Error Handling

The Hairscope webhook is fire-and-forget. Your middleware should handle failures:

async function syncToGHL(lead, retries = 3) {
  for (let attempt = 1; attempt <= retries; attempt++) {
    try {
      await axios.post(/* ... */);
      return;
    } catch (error) {
      if (attempt === retries) {
        console.error('GHL sync failed after retries:', error.message);
        // Log to dead-letter queue or alert
      }
      await new Promise(r => setTimeout(r, attempt * 2000));
    }
  }
}

Common Issues

IssueFix
401 UnauthorizedCheck API key is valid and has contacts.write scope
422 UnprocessableUsually missing locationId or invalid phone format
Duplicate contactsUse /contacts/upsert instead of /contacts/
Custom fields not savingVerify the key matches exactly what’s configured in GHL
Workflow not triggeringConfirm the tag filter matches the tag you’re sending

API Reference

EndpointMethodPurpose
/contacts/POSTCreate a new contact
/contacts/upsertPOSTCreate or update (recommended)
/contacts/:idPUTUpdate an existing contact
/contacts/:idGETRetrieve a contact
/contacts/:id/tagsPOSTAdd tags to a contact

Full API docs: GoHighLevel API Documentation

Support

For Hairscope integration issues:

For GoHighLevel API issues, refer to their developer documentation or contact GHL support directly.