Treat each location as its own boundary.
In HighLevel, a location is what most people call a subaccount. Contacts, custom fields, pipelines, calendars, and users belong to a location. An agency account sits above them, but almost every API call you make from n8n is scoped to one location id.
This shapes the integration more than any other fact. A custom field id from one location means nothing in another. A pipeline stage id is local to its location. A contact can exist in two locations with the same email and be two unrelated records.
So every mapping row I keep starts with the location id. When an integration serves several locations, the workflow identifies the location first and loads that location’s field and pipeline ids from storage, never from constants typed into the workflow.
Decide what identifies a contact.
HighLevel gives each contact an id. That id is the only stable identity. Email and phone are lookup keys that a person can change, share, or mistype. The first time n8n touches a contact, it should capture contact.id and store it next to the identifier the other system uses.
The contact upsert endpoint finds an existing contact using the location’s duplicate settings and field matching priority. That behavior is configured per location, so check it on the account you are integrating rather than assuming email wins. See the upsert contact reference.
Phone numbers are the usual source of silent mismatches. Normalize to E.164 before you send anything: country code included, no spaces, no punctuation. A number that arrives from a web form as ten digits and from the CRM with a plus sign is the same person to a human and two different strings to a comparison node. How HighLevel stores a number that arrives in another format is something to verify on the location, not assume.
Address custom fields by id.
Custom fields have a label a person sees and an id the API uses. Labels can be renamed by anyone with access to the settings page. Ids do not change. If a workflow maps by label, a helpful rename breaks it without anyone noticing until the data is wrong.
Pull the field list for the location from the API once, store the label to id pairs in an n8n Data Table or a database, and map from that. Custom field values are sent as a list of id and value pairs; confirm the exact shape and the value format for each field type in the custom fields reference, because dropdowns, dates, and checkboxes are not all plain strings.
Keep field types in the mapping too. A date field expects a date. A number field rejects a formatted string. Most mapping bugs I am asked to fix are a type mismatch that the API accepted silently or rejected with a message no one read.
Move opportunities by pipeline and stage id.
An opportunity belongs to a contact, a pipeline, and a stage. The pipeline and the stage are ids, not names. Names are for the board view; ids are for the API. The pipelines endpoint returns each pipeline with its stages and their ids. See the get pipelines reference.
Store the stage ids the integration needs under plain names of your own, like stage_new_lead or stage_quoted, and map from your names to the ids per location. When the team renames a stage in the CRM, nothing changes on your side. When they add a stage, you add a row.
Status is separate from stage. An opportunity can be open, won, lost, or abandoned while sitting in any stage, and the exact status values the API accepts should be confirmed in the current reference. Decide in the mapping which system owns status and which owns stage. Two systems both moving an opportunity produce updates that chase each other.
Use tags for signals and fields for data.
Tags are cheap. They are strings attached to a contact, they trigger workflows, and they are easy to filter on. They are also easy to misspell, easy to duplicate with different casing, and impossible to type or validate.
I use tags for signals that other automations react to, and custom fields for values something will read later. A tag says the contact came from a campaign. A field says which one, on what date, and what it cost. When a tag starts carrying a value in its name, it is time for a field.
Whatever you choose, list the tags the integration is allowed to add or remove, and treat that list as part of the mapping. Removing a tag can fire a HighLevel workflow just as adding one does.
Store the identifiers on the n8n side.
n8n workflows carry no memory between executions unless you give them storage. For a mapping this matters twice: you need the field and stage ids per location, and you need the link between a HighLevel contact id and the record id in the other system.
n8n Data Tables work well for both when volume is modest and the team wants to see the rows inside the editor. For higher volume, several locations, or anything that other applications also need to read, use a database the team already operates.
Store the location id, the HighLevel id, the external id, and the time of the last sync in each link row. That last field is what lets a later workflow answer whether an incoming update is newer than the one it already applied.
Keep a mapping table for every build.
This is the shape I fill in before writing any node. One row per field, kept next to the workflow, updated when either system changes. It is boring, and it is the document that lets someone else maintain the integration.
- Field name
- The label a person sees in each system, so a stakeholder can read the row.
- HighLevel id
- The custom field id or the standard field key, recorded per location.
- External key
- The field name or column in the other system.
- Type and format
- Text, number, date, dropdown, or checkbox, with the exact format each side expects.
- Direction
- Which system writes to which, or both, and which one wins on conflict.
- Transformation
- Normalization applied on the way through: E.164, trimmed whitespace, date parsing, option mapping.
- Required
- Whether the workflow should stop or continue when the value is missing.
- Owner
- The person who decides what the field means when the two systems disagree.
Questions.
Should I map GoHighLevel custom fields by name or by id?
By id. Labels can be renamed by anyone with settings access, and a rename silently breaks a mapping by name. Store label to id pairs per location and look them up.
Can the n8n HighLevel node update custom fields?
The built in node covers common contact and opportunity operations. Whether it exposes the custom field shape you need depends on the node version, so I check it and fall back to the HTTP Request node when it does not.
How do I update a contact in GoHighLevel from n8n without creating a duplicate?
Store the HighLevel contact id the first time you see the contact and update by id afterward. Use upsert only when you accept the location’s duplicate settings deciding what counts as a match.
What phone format does GoHighLevel expect?
Send E.164 with the country code. Verify how the specific location handles numbers that arrive in another format rather than assuming it normalizes them.
Written by Younes Nadif · 7 September 2026
