# GoHighLevel API v2 — Reference (2026-07)

Distilled from the official GoHighLevel docs (`marketplace.gohighlevel.com/docs/Authorization/PrivateIntegrationsToken/` and `help.gohighlevel.com/support/solutions/articles/155000003054`). Use this whenever the user wants to wire GHL into a local agent, a cron job, an MCP server, or any custom automation.

## Why Private Integration Tokens (PIT) — and why the user's earlier attempt failed

The user previously spent hours trying to "build an app to connect" to GHL. That path is the **Marketplace App** path, which requires OAuth flows, app registration, approval, and either Public distribution (full marketplace review) or Private distribution (limited to 5 installs). It's weeks of work and the wrong fit for a single agency.

**The right path for a single agency is Private Integration Tokens (PIT) — a static Bearer token you generate in the GHL UI, with scoped permissions, in about 10 minutes.** PIT is the recommended replacement for the legacy v1 API Keys, which are end-of-life.

**If the user has tried and failed before, the failure is almost certainly the path choice (Marketplace App or v1 API Keys), not the API itself.**

## The auth pattern (this is the only thing you need)

```http
GET https://services.leadconnectorhq.com/locations/search
Authorization: Bearer <PIT_TOKEN>
Version: v3
Accept: application/json
```

- **Base URL:** `https://services.leadconnectorhq.com/`
- **Header `Version: v3`** is required on every request (this is the API v2 contract version — the old `Version: 2021-07-28` was for the v1 API keys, which are end-of-life. Using the wrong version returns 404 on valid endpoints.)
- **Header `Authorization: Bearer <token>`** uses the static PIT — no refresh, no expiry (until you rotate)
- **Header `Content-Type: application/json`** on POST/PUT
- Token is **scoped** to specific permissions you select at creation — the user can't accidentally give the token more access than they intended
- The `/locations/search` endpoint is **GET** (not POST) when called with an agency PIT — POST is for some other auth flows, see the GHL docs if you need it

## Generating a PIT (the user does this in the GHL UI)

1. Log in to GHL as an agency admin
2. Go to **Settings → Private Integrations** (if not visible, enable the feature in Labs)
3. Click **"Create new Integration"**
4. Name it (e.g. "Hermes GHL Bridge"), add a description
5. Select scopes — start with the minimum needed:
   - `contacts.readonly` and/or `contacts.write` (read/write contacts)
   - `opportunities.readonly` and/or `opportunities.write` (read/write opportunities)
   - `locations.readonly` (read sub-account info, often needed as a prerequisite)
   - `calendars.readonly` / `calendars.write` (if automating scheduling)
   - `conversations.readonly` / `conversations.write` (if automating messaging)
6. Click Create, **copy the token immediately** — it cannot be shown again
7. Store the token somewhere the agent can read (e.g. `~/.hermes/profiles/main/.env` as `GHL_PRIVATE_INTEGRATION_TOKEN=...`)

**Limits:** max 5 PITs per agency AND 5 per location. If the user already has 5, they must delete one before creating more.

**Token rotation:** every 90 days is recommended. The "Rotate and expire this token later" option gives a 7-day overlap window where both old and new tokens work.

## API v1 is dead — do not use

- API v1 (the old `/v1/` endpoints and API Key auth) is **end-of-life** and no longer maintained.
- Any code, tutorial, or YouTube video older than mid-2025 that uses API Keys or `https://api.gohighlevel.com/v1/` is wrong.
- If the user has a tutorial or product that still uses v1, that product is broken and the user should contact the vendor or migrate to v2.
- API v2 is the only path. All endpoints live under `https://services.leadconnectorhq.com/`.

## Endpoints the user is most likely to want

These are the high-leverage endpoints for a GHL agency owner's day-to-day. Full list is at `developers.gohighlevel.com/`.

| Use case | Endpoint | Method |
|---|---|---|
| List contacts in a sub-account | `/contacts/?locationId=<ID>` | GET |
| Get a specific contact | `/contacts/{contactId}` | GET |
| Create a contact | `/contacts/` | POST |
| Update a contact (tags, custom fields) | `/contacts/{contactId}` | PUT |
| List opportunities in a pipeline | `/opportunities/search` (POST with `pipelineId`, `locationId`, `status`) | POST |
| Move opportunity to a new stage | `/opportunities/{oppId}` | PUT with `pipelineStageId` | PUT |
| List pipelines + stages | `/opportunities/pipelines?locationId=<ID>` | GET |
| List sub-accounts (locations) | `/locations/search` | POST |
| List calendars | `/calendars/?locationId=<ID>` | GET |
| Book an appointment | `/calendars/events/appointments` | POST |
| Add a contact to a workflow | `/contacts/{contactId}/workflow` | POST |

**The `locationId` parameter is critical.** GHL is multi-tenant (one agency, many sub-accounts). Almost every operation is scoped to a `locationId`. The user can find their sub-account IDs via `/locations/search` (with the agency PIT, no `locationId` needed).

## Wiring it into Hermes (the actual goal)

Once the user has a PIT, the workflow is:

1. **Add the token to `~/.hermes/profiles/main/.env`** as `GHL_PRIVATE_INTEGRATION_TOKEN=...`
2. **Make Hermes know about GHL** — three options, in increasing order of effort:
   - **One-off scripts:** the user can ask Hermes "list my contacts in pipeline X" and Hermes calls a script that hits the GHL API. Hermes already has shell + file tools, so this works out of the box.
   - **MCP server:** build a small MCP server wrapping the GHL endpoints the user actually uses, install it via the Agentic OS dashboard's MCP catalog (`/hermes → MCPs` tab), and Hermes can call GHL natively as a tool.
   - **Cron job:** for periodic sync (e.g. "every morning at 8am, pull yesterday's new contacts"), use Hermes's cron scheduler.
3. **Test with curl first** before wiring into Hermes. The bash form of the auth pattern works for testing any endpoint.

## Testing the token (the user can do this immediately)

```bash
# Get the agency's sub-accounts (proves the token works)
# Note: GET (not POST), Version: v3 (not 2021-07-28 — that's the deprecated v1 API key version)
curl --request GET \
  --url https://services.leadconnectorhq.com/locations/search \
  --header 'Authorization: Bearer ***' \
  --header 'Version: v3' \
  --header 'Accept: application/json'

# If the user has a sub-account ID, list contacts
curl --request GET \
  --url "https://services.leadconnectorhq.com/contacts/?locationId=<LOCATION_ID>" \
  --header 'Authorization: Bearer ***' \
  --header 'Version: v3'
```

If both return 200, the token is good. If 401, the token is wrong or revoked. If 403, the scope is missing — go back to the PIT settings and add the scope. If 404, the Version header is almost certainly wrong (you used `2021-07-28` instead of `v3`).

## When this skill applies (trigger phrases)

- "connect GHL" / "GHL API" / "GoHighLevel integration"
- "automate my GHL" / "pull contacts from GHL" / "list opportunities"
- "build an app for GHL" (user is probably wrong about the path — push them toward PIT)
- "use the agency token" / "private integration"
- "GHL webhook" (GHL also supports webhooks for inbound events — different mechanism, ask if that's the goal)

## Pitfalls

- **API v1 is dead.** Reject any code path, tutorial, or vendor product that uses `https://api.gohighlevel.com/v1/` or API Keys. Push the user to v2 + PIT.
- **The token is shown ONCE at creation.** If the user creates a PIT and doesn't copy it, they have to delete and recreate. The "Rotate" flow always shows the new token.
- **PIT limits: 5 per agency, 5 per location.** If the user has been creating tokens for every experiment, they may be at the cap.
- **The `Version: v3` header is required on every GHL API v2 request.** Forgetting it returns 404 on valid endpoints. The legacy `Version: 2021-07-28` was for the v1 API key auth and is end-of-life — do not use it. Worked example from 2026-07-12: I sent the user a recipe with `Version: 2021-07-28` (carried over from outdated docs), the API returned `Cannot GET /locations/` 404, and we wasted a round-trip before realizing the version header was the problem. The correct header is the literal string `v3`.
- **Most operations are scoped to a `locationId` (sub-account).** Operations at the agency level (like listing all sub-accounts) need the agency PIT and no `locationId`. Operations on contacts/opportunities/calendars need the `locationId`. The user often has one PIT per location AND a separate agency PIT — they may need both.
- **The user previously failed because they tried the Marketplace App path.** Don't assume that failure means the API is hard — the right path is much simpler. Lead with the PIT path, get the user a working token in 10 minutes, then build from there.
- **"Build an app to connect" is a red-flag phrase.** It suggests the user (or someone they talked to) is on the wrong path. Before doing any "app" work, confirm they're aware of PIT. PIT is not an "app" in the Marketplace sense — it's just a static API token.
- **GHL's Social Planner and other newer endpoints have known JWT issues with PIT** (per Make community forums, June 2026). If a specific endpoint returns "Invalid JWT" with a valid PIT, that's a GHL bug, not a configuration error. Workaround: use webhooks instead of polling for that endpoint.
- **Don't suggest the Marketplace App path unless the user is actually building a product for other agencies.** A single agency doing automation wants PIT, period.
