Home›Blog›GoHighLevel
GoHighLevel

GoHighLevel API With a Private Integration Token: Create and Update Contacts (Code Examples)

A practical guide to the GoHighLevel API with a Private Integration token, with copy-ready curl, JavaScript and PHP examples for creating and updating contacts.
Saleem Akhter
Oct 27, 2026
7 min read

Sooner or later every GoHighLevel account needs to talk to something else: a booking system, a custom website, an internal tool, a spreadsheet. Zapier and Make cover many of those connections, but when you need full control — or want to avoid paying per task — the API is the cleaner route.

This guide shows how to use the GoHighLevel API with a Private Integration token. It covers creating the token, choosing scopes, and working code for creating and updating contacts in curl, JavaScript and PHP, plus the error handling and security habits that keep an integration reliable.

What a Private Integration token is

A Private Integration token is a fixed access token you generate inside HighLevel for your own integrations. It is the simplest way to call the API for a single agency or sub-account: there is no OAuth flow to build and no app to publish. You choose which scopes the token has, and every request sends it as a Bearer token.

Use a sub-account (location) token when the integration works with one business’s contacts, calendars or opportunities. Use an agency token only for agency-level operations. Most integrations need the sub-account kind.

Step 1: create the token

  1. Open the sub-account and go to Settings → Private Integrations. If you do not see it, the feature may need to be enabled in Labs.
  2. Click Create new Integration and give it a clear name and description, such as “Website contact sync”.
  3. Select only the scopes the integration needs. For the examples below, that is contacts.write (and contacts.readonly if you also want to look contacts up).
  4. Copy the token immediately and store it somewhere safe. HighLevel shows it only once.

You will also need the sub-account’s Location ID, which appears in the sub-account’s settings and in its URL.

Security first: treat the token like a password. Keep it on the server, in an environment variable or secrets manager — never in front-end JavaScript, a public repository or a form’s hidden field.

Step 2: make your first request

All requests go to https://services.leadconnectorhq.com, with two headers: Authorization: Bearer <token> and a Version header. HighLevel’s Private Integration examples use 2021-07-28; some newer endpoint pages list a different value, so check the page for the endpoint you are calling.

Start by reading the location. If this returns the business details, the token and Location ID are correct.

curl --request GET \
  --url https://services.leadconnectorhq.com/locations/YOUR_LOCATION_ID \
  --header 'Authorization: Bearer YOUR_PRIVATE_INTEGRATION_TOKEN' \
  --header 'Version: 2021-07-28' \
  --header 'Accept: application/json'

Step 3: create a contact

Creating a contact is a POST to /contacts/ with the Location ID and the contact’s details in the body.

curl --request POST \
  --url https://services.leadconnectorhq.com/contacts/ \
  --header 'Authorization: Bearer YOUR_PRIVATE_INTEGRATION_TOKEN' \
  --header 'Version: 2021-07-28' \
  --header 'Content-Type: application/json' \
  --data '{
    "locationId": "YOUR_LOCATION_ID",
    "firstName": "Sam",
    "lastName": "Taylor",
    "email": "sam@example.com",
    "phone": "+15555550123",
    "source": "website",
    "tags": ["website-lead"]
  }'

Send phone numbers in international format with the country code. Tags are a simple way to start a workflow from an API-created contact: build a workflow with the trigger “Contact Tag” added, and every contact you create with that tag enters it.

Step 4: create or update in one call (upsert)

Most real integrations should not create duplicates when the same person submits twice. The upsert endpoint creates a contact if none matches, or updates the existing one. It follows the sub-account’s duplicate-contact settings, and its response tells you whether a new contact was created.

curl --request POST \
  --url https://services.leadconnectorhq.com/contacts/upsert \
  --header 'Authorization: Bearer YOUR_PRIVATE_INTEGRATION_TOKEN' \
  --header 'Version: 2021-07-28' \
  --header 'Content-Type: application/json' \
  --data '{
    "locationId": "YOUR_LOCATION_ID",
    "email": "sam@example.com",
    "firstName": "Sam",
    "tags": ["website-lead", "requested-quote"]
  }'

The same request in JavaScript (Node.js)

This version runs on a server with Node.js 18 or later, which includes fetch. It reads the token from an environment variable and throws a useful error when the API rejects the request.

const GHL_TOKEN = process.env.GHL_TOKEN;          // Private Integration token
const LOCATION_ID = process.env.GHL_LOCATION_ID;  // sub-account ID

async function upsertContact({ firstName, lastName, email, phone, tags = [] }) {
  const res = await fetch('https://services.leadconnectorhq.com/contacts/upsert', {
    method: 'POST',
    headers: {
      Authorization: `Bearer ${GHL_TOKEN}`,
      Version: '2021-07-28',
      'Content-Type': 'application/json',
      Accept: 'application/json',
    },
    body: JSON.stringify({ locationId: LOCATION_ID, firstName, lastName, email, phone, tags }),
  });

  const body = await res.json().catch(() => ({}));
  if (!res.ok) {
    throw new Error(`GoHighLevel API ${res.status}: ${JSON.stringify(body)}`);
  }
  return body; // includes the contact and whether it was newly created
}

upsertContact({ firstName: 'Sam', email: 'sam@example.com', tags: ['website-lead'] })
  .then((r) => console.log('Saved contact', r.contact?.id))
  .catch((err) => console.error(err.message));

The same request in PHP (WordPress)

On a WordPress site, use the built-in HTTP API. Define the token in wp-config.php so it never lives in the database or the theme.

// wp-config.php
define( 'GHL_TOKEN', 'your-private-integration-token' );
define( 'GHL_LOCATION_ID', 'your-location-id' );

// In a small plugin or your child theme's functions.php
function gnx_upsert_ghl_contact( array $contact ) {
	$response = wp_remote_post( 'https://services.leadconnectorhq.com/contacts/upsert', array(
		'timeout' => 15,
		'headers' => array(
			'Authorization' => 'Bearer ' . GHL_TOKEN,
			'Version'       => '2021-07-28',
			'Content-Type'  => 'application/json',
			'Accept'        => 'application/json',
		),
		'body'    => wp_json_encode( array_merge( array( 'locationId' => GHL_LOCATION_ID ), $contact ) ),
	) );

	if ( is_wp_error( $response ) ) {
		error_log( 'GHL request failed: ' . $response->get_error_message() );
		return false;
	}
	$code = wp_remote_retrieve_response_code( $response );
	if ( $code < 200 || $code >= 300 ) {
		error_log( 'GHL API ' . $code . ': ' . wp_remote_retrieve_body( $response ) );
		return false;
	}
	return json_decode( wp_remote_retrieve_body( $response ), true );
}

// Example
gnx_upsert_ghl_contact( array(
	'firstName' => 'Sam',
	'email'     => 'sam@example.com',
	'tags'      => array( 'website-lead' ),
) );

Common errors and what they mean

StatusUsual causeFix
401 UnauthorizedMissing, mistyped or rotated tokenCheck the Authorization header and that the token is still active
403 ForbiddenThe token lacks the scope for this endpoint, or belongs to another locationEdit the integration’s scopes; confirm the Location ID
400 / 422Invalid body — a wrong field name, bad phone format or missing locationIdRead the error body; it usually names the field
429 Too Many RequestsRate limit reachedSlow down and retry with a delay that increases each time
5xxTemporary problem on the API sideRetry later; log the request so nothing is lost

Habits that keep integrations reliable

  • Least privilege. Give each token only the scopes it needs, and use a separate integration for each system so one can be revoked without breaking the others.
  • Rotate regularly. HighLevel recommends rotating Private Integration tokens every 90 days. Its scheduled rotation keeps the old token working for a short overlap so you can update your code without downtime.
  • Rotate immediately if a token may have leaked — in a screenshot, a support ticket or a public repository.
  • Log failures with context. Record the status code and response body, never the token itself.
  • Retry only what is retryable. Retry 429 and 5xx responses with backoff; fix 4xx errors instead of repeating them.
  • Prefer upsert wherever the same person might arrive twice.

API, webhook or Zapier?

The API is best when your own code needs to read or write GoHighLevel data. If you only need to push form submissions into a workflow, an inbound webhook is simpler — see our guide to GoHighLevel API and webhooks. For connecting off-the-shelf apps without code, our Zapier and Make guide is the place to start.

Frequently asked questions

Is a Private Integration token the same as an API key?

It plays the same role. It is a static token tied to an agency or sub-account, with the scopes you choose, and it is sent as a Bearer token.

Can I call the API from browser JavaScript?

You should not. Anyone could read the token from the page. Call the API from your server and expose only what the browser needs.

Do I need OAuth?

Only if you are building an app that other agencies or sub-accounts will install. For your own integrations, a Private Integration token is simpler.

Where do I find the full list of endpoints?

In HighLevel’s developer documentation on the marketplace site, which lists each endpoint, its required scope and its parameters.

Ready to Implement This?

Let GHL Nexa set this up for you. Book a free 30-minute strategy call today.

SHARE THIS ARTICLE