If you’re syncing sub-account details—business names, addresses, timezones, and Location IDs—from your HighLevel agency dashboard into a warehouse, Postgres database, or custom client portal, you need GoHighLevel’s API v2. Let’s walk through working requests to pull full sub-account records across single locations and entire agencies.
When I first migrated our internal sync jobs from HighLevel API v1 to v2, my scripts instantly blew up with 401 Unauthorized errors. The authentication model had completely changed under the hood, demanding strict OAuth scopes and a mandatory version header. Here are the exact endpoints, headers, and code snippets that actually run cleanly in production.

Company Tokens vs. Location Tokens
Before firing off HTTP requests, make sure you’re using the right token level. In HighLevel API v2, the endpoint you hit depends entirely on whether your access token was minted at the Agency (Company) level or the Sub-Account (Location) level.
- Agency-Level (Company) Token: Generated using agency credentials or an Agency-level Private Integration. This is the only token that can query
/locations/searchto return every sub-account under your agency umbrella. - Location-Level Token: Issued when an app gets installed inside a specific sub-account. It can only inspect its own profile via
/locations/{locationId}and will return errors if you try to query agency-wide account lists.
If you’re building an admin dashboard, a backup worker, or agency-wide reporting, you need an agency-level token with the locations.readonly scope. If you haven’t set up your app yet, check our walkthrough on creating a GoHighLevel private integration to generate your keys.
Required Request Headers and API Versioning
GoHighLevel API v2 strictly requires an explicit Version header on every single call. Skip it, or pass an unsupported date, and the gateway will either reject the payload outright or hand back deprecated schema structures.
Include these headers on every request to the HighLevel API v2 gateway:
Authorization: Bearer YOUR_ACCESS_TOKEN
Version: 2021-07-28
Accept: application/jsonMake sure you point all requests to https://services.leadconnectorhq.com. The legacy rest.gohighlevel.com endpoint belongs to API v1 and won’t accept your v2 bearer tokens.
Method 1: Fetching All Sub-Accounts via Agency Search
To list every sub-account in your agency alongside its unique locationId and configuration, hit the GET /locations/search endpoint. You’ll need your companyId, which sits right inside your agency dashboard URL or settings page.
Here’s the curl command to pull the list:
curl -X GET "https://services.leadconnectorhq.com/locations/search?companyId=YOUR_COMPANY_ID&limit=50&skip=0" -H "Authorization: Bearer pit-12345678-abcd-1234-abcd-1234567890ab" -H "Version: 2021-07-28" -H "Accept: application/json"You’ll get back an array of location objects. Here’s a trimmed-down example response showing the core fields:
{ "locations": [ { "id": "ve9Aq8yTRdfgT56Yui8k", "name": "Downtown Dental Practice", "phone": "+15552345678", "email": "office@downtowndental.com", "address": "123 Main St, Suite 400", "city": "Austin", "state": "TX", "country": "US", "postalCode": "78701", "timezone": "America/Chicago", "companyId": "comp_898asdf89asdf", "business": { "name": "Downtown Dental LLC", "address": "123 Main St", "city": "Austin", "state": "TX", "country": "US", "postalCode": "78701", "timezone": "America/Chicago" } } ], "total": 1
}The top-level id field in each object is your locationId. You’ll need this string for any follow-up calls targeting contacts, pipelines, calendars, or custom fields within that specific sub-account.
Method 2: Fetching a Specific Sub-Account by Location ID
If you already have a sub-account’s ID and just need its latest business profile, address, or timezone, hit GET /locations/{locationId} directly. If you need alternative ways to grab this manually from the UI, see our guide on finding sub-account Location IDs.
Here’s the curl call for fetching a single location record:
curl -X GET "https://services.leadconnectorhq.com/locations/ve9Aq8yTRdfgT56Yui8k" -H "Authorization: Bearer pit-12345678-abcd-1234-abcd-1234567890ab" -H "Version: 2021-07-28" -H "Accept: application/json"The response wraps the payload inside a root location key:
{ "location": { "id": "ve9Aq8yTRdfgT56Yui8k", "name": "Downtown Dental Practice", "phone": "+15552345678", "email": "office@downtowndental.com", "address": "123 Main St, Suite 400", "city": "Austin", "state": "TX", "country": "US", "postalCode": "78701", "website": "https://downtowndental.com", "timezone": "America/Chicago", "settings": { "allowDuplicateContact": false, "allowDuplicateOpportunity": true } }
}Handling Pagination for Large Agency Portfolios
The /locations/search endpoint caps results at 50 locations per call. If you’re managing hundreds of client accounts, you’ll need to paginate using skip and limit query params.
Here’s the loop logic I use for batch jobs:
- Start the initial request at
limit=50&skip=0. - Grab the
totalcount from the root response. - Increment
skipby 50 on each iteration (skip=50,skip=100, etc.) untilskip >= total. - Drop a short 150ms sleep between requests so you don’t slam into the rate limits outlined in the official HighLevel documentation.
Node.js Implementation: Full Agency Sub-Account Sync
Here’s a complete Node.js script using native fetch (Node 18+) that handles pagination automatically and maps all sub-accounts into a clean array.
Save it as fetchLocations.js and run it with your environment variables:
const API_BASE = 'https://services.leadconnectorhq.com';
const ACCESS_TOKEN = process.env.GHL_ACCESS_TOKEN;
const COMPANY_ID = process.env.GHL_COMPANY_ID;
async function fetchAllSubAccounts() { const allLocations = []; const limit = 50; let skip = 0; let total = Infinity; while (skip setTimeout(resolve, 200)); } return allLocations;
}
fetchAllSubAccounts()
.then((locations) => { console.log(`Fetched ${locations.length} total sub-accounts.`); locations.forEach((loc) => { console.log(`[${loc.id}] ${loc.name} - ${loc.timezone}`); });
})
.catch((err) => console.error('Sync failed:', err.message));If you plan to pipe these location IDs into automated webhook flows later, take a look at how we format and send custom values and contact fields via webhooks.
Python Implementation: Quick Data Extraction
If you’re running ETL scripts or Python data pipelines, here’s the equivalent script using the requests package.
Save this as ghl_locations.py:
import os
import time
import requests
API_BASE = "https://services.leadconnectorhq.com"
ACCESS_TOKEN = os.getenv("GHL_ACCESS_TOKEN")
COMPANY_ID = os.getenv("GHL_COMPANY_ID")
headers = { "Authorization": f"Bearer {ACCESS_TOKEN}", "Version": "2021-07-28", "Accept": "application/json",
}
def get_all_locations():
results = []
limit = 50
skip = 0
total = 1
while skip < total:
params = { "companyId": COMPANY_ID, "limit": limit, "skip": skip,
}
res = requests.get(f"{API_BASE}/locations/search", headers=headers, params=params)
if res.status_code != 200:
raise Exception(f"Request failed ({res.status_code}): {res.text}")
data = res.json()
total = data.get("total", 0)
locations = data.get("locations", [])
results.extend(locations)
skip += limit
time.sleep(0.2)
return results
if __name__ == "__main__":
sub_accounts = get_all_locations()
print(f"Successfully retrieved {len(sub_accounts)} sub-accounts.")
for acc in sub_accounts:
print(f"ID: {acc.get('id')} | Name: {acc.get('name')}")Gotchas and Common HTTP Errors
These are the common errors you’ll run into when querying location endpoints, along with how to fix them quickly:
1. HTTP 401: Unauthorized / Invalid JWT
Usually caused by an expired OAuth token, a malformed string, or accidental whitespace inside your Authorization header. Standard OAuth access tokens expire after 24 hours—refresh them before running your sync jobs.
2. HTTP 403: Forbidden / Scope Missing
Your token is valid, but the Private Integration or OAuth app lacks the locations.readonly or locations.write scope. Open the integration in the HighLevel Developer Marketplace, check the box for the required scope, save, and re-authenticate.
For a step-by-step walkthrough on handling scopes and tokens, check our guide on authenticating and passing Location IDs in GoHighLevel API v2.
3. HTTP 400: companyId is required
Calling GET /locations/search without ?companyId=YOUR_COMPANY_ID will fail every time. Even if your bearer token is explicitly scoped to your agency, the query validator requires the companyId parameter in the URL string.
4. HTTP 429: Too Many Requests
HighLevel caps v2 endpoints at roughly 100 requests per 10 seconds per token. If you run an unthrottled pagination loop across 500+ locations, you’ll hit 429s fast. Keep at least a 150–200ms delay between pages and add basic exponential backoff.
Frequently Asked Questions
Can I get sub-account custom fields from the /locations endpoint?
No. The /locations/{locationId} payload only contains core business info, address, and timezone data. To get custom fields or values, you have to query GET /locations/{locationId}/customFields or GET /locations/{locationId}/customValues separately.
What is the difference between companyId and locationId?
Your companyId represents the parent agency account holding your HighLevel plan. The locationId represents a specific client sub-account under that agency where contacts, pipelines, and workflows live.
Can a sub-account level token call /locations/search?
No. /locations/search requires agency-level permissions. A sub-account token can only call /locations/{locationId} on its own specific Location ID.
Once you’ve retrieved your sub-account Location IDs, you’re ready to query individual sub-account resources. Check out our guide on how to authenticate and pass Location IDs in GoHighLevel API v2 to start pulling contacts, appointments, and pipeline data.

