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 AutomationsYour 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
- In GoHighLevel, go to Settings → Business Profile → API Keys (or use a Private Integration Token from the Marketplace).
- Generate a new API key or create a Private Integration with
contacts.writescope. - Copy the token — you’ll use it as a Bearer token in API requests.
- 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
| Endpoint | Method | Purpose |
|---|---|---|
/contacts/ | POST | Create a new contact |
/contacts/upsert | POST | Create 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/jsonThe
Versionheader 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:
locationIdis required — every contact must belong to a GHL location.tagsis an array of strings. Use tags to trigger workflows.customFieldsuseskey(the field key you set up in GHL) andfield_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
- Go to Automation → Workflows → Create Workflow.
- Add trigger: Contact Created.
- Add filter: Tag is
Hairscope Lead. - Add your actions.
Option B — “Contact Tag Added” Trigger
- Create a workflow with trigger: Contact Tag Added.
- Set the tag filter to
Hairscope Lead. - 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 booking6. Custom Fields Setup
Before the customFields mapping works, create the fields in GoHighLevel:
- Go to Settings → Custom Fields.
- Create fields:
gender,age,preferred_clinic. - Note the field key for each — use that in the
keyproperty.
7. Phone Number Formatting
GHL requires E.164 international format:
✅ +447123456789
✅ +919876543210
❌ 07123456789
❌ 9876-543-210Normalize 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
| Issue | Fix |
|---|---|
| 401 Unauthorized | Check API key is valid and has contacts.write scope |
| 422 Unprocessable | Usually missing locationId or invalid phone format |
| Duplicate contacts | Use /contacts/upsert instead of /contacts/ |
| Custom fields not saving | Verify the key matches exactly what’s configured in GHL |
| Workflow not triggering | Confirm the tag filter matches the tag you’re sending |
API Reference
| Endpoint | Method | Purpose |
|---|---|---|
/contacts/ | POST | Create a new contact |
/contacts/upsert | POST | Create or update (recommended) |
/contacts/:id | PUT | Update an existing contact |
/contacts/:id | GET | Retrieve a contact |
/contacts/:id/tags | POST | Add tags to a contact |
Full API docs: GoHighLevel API Documentation
Support
For Hairscope integration issues:
- Website: https://hairscope.ai
- Support Portal: https://hairscope.ai/support
- Email: support@hairscope.ai
For GoHighLevel API issues, refer to their developer documentation or contact GHL support directly.