How to send leads to GoHighLevel through the API
Send website leads into a GoHighLevel sub-account with a Private Integration token and the contacts upsert endpoint, without making duplicate contacts.

On this page
To send leads to GoHighLevel (HighLevel) through the API, create a Private Integration token in the sub-account with the contacts.write scope, then POST each lead to https://services.leadconnectorhq.com/contacts/upsert with the sub-account’s Location ID. Upsert adds the contact if they’re new and updates them if they already exist, so a customer who enquires twice doesn’t become two contacts.
That’s the short version. Below is each step, the exact request, and the mistakes that trip people up.
What you need before you start
- Access to the sub-account (the location) that leads should land in. Agency-level keys aren’t needed for this.
- A Private Integration token. In the sub-account, open Settings → Private Integrations, choose Create new Integration, name it after your website or estimator, and tick only the contacts.write scope. Copy the token straight away: HighLevel won’t show it again.
- The Location ID, from the sub-account’s Settings → Business Profile.
Give the token the fewest scopes that do the job. A token that can only write contacts can’t read your conversations, payments or calendars if it ever leaks. There’s more on this in how to keep CRM API keys safe.
The request
POST https://services.leadconnectorhq.com/contacts/upsert
Authorization: Bearer <your Private Integration token>
Version: v3
Content-Type: application/json
{
"locationId": "<your Location ID>",
"firstName": "Sophie",
"lastName": "Turner",
"email": "[email protected]",
"phone": "+447700900123",
"postalCode": "BS6 5QA",
"source": "Website estimator",
"tags": ["website-lead", "fencing"]
}
A few details matter:
- The
Versionheader is required. Without it the request is refused. The current contacts upsert reference listsv3; check the version switcher in HighLevel’s API docs if you’re maintaining older code that uses a date version. locationIdis the only required field, but send an email or phone, or there’s nothing to match a returning customer on.- Phone numbers work best in international format (
+44…,+1…), so HighLevel’s SMS and calls work without editing. sourceandtagsare what let you build a workflow that fires only for website leads.
The reply tells you whether the contact was new:
{ "new": true, "contact": { "id": "…", "locationId": "…" }, "traceId": "…" }
Keep the contact id if you want to add notes or opportunities to the same person later.
Duplicates: how upsert decides
Upsert follows the sub-account’s Allow Duplicate Contact setting. With duplicates off (the usual choice), HighLevel matches on email and phone and updates the person it finds. If your sub-account allows duplicates, every enquiry can make a new contact. Why duplicates happen and how upsert stops them goes into this in more depth.
Handling errors and retries
| Reply | What it means | What to do |
|---|---|---|
| 401 | The token is wrong, revoked or rotated | Make a new token and update it |
| 403 | The token is missing contacts.write or is for another location |
Check scopes and the Location ID |
| 422 | A field is invalid, often a badly formatted phone or email | Fix the data, don’t retry as-is |
| 429 or 5xx | Rate limit or a HighLevel outage | Retry later, with a growing wait |
Never drop a lead because the CRM was down. Save it first, then send it, and retry for a few hours on 429 and 5xx replies. Because upsert matches on email and phone, retrying is safe: it won’t make a second contact.
Starting a workflow when the lead arrives
Once leads arrive with a tag, open Automation → Workflows and use the Contact Tag trigger (tag added: website-lead). From there you can text the customer, assign them to someone and create an opportunity in your pipeline. Replying quickly is most of the battle: see speed to lead.
Doing it in RoughFigure
In RoughFigure we’ve built the HighLevel connection for you. Open Integrations → Send leads to your CRM, choose HighLevel, paste your Private Integration token and Location ID, and click Connect. That one click is all it takes: from then on every lead from your estimator arrives in the sub-account as a contact, with their name, email, phone, postcode and “RoughFigure” as the source, updated rather than duplicated if they’re already there.
Click Send a test lead to see “RoughFigure connection check” arrive, and if HighLevel is ever down we keep retrying for about four hours. See the HighLevel integration or read how to connect your CRM.
Questions
Do I need a HighLevel marketplace app to use the API?
No. For one sub-account sending leads into itself, a Private Integration token is enough. A marketplace app with OAuth is for software that many agencies install.
Should I use an agency token or a sub-account token?
A sub-account token. It can only reach that one location, which is what you want for a website form.
How often should I rotate the token?
HighLevel suggests rotating Private Integration tokens regularly, every 90 days or so. Make the new token, update your website or RoughFigure, then delete the old one.
Can I create an opportunity at the same time?
Yes, with a second call to the opportunities endpoint using the contact ID you got back. Many businesses find a workflow triggered by a tag simpler to change later.


