Where to find the GoHighLevel API documentation
GHL's official reference is the HighLevel API documentation. It opens on v3, and each endpoint page lists the scope and headers that call needs.
The developer portal, developers.gohighlevel.com, also still links the old API 1.0 docs, so check which set you are reading.
GoHighLevel API v1, v2 and v3: what is current
GHL uses three labels, and they mean different things.
- API v1 is the old API that used API keys. GHL says it reached end of support on December 31, 2025. Old v1 integrations still run, but get no support or updates.
- API v2 (API 2.0) is what GHL's help center calls the current OAuth-based API. It runs at
services.leadconnectorhq.com. - v3 is the newest
Versionheader value, released June 11, 2026. Older values are dates, such as2021-07-28.
What changed in v3
GHL's help center says "API v3 is now available for supported endpoints." So check each endpoint page for the Version value it accepts.
The v3 reference shows four things:
- Same base URL and tokens. v3 pages accept a sub-account OAuth token or a sub-account Private Integration token.
- Mostly the same paths. Contact, calendar event and opportunity search paths stay the same. The location token call moved from
/oauth/locationTokento/oauth/location-token. - Some new parameter names. Opportunity search takes
location_idin 2021-07-28, butlocationIdin v3. - A new response format. GHL's v3 overview pages say v3 uses a new response style, so some responses differ from 2021-07-28. Compare each response before you switch.
What agencies should use now
For new builds, send Version: v3 on every endpoint that lists it. Copy the value from the endpoint page.
For existing builds on 2021-07-28, there is no deadline yet. GHL's versioning page lists every older version's end date as TBD. Still, the 2021-07-28 docs now say they are "no longer actively maintained."
Once GHL retires a version, it rejects requests that send it. GHL recommends moving to the latest version before that date. Test each call on v3 first, since responses can differ.
Two ways to authenticate
GHL supports two types of authorization: Private Integration tokens and OAuth 2.0. Both send the token as Authorization: Bearer <token>.
Private Integration tokens: internal tools, one sub-account
A Private Integration token is a static OAuth2 access token that you create inside GHL. GHL suggests it for internal jobs, such as data sync, reporting dashboards and automated tasks. It fits when you need one sub-account at a time and no webhooks.
To create one:
- 01Open Settings → Private Integrations in the agency or a sub-account. If the agency menu lacks it, turn it on in Labs.
- 02Create a new integration and give it a name and description.
- 03Select only the scopes it needs.
- 04Copy the token right away. GHL shows it only once.
GHL allows up to 20 tokens at the agency level, and 20 at the sub-account level. Tokens do not refresh on their own, and GHL recommends rotating them every 90 days.
"Rotate and expire this token later" keeps old and new tokens working for 7 days. If a token leaks, choose "Rotate and expire this token now."
OAuth 2.0 marketplace apps: many sub-accounts, webhooks, public apps
Use OAuth 2.0 when one integration serves many sub-accounts, needs webhooks or will go public.
The flow:
- 01Create an app in the GHL Marketplace. GHL recommends starting it as Private.
- 02Set scopes and a redirect URL, then generate a Client ID and Client Secret. GHL shows the secret only once.
- 03Install the app from its Installation URL. GHL redirects to your URL with a
codequery parameter. - 04POST that code to
/oauth/tokenwith your client ID and secret,grant_type=authorization_codeand auser_type. The v3 reference also requiresVersion: v3on this call.
Access tokens last about a day (expires_in is 86399 seconds). Refresh tokens last up to a year, or until used. Each refresh returns a new refresh token and ends the old one.
Company (agency) tokens call agency APIs, such as creating sub-accounts. Location tokens call sub-account APIs, such as contacts.
To turn a Company token into a Location token, call POST /oauth/location-token with companyId and locationId. In v3, this call needs the oauth.write scope and does not accept Private Integration tokens.
Which one should you pick?
| Your job | Use |
|---|---|
| Sync one client's CRM to a dashboard | Private Integration token |
| Run one integration across many sub-accounts | OAuth marketplace app |
Receive events such as ContactCreate | OAuth marketplace app |
| Add custom workflow actions or triggers | OAuth marketplace app |
GHL API base URL, headers and a first request
Requests go to https://services.leadconnectorhq.com. Send these headers:
Authorization: Bearer <token>Version: v3, or the value the endpoint page listsContent-Type: application/jsonwhen you send a body
Many sub-account calls also need the sub-account's ID, usually as locationId. Find it under Settings → Business Profile, or in the browser URL after /location/.
Example: upsert a contact with a Private Integration token
This call creates a contact or updates a match. It needs the contacts.write scope and sends Version: v3. Heads-up: GHL's Private Integrations page still shows 2021-07-28 in its examples.
curl -X POST 'https://services.leadconnectorhq.com/contacts/upsert' \
-H 'Authorization: Bearer YOUR_PRIVATE_INTEGRATION_TOKEN' \
-H 'Version: v3' \
-H 'Content-Type: application/json' \
--data-raw '{
"locationId": "YOUR_LOCATION_ID",
"firstName": "Jane",
"email": "[email protected]",
"phone": "+15555550123",
"source": "website form"
}'For a simple read test, try GET /contacts/{contactId} with the contacts.readonly scope.
The main objects
| Object | What you can do | Example |
|---|---|---|
| Contacts | Create, get, update, delete, upsert and search, plus tags, notes and tasks | POST /contacts/upsert (contacts.write) |
| Opportunities | Create, update, upsert, change status and search; manage pipelines | GET /opportunities/search (opportunities.readonly) |
| Calendars | Calendars, groups, appointments, blocked slots and availability | GET /calendars/events (calendars/events.readonly) |
| Conversations | Create and read conversations, send messages, add inbound messages, upload attachments | "Send a new message" |
| Custom fields | Create, update and delete fields and folders; list fields by object key | Custom Fields V2 API |
| Custom objects | Schemas for contact, opportunity, business and custom objects; records for business and custom objects | Objects API |
| Webhooks | Real-time events, 50+ types | ContactCreate, ContactUpdate |
GoHighLevel API rate limits
GHL's rate limits page covers API 2.0 calls made with OAuth:
- Burst: 100 requests per 10 seconds
- Daily: 200,000 requests per day
Limits count per marketplace app, per resource. A resource is one sub-account (Location) or one agency (Company). More installs do not split your allowance, because each install gets its own budget.
Each response carries headers that show where you stand:
| Header | What it tells you |
|---|---|
X-RateLimit-Limit-Daily | Your daily limit |
X-RateLimit-Daily-Remaining | Requests left today |
X-RateLimit-Interval-Milliseconds | Length of the burst window |
X-RateLimit-Max | Most requests allowed per burst window |
X-RateLimit-Remaining | Requests left in the current window |
GHL says to read these headers instead of counting requests yourself. Slow down as X-RateLimit-Remaining nears zero.
Webhooks: marketplace apps vs workflow webhooks
GHL has two kinds of webhooks, and they solve different problems.
Marketplace app webhooks
These send events, such as ContactCreate, to your server for each sub-account that installs your app. They need an OAuth app, since GHL steers Private Integration tokens toward jobs without webhooks.
First, add the scopes your events need. Then open the webhook section under Advanced Settings, turn on each event and paste your URL. Events and URLs can change at any time, but scopes change only in a draft version.
Three rules from GHL's webhook guide keep delivery healthy:
- Verify signatures. Check
X-GHL-Signature(Ed25519) and reject requests that fail. GHL gave September 1, 2026 as the deprecation date for the legacyX-WH-Signature. - Reply fast. Return a 2xx right away and do heavy work in the background. GHL retries a non-2xx or missing response up to 12 times.
- Watch your success rate. GHL checks URLs that get over 10,000 webhooks in 3 days. Below 90% success, you get a warning. Fail the next check, and delivery pauses.
A timeout can trigger a retry of work you already did. Make your handler safe to run twice.
Workflow webhooks
These are workflow actions and triggers inside one sub-account.
- Webhook (Outbound) action: sends contact data to a URL, by POST by default.
- Custom Webhook action: sends GET, POST, PUT or DELETE requests, with Bearer, API key, Basic Auth or OAuth2.
- Inbound Webhook trigger: starts a workflow when another system sends JSON to the trigger's own URL.
GHL calls Inbound Webhook a premium trigger. Each sub-account gets 100 free executions, so check GHL's premium trigger article for costs past that.
Use workflow webhooks for logic inside one account. Use marketplace webhooks when your code must hear from many accounts.
Common mistakes, and how to avoid them
Using the wrong token or location
The Contacts, Calendars, Conversations and Opportunities docs call for a sub-account token. That means a Location OAuth token or a sub-account Private Integration token.
Holding a Company token? Exchange it at the location token endpoint first. Then send the locationId of the sub-account that token belongs to.
Missing scopes
Endpoint pages list the scope each call needs, such as contacts.readonly to read a contact. You can edit a Private Integration's scopes, and the token keeps working. A live marketplace app needs a new draft first.
Letting tokens expire
OAuth access tokens last a day. After each refresh, save both the new access token and the new refresh token. GHL suggests one wrapper for every call: send, refresh on expiry, save, retry.
Creating duplicate contacts
Use upsert for form and sync traffic. It follows the sub-account's "Allow Duplicate Contact" setting, under Settings → Business Profile → Contact Deduplication Preferences.
To check first, call GET /contacts/search/duplicate with locationId, plus email or number.
Ignoring pagination
Opportunity search takes limit (default 20, max 100) and page. Its meta object returns startAfter, startAfterId and nextPageUrl for the next page.
The 2021-07-28 docs mark GET /contacts/ as deprecated. Use Search Contacts, POST /contacts/search, instead.