You fire off what looks like a perfectly valid API call against GoHighLevel, only to get slapped with a 401 Unauthorized or a cryptic 422 Unprocessable Entity claiming the location wasn’t found. In HighLevel API v2, every single contact, calendar booking, pipeline stage, and custom field belongs to a specific sub-account identified by its locationId. Miss it or misplace it, and the API simply drops your request.
Here is how to grab that Location ID manually, pull it dynamically through webhooks and agency endpoints, and wire it into authenticated Node.js requests without running into scope mismatches.

Understanding Location IDs in GoHighLevel API v2
HighLevel operates on a strict hierarchy: Agency accounts (the parent organization) and Locations/Sub-accounts (individual client accounts or branches). Almost every core CRM endpoint—like /contacts/, /opportunities/, and /conversations/—needs a locationId string.
Back in API v1, legacy API keys were issued directly per sub-account, so the key itself implied the location. API v2 changed that completely. You now authenticate with OAuth access tokens or Private Integration tokens. That means you have to explicitly tell GHL which sub-account you’re touching—either in the query params, the JSON body, or the route path depending on what the official HighLevel API documentation expects for that endpoint.
Method 1: Grab the Location ID Manually from the URL or Settings
If you’re just hacking together a one-off script, configuring a Zapier hook, or testing endpoints in Postman, don’t waste time writing API calls to find the ID. Grab it straight from the browser.
Two quick ways:
- The Sub-Account URL: Log in, switch into the target sub-account, and check your address bar. You’ll see something like
https://app.gohighlevel.com/v2/location/Ve89xK1LmN0pQrStUvWx/dashboard. That 20-character string right after/location/is your client Location ID. - Business Profile Settings: Inside the sub-account, head to Settings (bottom-left gear) → Business Profile. Under the “General Information” card, grab the value from the Location ID field.
If you have dozens of accounts and need to catalogue them, take a look at our guide on how to find sub-account location IDs in GoHighLevel across your agency dashboard.
Method 2: Query Location IDs Programmatically via Agency API
When building SaaS integrations, client onboarding scripts, or multi-tenant sync tools, manual copying isn’t an option. You can list every sub-account under your agency using the agency-level /locations/search endpoint.
You’ll need an agency-scoped Private Integration Token or an OAuth token with the locations.readonly scope. The endpoint supports pagination and search filters by company name or email.
Here is a clean Node.js script to list all client locations and extract their IDs:
import axios from 'axios'; const AGENCY_API_KEY = process.env.GHL_AGENCY_TOKEN;
const GHL_API_BASE = 'https://services.leadconnectorhq.com'; async function getAllClientLocations() { try { const response = await axios.get(`${GHL_API_BASE}/locations/search`, { headers: { 'Authorization': `Bearer ${AGENCY_API_KEY}`, 'Version': '2021-07-28', 'Accept': 'application/json' }, params: { limit: 50, skip: 0 } }); const locations = response.data.locations; const clientMap = locations.map(loc => ({ id: loc.id, name: loc.name, email: loc.email, phone: loc.phone })); console.log(`Retrieved ${clientMap.length} client locations:`); console.table(clientMap); return clientMap; } catch (error) { console.error('Failed to fetch locations:', error.response?.data || error.message); throw error; }
} getAllClientLocations();Hitting this against an agency with 40+ locations takes about 300ms and returns the exact id strings you need for subsequent calls. If you are handling authentication via OAuth rather than static tokens, make sure your token refresh flow is reliable. We walked through that setup in our guide to GoHighLevel API v2 OAuth token refresh in Node.js.
Method 3: Extract Location ID Dynamically from Workflow Webhooks
In most real-world setups, HighLevel automations trigger external webhooks whenever a contact does something interesting. You never want to hardcode the Location ID in your receiving server for these workflows—GHL already hands it to you in the payload.
When an automation runs a Custom Webhook action, the Location ID shows up either at the top-level locationId property or nested under location.id depending on the trigger type.
Here is a small Express middleware helper to normalize and pull the incoming Location ID from any webhook payload:
import express from 'express';
const app = express();
app.use(express.json());
app.post('/api/webhooks/ghl-receiver', (req, res) => { const payload = req.body; // Extract Location ID across different trigger structures const locationId = payload.locationId || payload.location?.id || payload.customData?.location_id; if (!locationId) { console.warn('Webhook received without a valid locationId'); return res.status(400).json({ error: 'Missing locationId in payload' }); } console.log(`Processing event for Location ID: ${locationId}`); console.log(`Event Type: ${payload.type || 'Workflow Webhook'}`); // Route processing based on location // e.g., dispatchToLocationQueue(locationId, payload); return res.status(200).json({ status: 'received', locationId });
});
app.listen(3000, () => console.log('Webhook server active on port 3000'));If you’re passing custom parameters or need to map field keys on the fly from the workflow builder, check out our guide on custom values and custom fields in GoHighLevel webhooks.
Building an Authenticated API Request with Location ID
Once you have the ID, you need to pass it where the endpoint expects it. HighLevel isn’t completely uniform here:
- Query parameter: GET requests like
/contacts/?locationId=Ve89xK1LmN0pQrStUvWxand/custom-fields/?locationId=... - Request body: POST and PUT requests (like creating contacts or opportunities) where
locationIdmust be a top-level string in the payload. - Required header: All v2 requests require the
Version: 2021-07-28header alongside your Bearer token, following standard HTTP authorization specs.
Here is how you handle both GET and POST requests cleanly in Node:
import axios from 'axios'; const GHL_API_BASE = 'https://services.leadconnectorhq.com';
const AUTH_TOKEN = process.env.GHL_ACCESS_TOKEN;
const CLIENT_LOCATION_ID = 'Ve89xK1LmN0pQrStUvWx'; const ghlClient = axios.create({ baseURL: GHL_API_BASE, headers: { 'Authorization': `Bearer ${AUTH_TOKEN}`, 'Version': '2021-07-28', 'Content-Type': 'application/json' }
}); // 1. Search contacts within the specific client location
async function searchContactsByPhone(phone) { const response = await ghlClient.get('/contacts/', { params: { locationId: CLIENT_LOCATION_ID, query: phone } }); return response.data.contacts;
} // 2. Create a contact explicitly tied to the client location
async function createClientContact(contactData) { const payload = { locationId: CLIENT_LOCATION_ID, firstName: contactData.firstName, lastName: contactData.lastName, email: contactData.email, phone: contactData.phone, tags: ['api-lead', 'inbound'] }; const response = await ghlClient.post('/contacts/', payload); return response.data.contact;
} // Execution example
async function run() { try { const newContact = await createClientContact({ firstName: 'Devon', lastName: 'Vance', email: 'devon.v@example.com', phone: '+15552345678' }); console.log(`Created Contact ID: ${newContact.id} in Location: ${newContact.locationId}`); } catch (err) { console.error('API Error:', err.response?.data || err.message); }
} run();For more details on query parameters across different resources, read our breakdown for finding your GoHighLevel Location ID for API v2 requests.
The Gotcha: Token Scope Mismatches (401 and 422 Errors)
The most common headache here is mixing up Agency tokens and Location tokens. GHL binds tokens differently depending on how they were generated:
- Location-scoped tokens: Created inside Sub-account Settings → Developers → Private Integrations (or via sub-account OAuth install). This token is strictly bound to one Location ID. If you pass a different
locationIdin the request body, GHL will reject it with a403 Forbiddenor422 Location mismatch. - Agency-scoped tokens: Created under Agency Settings → Developers. This token can access all sub-accounts under the agency, but you must provide the target
locationIdon every single data call—otherwise GHL doesn’t know which CRM partition you’re writing to.
If you ever hit {"statusCode": 422, "message": "Location ID is required"}, check your payload structure first. Nesting locationId inside a contact or data wrapper instead of the root object is an easy mistake to make.
Handling Dynamic Multi-Tenant Routing in Production
If you’re building a multi-tenant backend that syncs data for multiple client accounts, avoid hardcoding IDs anywhere. Store a mapping between your internal tenant IDs (or customer domains) and their HighLevel Location IDs in your database.
Here is a basic client resolver pattern to keep requests isolated and clean:
class HighLevelIntegrationService { constructor(agencyToken) { this.agencyToken = agencyToken; this.clientCache = new Map(); // In production, use Redis or Postgres } registerTenant(tenantId, ghlLocationId) { this.clientCache.set(tenantId, ghlLocationId); } async executeForTenant(tenantId, apiAction) { const locationId = this.clientCache.get(tenantId); if (!locationId) { throw new Error(`No Location ID registered for tenant: ${tenantId}`); } const clientContext = { locationId, headers: { 'Authorization': `Bearer ${this.agencyToken}`, 'Version': '2021-07-28' } }; return await apiAction(clientContext); }
} // Usage
const ghlService = new HighLevelIntegrationService(process.env.GHL_AGENCY_TOKEN);
ghlService.registerTenant('client_alpha', 'Ve89xK1LmN0pQrStUvWx'); ghlService.executeForTenant('client_alpha', async ({ locationId, headers }) => { const res = await axios.get('https://services.leadconnectorhq.com/opportunities/search', { headers, params: { locationId, status: 'open' } }); console.log(`Found ${res.data.opportunities.length} open deals.`);
});Frequently Asked Questions
Can a Location ID ever change in GoHighLevel?
No. The locationId is an immutable primary key generated when the sub-account is spun up. Even if you rename the client, change the custom domain, or transfer the sub-account between agency plans, the underlying Location ID stays the same.
What is the difference between Company ID and Location ID?
The companyId belongs to the parent Agency account. The locationId belongs to a specific sub-account. Billing, agency-wide users, and company settings use companyId, while contacts, pipelines, calendars, and messages require locationId.
Why does my GET request return 401 when the Location ID is correct?
A 401 means authentication failed, not location resolution. Check if your Bearer token expired, was revoked, or if you forgot the Version: 2021-07-28 header. If the Location ID itself was invalid or inaccessible, GHL returns a 403 or 404 instead.
Where do I find the Location ID in workflow custom code actions?
Inside HighLevel Workflow Custom Code steps (Node.js or Python), the ID is already part of the context. Access it directly with inputData.locationId, or map the Location → ID variable in the workflow UI input builder.
Next Steps
Once your routes cleanly extract and pass Location IDs, you can scale up your automations and background sync jobs. If you’re building worker queues or background integrations, make sure your OAuth refresh loop is solid by following our tutorial on GoHighLevel API v2 OAuth token refreshes in Node.js.

