All posts
n8n

How to Connect GoHighLevel to n8n (Webhook Guide)

The two reliable ways to connect GoHighLevel and n8n: inbound webhooks and the v2 API, plus the gotchas that break most setups. A step-by-step guide.

July 8, 2026·9 min read·by Olexander Cheberko
Table of contentstap to expand

The reliable way to connect GoHighLevel to n8n is a pair of one-directional links. GoHighLevel's native Webhook action posts events into an n8n webhook, and n8n calls the GoHighLevel v2 API back to update contacts, opportunities, or whatever comes next. No paid connector, and no Zapier sitting in the middle.

I've wired these two tools together on real client accounts for two years now: lead routing, AI qualification, review requests, billing sync. Below is the setup that holds up in production, and the handful of things that quietly break it.

Quick answer

Build two independent pipes, not one two-way "sync":

DirectionMethodAuth
GoHighLevel to n8nWorkflow Webhook action into an n8n Webhook nodenone (secret in URL)
n8n to GoHighLevelHTTP Request node to the v2 API (services.leadconnectorhq.com)Private Integration token

Point GoHighLevel at the n8n Production URL (not Test), write back with a Private Integration token, and set the Version header to 2021-07-28 on every v2 call. Trying to force a single bidirectional sync is how you end up with loops, so keep the two pipes separate.

What you need before you start

  • A GoHighLevel sub-account (Agency Unlimited or a plan that exposes Workflows and Private Integrations).
  • An n8n instance, either Cloud or self-hosted, reachable from the public internet so GoHighLevel can POST to it.
  • Admin access to that sub-account's Settings, since Private Integrations live there.
  • A rough map of what should trigger what: which GoHighLevel event fires n8n, and what n8n does with it.

That last one matters more than the tooling. Decide the flow before you touch a node and the build takes an afternoon instead of a week.

How do I create a Private Integration token?

Private Integrations replaced the old per-location API key. The token is scoped, so you grant it only what the workflow actually touches.

  1. In the sub-account, open Settings → Private Integrations.
  2. Click Create New Integration, give it a name like n8n-automation, and select the scopes.
  3. For a contact-centric build, the usual set is contacts.readonly, contacts.write, opportunities.readonly, opportunities.write, and locations/customFields.readonly. Add conversations.write if n8n needs to send SMS or email through GoHighLevel.
  4. Save, then copy the token. It starts with pit- and is shown once, so store it in your n8n credentials immediately.

In n8n, save that token as a Header Auth credential (Authorization: Bearer pit-...), or drop it straight into the HTTP Request node's authentication. Either way, every v2 call also needs one more header: Version: 2021-07-28. The API dates its versions, and leaving that header off is a quiet, common reason a write-back node returns an error while everything else looks fine.

Step 1: Create the n8n webhook

In n8n, add a Webhook node as the trigger. Set the method to POST, copy the Production URL (the Test URL trap is covered below), and add a Respond to Webhook node so GoHighLevel gets a quick 200 instead of hanging on the request. GoHighLevel expects a fast response, so answer first and do the slow work afterward.

Step 2: Post from GoHighLevel

In your sub-account, open Automation → Workflows and pick the trigger that should fire n8n, such as Contact Created or Opportunity Stage Changed. Add a Webhook action, paste the n8n Production URL, leave the method as POST, and save. GoHighLevel now sends the contact payload to n8n every time that workflow runs.

Run it once and look at what actually arrives. A Contact Created payload lands in n8n looking roughly like this:

{
  "type": "ContactCreate",
  "locationId": "ve9EPM428h8vShlRW1KT",
  "contact_id": "9NwqQ2n0i7Xk3mA4pLoZ",
  "first_name": "Dana",
  "last_name": "Okafor",
  "full_name": "Dana Okafor",
  "email": "dana@brightpath.io",
  "phone": "+14155550142",
  "tags": ["website-form", "new-lead"],
  "date_created": "2026-07-08T14:22:05.000Z",
  "customFields": [
    { "id": "3sVEQKtqB2r9uLhWc0Za", "value": "SaaS" },
    { "id": "9kPtnXe4Rf1yQd7oM2Cv", "value": "10000" },
    { "id": "aZ12BqLm8Ns5Vh6Wp3Tk", "value": "Google Ads" }
  ]
}

The shape is not identical across every account, so always inspect a real event rather than trusting a schema you found online. The part that trips people up is customFields: values come keyed by internal ID, not label.

How do I turn custom-field IDs into labels?

Right after the webhook, add a Code node (a Set node with expressions works too) that maps the IDs to names you can actually read. Build the map once from the field list in Settings → Custom Fields or a GET /locations/{id}/customFields call, then reuse it:

// Code node: turn GHL custom-field IDs into readable keys
const fieldMap = {
  "3sVEQKtqB2r9uLhWc0Za": "industry",
  "9kPtnXe4Rf1yQd7oM2Cv": "monthly_budget",
  "aZ12BqLm8Ns5Vh6Wp3Tk": "lead_source",
};
 
const incoming = $json.customFields || [];
const fields = {};
 
for (const f of incoming) {
  const label = fieldMap[f.id];
  if (label) fields[label] = f.value;
}
 
return [{ json: { ...$json, fields } }];

From here the rest of the workflow reads $json.fields.industry instead of a random string. Do this early, once, and every downstream node stays legible.

Step 3: Write data back with the v2 API

To update GoHighLevel from n8n, add an HTTP Request node aimed at the v2 base https://services.leadconnectorhq.com. To update a contact you PUT to /contacts/{contactId} with the token and the version header. As a raw call it looks like this:

curl --request PUT \
  --url https://services.leadconnectorhq.com/contacts/9NwqQ2n0i7Xk3mA4pLoZ \
  --header 'Authorization: Bearer pit-0f2a...redacted' \
  --header 'Version: 2021-07-28' \
  --header 'Content-Type: application/json' \
  --data '{
    "tags": ["qualified", "n8n-enriched"],
    "customFields": [
      { "id": "3sVEQKtqB2r9uLhWc0Za", "value": "SaaS" }
    ]
  }'

In the n8n node that maps to Method PUT, URL https://services.leadconnectorhq.com/contacts/{{ $json.contact_id }}, header auth for the token, a manual Version header, and a JSON body. Note the write side still uses IDs for customFields, so keep your field map handy in both directions. Build on v2, because the v1 keys are on their way out.

A real workflow: lead lands, enrich, qualify, write back

Here is one of the most common builds I ship, end to end:

  1. Lead lands. A GoHighLevel form submission fires a Contact Created workflow, which POSTs to the n8n webhook.
  2. Normalize. The Code node above turns customFields IDs into industry, monthly_budget, and lead_source.
  3. Enrich. n8n calls an external source (company data, email validation, whatever the offer needs) and attaches the result.
  4. Qualify. An AI node scores the lead against your criteria and returns a tier plus a one-line reason. This is where a model earns its keep: it reads the free-text fields a rules engine can't.
  5. Write back. The HTTP Request node PUTs the tier into a custom field, adds a qualified tag, and, for hot leads, creates an opportunity in the right pipeline stage.
  6. Notify. GoHighLevel takes over from the tag: it books the call, texts the rep, starts the nurture.

That is the exact spine of the live lead-qualification build on the site, where a chatbot and a voice agent feed GoHighLevel and lead processing dropped from two to three hours down to under ten seconds. The point isn't the AI. It's that the two pipes let each tool do what it's good at: GoHighLevel owns the CRM and the follow-up, n8n owns the branching, enrichment, and API glue.

Why does the connection fail?

These are the ones I get called in to fix.

SymptomCauseFix
Worked while building, dead in productionGoHighLevel is pointed at the n8n Test URL, which only listens with the editor openSwap in the Production URL and re-save the GoHighLevel Webhook action
Custom fields show as random stringsValues arrive keyed by internal ID, not labelMap IDs to names in a Code or Set node once, right after the webhook
Workflow fires itself over and overn8n's write-back re-triggers the same GoHighLevel workflowGate the GoHighLevel trigger with a tag or field filter that n8n never sets
429 errors on bulk runsThe v2 API is rate-limitedBatch the calls and add a small Wait between them instead of looping flat out
Leads vanish, no error anywhereA webhook or node failed silentlyAdd an Error Trigger workflow that pings Slack or Telegram on any failed execution

The last row is the one that costs money, because a silent failure loses leads without ever raising a flag. If yours seems to fire sometimes and not others, the n8n not-triggering checklist walks through the usual suspects.

When should I self-host n8n?

Cloud is the right call to start: nothing to maintain, and you can be live in an hour. Self-hosting earns its keep once volume climbs, because there's no per-execution fee. A busy GoHighLevel account firing thousands of webhook events a month runs into Cloud's execution ceiling fast, and at that point a self-hosted instance is dramatically cheaper per run.

Self-host also when you need long-running or heavy jobs, custom community nodes, or data that has to stay inside your own infrastructure for compliance. The trade is real: you own updates, backups, and uptime. If nobody on the team wants to babysit a server, stay on Cloud and pay for the convenience. I run both depending on the client, and I'll tell you plainly which one fits before you commit.

If you're copying leads between GoHighLevel and another tool by hand, or your follow-up depends on somebody remembering, that's the signal to wire this up. I build n8n automations and full GoHighLevel automation setups, and connecting the two the way this guide describes is most of what I do. Send me the flow you have in your head and I'll tell you whether it's an afternoon or a proper project before either of us spends a dollar.

Tags

n8nGoHighLevelwebhookintegrationautomation

Frequently asked questions

Can GoHighLevel connect to n8n without a paid integration?

Yes. GoHighLevel's native Webhook action posts data to an n8n webhook URL for free, and n8n calls the GoHighLevel v2 API back with a Private Integration token. You don't need a third-party connector or any paid middleware.

Do I use the n8n Test URL or Production URL in GoHighLevel?

The Production URL. The Test URL listens only while the n8n canvas is open in test mode, so a workflow that works while you're building it silently stops in production. This is the single most common reason a GoHighLevel-to-n8n connection breaks for no obvious reason.

How does n8n send data back into GoHighLevel?

With an HTTP Request node that calls the GoHighLevel v2 API at services.leadconnectorhq.com, authenticated by a Private Integration token. The old v1 API keys are deprecated, so build anything new on v2.

What Version header does the GoHighLevel v2 API need?

Every v2 request needs a Version header set to 2021-07-28. Leave it off and the API rejects the call, which is a common cause of a write-back node that returns an error for no obvious reason.

Why are my GoHighLevel custom fields showing as random IDs in n8n?

GoHighLevel sends custom fields by their internal ID, not their label. Pull the field list once from the API or the webhook payload, map ID to name in an n8n Code or Set node, and reference the friendly names from then on.

Should I self-host n8n for a GoHighLevel integration?

For anything past a couple of simple workflows, yes. Self-hosted n8n has no per-execution fee, which adds up fast when a busy GoHighLevel account fires thousands of webhook events a month.

Need something like this built?

Free 15-min discovery call. I'll listen, ask honest questions, and tell you if I can help.

More in n8n