📇

GoHighLevel CRM

Verified

by OpenClaw Launch

Integrate with GoHighLevel CRM to create and manage contacts, send SMS and email messages, track deals through pipelines, and book appointments via the GHL API v2. Designed for service businesses that use GoHighLevel as their CRM.

gohighlevelghlcrmcontactspipelinecalendarsmsappointmentshighlevel

GoHighLevel CRM

Integrate with GoHighLevel to manage contacts, send messages (SMS/email), track deals through pipelines, and book appointments. Uses the GHL API v2 with a Private Integration Token.

Credentials

Store the GoHighLevel Private Integration Token and Location ID in a file in your workspace and read them from there on every call. Use a file named .ghl_credentials.json in your workspace directory, shaped like:

{"token": "<your-private-integration-token>", "locationId": "<your-location-id>"}

Load them before making requests. In a Node/Python script, read and parse .ghl_credentials.json from your workspace (this is the most reliable way). In a shell, if jq is available: GHL_TOKEN=$(jq -r .token .ghl_credentials.json); GHL_LOC=$(jq -r .locationId .ghl_credentials.json) — otherwise read the file in a small script instead of assuming jq exists.

Before asking the user for anything, CHECK whether credentials are already saved:

  1. Read .ghl_credentials.json from your workspace if it exists, and use it.
  2. Otherwise look for a token you saved earlier in the workspace (any file matching *ghl*token*, *gohighlevel*token*, or *highlevel*token* — e.g. tmp_active_ghl_token.local). If found, use it and copy it into .ghl_credentials.json so future calls are consistent. If that file holds only a token (no Location ID), use the token, ask the user only for the Location ID, and write both fields.

Only if no saved token is found at all, ask the user IN THE CHAT for their GoHighLevel Private Integration Token (Settings > Business Profile > Integrations > Private Integration Token) and their Location ID (Settings > Business Info, or the value after /location/ in the GoHighLevel URL), then write both to .ghl_credentials.json.

IMPORTANT — never ask the user to set environment variables, edit configuration files (openclaw.json / config.yaml), paste the token into the terminal, or restart the bot / gateway. None of that is needed or possible on a hosted bot, and the gateway does not expose these as environment variables to scripts. Do NOT rely on process.env.GHL_API_KEY / process.env.GHL_TOKEN — always read the workspace file.

If a GHL API call returns 401 (or a 403 whose body does NOT mention Cloudflare error 1010 / browser_signature_banned), the saved token has rotated or expired: ask the user in the chat for a fresh Private Integration Token, overwrite .ghl_credentials.json with it, and retry. No restart is needed. But a 403 with an error-1010 / browser_signature_banned body is NOT a token problem — see the User-Agent note under API Reference; just add the User-Agent header and retry.

API Reference

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

Required headers for every request:

Authorization: Bearer GHL_API_TOKEN
Version: 2021-07-28
Content-Type: application/json
User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/148.0.0.0 Safari/537.36

CRITICAL — send the User-Agent header above on EVERY GoHighLevel request, in every language (curl, Node fetch/axios, Python urllib/requests — all of them). GoHighLevel is behind Cloudflare, which blocks automation clients that send their default signature (Python Python-urllib / python-requests, etc.) with HTTP 403 and a body containing error_code 1010 / browser_signature_banned, BEFORE your token is ever checked. A 403 whose body mentions 1010 or browser_signature_banned is NOT a bad or expired token — it means the request had no User-Agent (or a banned one). Add the browser User-Agent above and retry the same call; do NOT ask the user for a new token and do NOT fall back to a browser.

Note: Calendar endpoints use Version: 2021-04-15 instead.

Contacts

Create a contact:

curl -X POST https://services.leadconnectorhq.com/contacts/ \
  -H "Authorization: Bearer $GHL_TOKEN" \
  -H "Version: 2021-07-28" \
  -H "Content-Type: application/json" \
  -d '{"firstName":"John","lastName":"Doe","email":"[email protected]","phone":"+1234567890","locationId":"LOC_ID","tags":["lead"],"source":"OpenClaw Bot"}'

Upsert contact (create or update by email/phone -- avoids duplicates):

POST /contacts/upsert

Same body as create. Use this instead of create when you are unsure if the contact already exists.

Search contacts:

GET /contacts/?locationId=LOC_ID&[email protected]
GET /contacts/?locationId=LOC_ID&phone=+1234567890

Update contact:

PUT /contacts/{contactId}

Body: partial fields only, e.g. {"tags":["qualified"]}

Add/remove tags:

POST   /contacts/{contactId}/tags   body: {"tags":["tag1","tag2"]}
DELETE /contacts/{contactId}/tags   body: {"tags":["tag1"]}

Add contact to workflow:

POST /contacts/{contactId}/workflow/{workflowId}

Conversations & Messaging

Send SMS:

curl -X POST https://services.leadconnectorhq.com/conversations/messages \
  -H "Authorization: Bearer $GHL_TOKEN" \
  -H "Version: 2021-07-28" \
  -H "Content-Type: application/json" \
  -d '{"type":"SMS","contactId":"CONTACT_ID","message":"Your appointment is confirmed for tomorrow at 10am."}'

Send email:

POST /conversations/messages
body: {"type":"Email","contactId":"CONTACT_ID","subject":"Appointment Confirmation","html":"<p>Your appointment is confirmed.</p>"}

Message types: SMS, Email, WhatsApp, FB, IG, Live_Chat, GMB

Get conversation messages:

GET /conversations/{conversationId}/messages

Opportunities / Pipeline

First, get your pipeline and stage IDs:

GET /opportunities/pipelines?locationId=LOC_ID

This returns all pipelines with their stages. Save the pipeline ID and stage IDs.

Create opportunity (deal):

curl -X POST https://services.leadconnectorhq.com/opportunities/ \
  -H "Authorization: Bearer $GHL_TOKEN" \
  -H "Version: 2021-07-28" \
  -H "Content-Type: application/json" \
  -d '{"title":"Cleaning - 123 Main St","pipelineId":"PIPELINE_ID","stageId":"STAGE_ID","contactId":"CONTACT_ID","locationId":"LOC_ID","monetaryValue":350,"status":"open"}'

Update opportunity stage (move deal forward):

PUT /opportunities/{opportunityId}
body: {"stageId":"NEW_STAGE_ID"}

Mark deal as won/lost:

PUT /opportunities/{opportunityId}/status
body: {"status":"won"}

Status values: open, won, lost, abandoned

Search opportunities:

GET /opportunities/search?location_id=LOC_ID&pipeline_id=PIPELINE_ID

Calendars & Appointments

IMPORTANT: Calendar endpoints use Version: 2021-04-15

Get all calendars:

GET /calendars/?locationId=LOC_ID

Check available time slots:

GET /calendars/{calendarId}/free-slots?startDate=1713600000000&endDate=1714204800000&timezone=America/New_York

Dates are Unix timestamps in milliseconds.

Book an appointment:

curl -X POST https://services.leadconnectorhq.com/calendars/events/appointments \
  -H "Authorization: Bearer $GHL_TOKEN" \
  -H "Version: 2021-04-15" \
  -H "Content-Type: application/json" \
  -d '{"calendarId":"CAL_ID","locationId":"LOC_ID","contactId":"CONTACT_ID","startTime":"2026-04-20T14:00:00Z","endTime":"2026-04-20T15:00:00Z","title":"Cleaning Service","appointmentStatus":"confirmed","address":"123 Main St","toNotify":true}'

Update appointment:

PUT /calendars/events/appointments/{eventId}

Cancel appointment:

PUT /calendars/events/appointments/{eventId}
body: {"appointmentStatus":"cancelled"}

Examples

  • "A new client just called asking for a house cleaning quote at 123 Main St. Add them to GoHighLevel and create a deal in my pipeline."
  • "Check my calendar availability for next Tuesday and book a painting estimate for the client we just spoke with."
  • "Send an SMS to the client confirming their appointment tomorrow at 2pm."
  • "Move the cleaning job for John Doe to the 'Completed' stage and mark it as won."

Guidelines

  • Always use /contacts/upsert instead of /contacts/ to avoid creating duplicate contacts
  • Before creating an opportunity, call GET /opportunities/pipelines to get valid pipeline and stage IDs
  • Before booking an appointment, call GET /calendars/{calendarId}/free-slots to verify the slot is available
  • Use the contact's phone number as the primary identifier when upserting (most reliable for service businesses)
  • Store the GHL API token and location ID in the .ghl_credentials.json workspace file (see Credentials above) -- never use environment variables (they are not exposed to scripts on a hosted bot), never hardcode them, and never include them in conversation logs
  • If a request returns 429 (rate limited), wait and retry with exponential backoff
  • ALWAYS set the browser User-Agent header (see API Reference) on every request -- GoHighLevel's Cloudflare returns HTTP 403 error 1010 (browser_signature_banned) for automation clients that send no User-Agent (Python urllib/requests defaults are blocked)
  • If a request returns 401 (or a 403 that is NOT a Cloudflare error-1010 / browser_signature_banned response), the saved token has rotated/expired -- ask the user in the chat for a fresh Private Integration Token and overwrite .ghl_credentials.json (no restart needed). A 403 with a 1010 / browser_signature_banned body means a missing/banned User-Agent, NOT a bad token -- add the User-Agent header and retry instead
  • Always include "source": "OpenClaw Bot" when creating contacts so the user can track bot-generated leads in GHL
  • Calendar endpoints require Version: 2021-04-15 header, all other endpoints use Version: 2021-07-28
  • When the user asks to "send a message", default to SMS unless they specify email or another channel