Fix: n8n HTTP Request 401 unauthorized on the GoHighLevel API
Your n8n HTTP Request node keeps getting 401 from GHL. It's almost always a legacy API key, a missing Version header, or a token scope mismatch. Here's the ladder.
Symptoms — what you're seeing
- Every request from an n8n HTTP Request node to the GoHighLevel API returns
401 Unauthorized - The exact same call works in Postman or curl, but fails from n8n
- It worked last month and suddenly stopped — no code changes on your side
- The response body says
{"message":"Invalid JWT"},Invalid token, orThe token is not authorized for this scope - Some GHL endpoints work with your token and others return 401 or 403
Why this happens
The 401 almost always comes down to one of four things, and they're easy to tell apart once you know what GHL changed. GoHighLevel deprecated the old API Keys (the long "Agency API Key" you'd paste in) in favour of Private Integration tokens and OAuth. A legacy key still authenticates against the v1 API but returns 401 against the v2 API at services.leadconnectorhq.com. If you followed a tutorial written before the change, you're sending a key the v2 endpoint no longer accepts.
The second cause is the missing Version header. The GHL v2 API requires a Version header with a dated value. Without it, the API rejects the request before it even checks your token — which surfaces as a 401 even though the token is fine.
The third is token scope. A Private Integration is created at a specific level (Agency or Sub-Account/Location) and granted specific scopes. A Location token calling an Agency-level endpoint — or a token missing the contacts.readonly / contacts.write scope for the call you're making — returns 401 or 403 depending on the endpoint.
The fourth is mechanical: the token was pasted into the n8n credential with a trailing newline or a literal Bearer prefix, so the header goes out as Authorization: Bearer Bearer eyJ... or with a stray \n. The API sees a malformed token and returns 401.
The fix, step by step
- Create a Private Integration token (not an API key). In GHL go to Settings → Private Integrations → Create new integration. Create it at the level you're calling: Sub-Account for contact/opportunity/calendar work, Agency only if you're calling agency-level endpoints. Copy the token once — GHL won't show it again.
- Grant the exact scopes the call needs. Don't grant everything; grant what the endpoint requires. For contact work that's
contacts.readonlyandcontacts.write. For calendars,calendars.readonly/calendars.write. A 403 (not 401) after auth succeeds is almost always a missing scope. - Store it as an n8n credential, not inline. Create a Header Auth credential (or use the HTTP Request node's Generic Credential → Header Auth). Set the header name to
Authorizationand the value toBearer YOUR_TOKEN— with exactly one space, no trailing newline.Header Auth credential Name: Authorization Value: Bearer pit-xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
- Add the
Versionheader on the node. In the HTTP Request node, add a header:Version: 2021-07-28
This is the single most common missing piece. A correct token with noVersionheader still 401s. - Point at the v2 base URL. Use
https://services.leadconnectorhq.com, not the oldrest.gohighlevel.com/ v1 host. Mixing a v2 token against a v1 host (or vice-versa) is a guaranteed 401. - Test with one read call. Fetch a single contact or list calendars. A 200 confirms token + scope + version + host are all aligned before you wire the rest of the workflow.
Common variations
- "Works in Postman, 401 in n8n" — Postman had the
Versionheader saved in the collection and you didn't copy it into the n8n node. AddVersion: 2021-07-28. - "401 became 403 after I fixed the token" — auth now succeeds but the token lacks the scope for that endpoint. Recreate the Private Integration with the missing scope; scopes can't be edited after creation on some accounts.
- "Some endpoints 401, others work" — you're using a Location token on an Agency endpoint (or vice-versa). Agency and Sub-Account tokens are not interchangeable.
- "401 only on self-hosted n8n, fine on Cloud" — clock skew. If your host's system clock is off by more than a few minutes, time-sensitive token validation fails. Sync with NTP.
- "Invalid JWT" — the token has a trailing newline or a doubled
Bearerprefix. Re-paste it cleanly into the credential.
When this isn't the issue
- If you're getting
429 Too Many Requestsrather than 401, the token is fine and you're hitting rate limits — see the GHL API rate-limit strategy for the queue-and-backoff pattern. - If the request never reaches GHL at all (timeout, not a 401), a reverse proxy or firewall on a self-hosted instance may be stripping the request — check the same way you would for a webhook not receiving data.
- If auth works but the response is empty, the issue is the query/body, not the token.
Wiring GHL into n8n from scratch? The connect GoHighLevel to n8n guide sets up the full webhook pipeline, and the n8n production reliability checklist covers credential management at scale.
Need this workflow customized or debugged? Book a 30-min scope call →
Stuck configuring this workflow?
I build and fix n8n workflows for agencies and SaaS teams. 30-minute scope call — bring the problem, leave with an estimate.