Legacy Company API Keys in GoHighLevel stopped getting updates the moment HighLevel rolled out API v2. If you’re trying to sync contacts, pull appointments, or fire workflows from a custom backend, you don’t need to build a full public OAuth app with token refresh rotators. You just need a Private Integration.
Here’s how to create a Private Integration in the Marketplace Developer Portal, set up the right scopes, install it to your target sub-account, grab your Location ID, and send your first authenticated API v2 request.

Why API v2 Replaced Legacy Company API Keys
For years, GHL integrations ran on a single Company API Key tucked under Sub-Account Settings. It was convenient, but risky: every key had full admin access, couldn’t be scoped down, and offered zero auditability.
HighLevel deprecated those v1 keys for API v2. Public apps solve this with OAuth 2.0, but managing redirect URLs, auth code exchanges, and refreshing tokens every 24 hours is unnecessary overhead for internal tools. A Private Integration gives you a long-lived Bearer token scoped down strictly to what your backend actually needs.
If you’re wondering why an old integration script suddenly throws 401 Unauthorized against v2 endpoints, this is why: API v2 endpoints outright reject legacy v1 keys. You need a Bearer token from an app or Private Integration.
Step 1: Access the HighLevel Marketplace Developer Portal
Private Integrations live inside the HighLevel Marketplace console, not your sub-account settings. You’ll need agency admin access to open the developer tools.
- Sign in to your HighLevel Agency Dashboard.
- In the left sidebar, click Marketplace (or go directly to the GoHighLevel Marketplace).
- Click Developers or My Apps in the top-right corner.
- Click Create App.
- When asked for the distribution type, choose Private.
Don’t pick “Public” unless you’re building a multi-tenant integration for other agencies to install. A Private Integration stays locked to your agency and whichever sub-accounts you explicitly grant.
Step 2: Configure App Details and Scopes
Give your integration a name (like custom-crm-sync) and a quick description. HighLevel requires these before unlocking the permissions tab.
Switch to the Scopes tab. This is where API v2 enforces access control. If your script hits an endpoint without the required scope enabled here, HighLevel throws a 403 Forbidden with an Insufficient Scope error payload.
Here are the scopes I routinely check for backend sync scripts:
contacts.readonlyandcontacts.write— Query, create, update, and delete contacts.calendars.readonlyandcalendars.write— Pull appointment slots and schedule bookings.conversations.readonlyandconversations.message.write— Send SMS and email replies directly in conversation threads.workflows.readonly— Inspect triggers and execution statuses.locations.readonly— Read sub-account metadata, timezone, and business settings.
Only check the permissions your script actually executes. You can add more later, but doing so requires reinstalling or updating the token permissions on the sub-account.
Step 3: Install the Private Integration on Your Sub-Account
Saving your app doesn’t automatically activate it. You need to explicitly install it on the sub-account containing your target data.
- Inside your app settings in the Developer Portal, go to Private Integration Keys or Install App.
- Pick your target sub-account (location) from the dropdown.
- Review the permissions and click Authorize & Install.
- HighLevel generates a Bearer token (prefixed with
pit-in recent portal builds). - Copy it immediately into your server’s
.envfile.
If you’re pairing this with automated triggers that send data outbound to your servers, check out our guide on how to send custom values and contact fields in GoHighLevel webhooks.
Step 4: Find Your GoHighLevel Location ID
Virtually every API v2 request requires a locationId parameter. Leave it out or pass an invalid string, and you’ll get a 400 Bad Request: locationId is required response.
Here are the three fastest ways to grab it:
Method A: Browser URL (Fastest)
Switch into the sub-account inside your HighLevel dashboard and check the URL bar:
https://app.gohighlevel.com/v2/location/k7d9F1mX8aP2zL5qR9sT/dashboard
The string right after /location/ (here, k7d9F1mX8aP2zL5qR9sT) is your Location ID.
Method B: Business Profile Settings
Head to Settings > Business Profile inside the sub-account. Scroll down to the General section and you’ll see the Location ID field with a copy button. For more edge cases across agency views, see our guide on how to find a sub-account Location ID in GoHighLevel.
Method C: Query the Locations API Endpoint
If you only have your Private Integration token and want to pull the sub-account ID programmatically, hit the Location endpoint with cURL.
Run this in your terminal:
curl --request GET --url https://services.leadconnectorhq.com/locations/k7d9F1mX8aP2zL5qR9sT --header 'Authorization: Bearer pit-xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx' --header 'Version: 2021-07-28' --header 'Accept: application/json'HighLevel returns the full sub-account record along with its verified ID:
{ "location": { "id": "k7d9F1mX8aP2zL5qR9sT", "name": "Acme Consulting Services", "email": "billing@acmeconsulting.com", "phone": "+15550192834", "address": "100 Market St", "city": "Austin", "state": "TX", "country": "US", "postalCode": "78701", "website": "https://acmeconsulting.com", "timezone": "America/Chicago" }
}Step 5: Make Your First API v2 Call
With your token and Location ID ready, let’s create a contact. Note the two required headers: Authorization: Bearer and Version: 2021-07-28. HighLevel v2 endpoints reject calls that omit the Version header.
Here is a working Node.js snippet using native fetch:
import dotenv from 'dotenv';
dotenv.config(); const GHL_API_URL = 'https://services.leadconnectorhq.com/contacts/';
const ACCESS_TOKEN = process.env.GHL_PRIVATE_TOKEN;
const LOCATION_ID = process.env.GHL_LOCATION_ID; async function createContact() { const payload = { locationId: LOCATION_ID, firstName: 'Marcus', lastName: 'Vance', email: 'marcus.vance@example.com', phone: '+15558493021', tags: ['api-lead', 'onboarding'], customFields: [ { id: 'hZy9xW8vU7tS', key: 'lead_source_detail', field_value: 'Direct Node.js Sync' } ] }; try { const response = await fetch(GHL_API_URL, { method: 'POST', headers: { 'Authorization': `Bearer ${ACCESS_TOKEN}`, 'Version': '2021-07-28', 'Content-Type': 'application/json', 'Accept': 'application/json' }, body: JSON.stringify(payload) }); const data = await response.json(); if (!response.ok) { console.error(`GHL Error [${response.status}]:`, data); return; } console.log('Contact created successfully. ID:', data.contact.id); } catch (error) { console.error('Network or execution error:', error); }
} createContact();Run that script and HighLevel will respond with a 201 Created status code and the full Contact object payload.
Troubleshooting Common API v2 Integration Errors
If your script fails on the first run, it’s almost always one of these three issues:
1. 401 Unauthorized: Invalid JWT or Token Format
If you get 401 Unauthorized with Invalid Token, check these points:
- Make sure the header contains the
Bearerprefix with a trailing space before the token string. - Confirm you’re sending requests to
services.leadconnectorhq.com, not the deprecatedrest.gohighlevel.com/v1/base URL. - Check if the token was revoked or regenerated in the Developer Portal.
2. 403 Forbidden: Insufficient Scope
This happens when your request hits an endpoint your app wasn’t authorized for—like creating an SMS conversation without the conversations.message.write scope.
Head back to Marketplace > Developers > My Apps > [Your App] > Scopes, check the missing scope, hit save, and reinstall the app on the sub-account so the token permissions refresh.
3. 422 Unprocessable Entity or 400 Bad Request
HighLevel strictly validates phone numbers using E.164 formatting. Passing a formatted number like (555) 019-2834 triggers a validation error. Strip all punctuation and supply the country code (e.g., +15550192834).
Also verify that custom field IDs in your customFields payload actually exist inside that specific sub-account.
If your automated backend actions don’t seem to fire down the line, read our guide on how to fix GoHighLevel workflow webhooks not firing.
Environment Variable Best Practices
Never hardcode your Location ID or Private Integration keys inside frontend bundles or commit them to source control. Keep them isolated in your server environment.
Here’s how to structure your .env file:
# GoHighLevel API v2 Credentials
GHL_API_BASE_URL=https://services.leadconnectorhq.com
GHL_API_VERSION=2021-07-28
GHL_LOCATION_ID=k7d9F1mX8aP2zL5qR9sT
GHL_PRIVATE_TOKEN=pit-9b4e12fa-71a2-4a81-bb09-318eec4c9192
PORT=3000Keep your staging and production Location IDs separated across deployment stages so test records don’t contaminate client sub-accounts.
Frequently Asked Questions
Do GoHighLevel Private Integration tokens expire?
No. Unlike public OAuth 2.0 access tokens that expire every 24 hours, Private Integration tokens (prefixed with pit-) remain valid indefinitely until an admin manually revokes them or uninstalls the app from the sub-account.
Can one Private Integration access multiple sub-accounts?
Yes. You can install a single Private Integration into multiple sub-accounts across your agency. Just make sure each API call passes the correct locationId in its payload or query params.
What is the rate limit for HighLevel API v2 calls?
HighLevel enforces a baseline limit of 100 requests per 10-second window per Location for standard endpoints, and 10 requests per 10-second window for resource-heavy operations like bulk imports. Hitting the threshold returns a 429 Too Many Requests status code. Always build exponential backoff into your HTTP clients.
Where do I find official API endpoint documentation for v2?
You can find full endpoint paths, request bodies, and sample responses directly in the HighLevel Developer Docs.
Next Steps
Now that your Private Integration is running and your Location ID is verified, check out our guide on how to find and use GoHighLevel Location IDs in API v2 calls for filtering by custom tags and handling pagination cleanly.

