# GHL API Reference — condensed, task-relevant notes

Base: `https://services.leadconnectorhq.com` · Headers on EVERY call:
`Authorization: Bearer $GHL_TOKEN`, `Version: 2021-07-28`, `Accept: application/json`

**Auth as of 2026-08-03: OAuth (V3 agency token) is PRIMARY — see `oauth-token-lifecycle.md`.** The PIT below is legacy. The V3 agency token reads+writes most sub-account endpoints (Social Planner verified); funnels remain location-token-only (401 authClass even on V3). Sub-account creation works via `POST /locations/` {name, companyId, email} → 201. Website/funnel CLONING between sub-accounts is UI-only (snapshots) — no API exists.

Auth: Private Integration Token (sub-account token), created at
Subaccount → Settings → Private Integrations. Token shown ONCE at creation.
Rotation UI shows "new"/"old" cards; both live during ~7-day grace, then old expires.

## Endpoints verified live 2026-07-18

| Endpoint | Notes |
|---|---|
| `GET /locations/search?limit=N` | Works even on nearly-scopeless tokens. Returns id, companyId, name, address. Good identity probe. |
| `GET /funnels/funnel/list?locationId=…` | Lists funnels incl. steps, page IDs, URLs, dates. READ-ONLY. |
| `GET /funnels/page?locationId=…&funnelId=…&limit=N&offset=N` | Pages of one funnel. READ-ONLY. Docs: marketplace.gohighlevel.com/docs/ghl/funnels/ |
| `GET /contacts/?locationId=…&limit=N` | |
| `GET /forms/?locationId=…&limit=N` | |
| `GET /calendars/?locationId=…` | |
| `GET /campaigns/?locationId=…` | |

## What does NOT exist (verified via scope picker screenshot + docs)

- No `funnels/funnel.write`, no page-content write scopes for Private Integrations.
  "Select all" is the ceiling. Funnel/website content is NOT API-editable.
- No public API for AI Studio projects at all — copy-paste via its code editor only.
- AI Studio has no bulk export (zip/GitHub) — feature requested on ideas.gohighlevel.com, unshipped.

## Error dictionary

| Response | Meaning | Action |
|---|---|---|
| 401 "Invalid Private Integration token" | String wrong or dead | Re-copy token as TEXT (never OCR from screenshot) |
| 401 "The token is not authorized for this scope" | Token VALID, scope missing | Scopes lock at token creation — mint a NEW integration/token with scopes selected during creation |
| 200 on `/locations/search` but 401 everywhere else | The scope-lock pitfall | Same fix: new token |

## Misc quirks

- `Version` header is mandatory; omitting it = 401/422 depending on endpoint.
- Token values are UUID-shaped (`pit-xxxxxxxx-…`) — beware transcription errors; always
  have the user use the UI's Copy button and paste as text.
- Response envelopes: `{"funnels":[…]}`, `{"contacts":[…]}`, `{"forms":[…], "total":N}` etc.
