highlevel-cli
v2.1.0
Published
Command-line interface for GoHighLevel, built around the writes that lose data
Maintainers
Readme
highlevel-cli
A command-line interface for GoHighLevel, built around the writes that lose data.
New here? → QUICKSTART.md - nothing to a working command in five minutes.
792 commands. 165 hand-written across 16 groups that guard GoHighLevel's destructive write behaviour, plus 627 generated from the official v3 OpenAPI specs so nothing is missing. Three API layers. And - the part that matters - a write-safety layer built out of the things that have actually gone wrong on live accounts.
Works with one sub-account. Scales to many: every command takes --profile, and the
fleet group drives a whole roster from a YAML manifest.
npm install -g highlevel-cli
ghl --dry-run --profile LOCATION_A locations custom-values set --key business_hours --value "8-5"Every shipped command, grouped by resource, with the curated/generated overlaps flagged:
ghl --help for the groups, ghl api --help for the generated tree.
Write safety
This is the reason the project exists, so it goes first.
GoHighLevel has several endpoints that destroy data on a well-formed request. Not on a malformed one - on exactly the request the docs describe. Every item below is a guard around something that already broke a production account.
- Global
--dry-run. Intercepts everyPOST/PUT/DELETEacross all three clients and returns synthetic success. Reads pass through untouched. Put it in front of any command and you get the exact request body you would have sent, on stderr and in the audit log. - Audit log. Every write - dry-runs included - appends a JSONL record to
~/.ghl-cli/audit.log: profile, method, path, body, outcome. A replayable answer to "what touched this sub-account, and when". - Read-back verification with lag polling. GHL has ~1-2s read-after-write lag on custom values, snippets, and forms. A naive immediate re-read reports a successful write as lost, so verification polls instead of reading once.
- Fetch-merge on full-replace PUTs. Workflows, calendar slot blocks, domains, trigger links, and the service menu all silently null every field you omit. Partial updates are merged against a fresh read before sending.
WorkflowStepWipeError. The shared internal client hard-refuses a workflow-document PUT that carries noworkflowData.templates. The rule it enforces was written down first, and violated anyway: a bulk folder sweep blanked the action steps of roughly a hundred live workflows, returning 200 on every call and looking correct until the automations stopped firing. Documentation did not prevent that. A guard on the path every internal write shares does.- Calendar
slotDurationunit trap. SendingslotDurationUnit: "hours"is silently divided by 60, turning a 2-hour service into 2-minute bookable slots on a live calendar. - Confirm gates on every irreversible or batch-destructive write.
--yesto script past them, deliberately. - 429-only retry with backoff and
Retry-After. 5xx bubbles up rather than being retried blind, andPOST /locations/- billable, no rollback - never auto-retries. - Profile isolation via contextvars, not globals. A raw
threading.Threaddoes not copy context, so an unwrapped thread lands your write on the wrong sub-account.
What this does not protect you from
Guards that stop at a known boundary are worth more than guards you believe are total, so:
ghl apiis raw. The generated tree sends exactly the body you give it: no fetch-merge, no read-back. On GoHighLevel's full-replacePUTs that is a data-loss command. Where a curated equivalent exists, it is the one to use -ghl calendars updatemerges,ghl api calendars update-calendardoes not.WorkflowStepWipeErrorcatches a blanked step list, not a shortened one. A PUT carrying one of three existing steps passes the guard and deletes the other two, because the client cannot know what the document held before. Read the workflow first.--profiledoes not scope--experimentaldeletes. Several internal endpoints (DELETE /forms/{id}among them) take no location, so the id you pass decides the account. The profile is an audit label there, not a boundary.- Pagination can end early.
paginate()stops when a page yields no cursor, and reaching--max-pagesends it silently. Treat a result at the page limit as partial. doctorproves the credentials resolve, not that they are sufficient. Apit-token is opaque - no expiry, no scope list - and the live test reads one endpoint.
Why this and not the official MCP
HighLevel ships an official MCP server with 36 tools: Calendar (2), Contacts (8), Conversations (3), Locations (2), Opportunities (4), Payments (2), Blogs (7), Email templates (2), Social (6).
What that list contains zero of: workflows, snippets, forms, custom values, pipeline
writes, reputation, snapshots, sub-account provisioning, the service menu, AI agents, billing,
and analytics. There is also no --dry-run, no audit trail, and no fetch-merge - so a partial
update is a live data-loss event rather than a caught mistake.
HighLevel has published a roadmap toward 250+ tools, so treat the endpoint gap as being on a clock. The write-safety layer is the part that does not erode.
Install
Needs GoHighLevel sub-account access (agency access for the fleet and agency commands).
It does not need Python - the released binaries carry their own.
npm
npm install -g highlevel-cli # then just `ghl`
npx highlevel-cli doctor # or run it without installingThe npm package carries a prebuilt binary, so this needs Node only to install -
the tool itself has no runtime dependency once it is on disk. npm picks the right
binary for your platform from the os/cpu fields; there is no postinstall
script, so npm ci --ignore-scripts works too.
Direct download
One file, no runtime, nothing else installed. Pick your platform from the latest release:
curl -L -o ghl https://github.com/chowdhuryrz/highlevel-cli/releases/latest/download/ghl-macos-arm64
chmod +x ghl && sudo mv ghl /usr/local/bin/
ghl --versionSwap ghl-macos-arm64 for ghl-macos-x86_64, ghl-linux-x86_64, or
ghl-windows-x86_64.exe. Each release ships a .sha256 beside every binary.
With Python, if you would rather manage it that way
Needs Python 3.10+. Not on PyPI yet, so these install from the repo:
uv tool install git+https://github.com/chowdhuryrz/highlevel-cli # puts `ghl` on PATH — like `npm i -g`
pipx install git+https://github.com/chowdhuryrz/highlevel-cli # same isolation, if you already use pipxPlain pip install works only inside a virtualenv you control. Outside one it drops this
package and its dependencies into whichever environment is active - on a current macOS
that is usually an externally-managed-environment refusal, and where it succeeds it can
break an unrelated project sharing the interpreter.
No uv? curl -LsSf https://astral.sh/uv/install.sh | sh
From a clone
git clone https://github.com/chowdhuryrz/highlevel-cli && cd highlevel-cli
./install.shThen:
- Copy
.env.example→.envand fill in at minimumGHL_API_KEY_DEFAULTandGHL_LOCATION_ID_DEFAULT.DEFAULTis just the profile name used when you pass no--profile; any name works. The CLI reads the nearest.envfrom your working directory upward, and real environment variables always win over it. - Audit it:
ghl doctor- enumerates every token, decodes the JWTs, checks expiry, and smoke-tests one read per profile. - Smoke-test:
ghl --json locations get - Optional:
ghl extensionunpacks a small Chrome extension that grabs your Firebase credentials, which only the--experimentalcommands need. Everything else runs on the token from step 1.
.env is yours and is gitignored. Nothing in this repo writes to it.
The three API layers
| Layer | Host | Auth | Covers |
|---|---|---|---|
| Public | services.leadconnectorhq.com | per-profile PIT (GHL_API_KEY_<PROFILE>) | reads plus most location writes |
| Internal | backend.leadconnectorhq.com and parts of services | Firebase JWT from GHL_FIREBASE_REFRESH_TOKEN | everything the web UI can do: workflow writes, snippets, pipelines, form and custom-value deletes, reputation, stats, wallet |
| Agency | services.leadconnectorhq.com | agency PIT (GHL_AGENCY_API_KEY) | snapshots, sub-account create, location-token mint |
A Private Integration Token cannot write workflows, pipelines, snippets, or domains, and
cannot read reviews, stats, or wallet at all. Those are the --experimental surfaces: they run
against undocumented internal endpoints that GoHighLevel may change without notice. Read
SECURITY.md before you rely on them.
Per-endpoint detail, including full reproductions of every internal route - method, path,
headers, auth layer, request and response shape - is in
docs/api-capabilities.md. That document is the most useful
thing in this repo.
Usage
# default profile
ghl contacts list --limit 5
ghl --json workflows list
# switch sub-account
ghl --profile LOCATION_A --json contacts list
GHL_PROFILE=LOCATION_B ghl --json forms list
# preview any write without sending it
ghl --dry-run --profile LOCATION_A locations custom-values set --key business_hours --value "..."
# internal API (needs GHL_FIREBASE_REFRESH_TOKEN)
ghl --experimental --profile LOCATION_A --json workflows get <workflowId>
# across a roster of sub-accounts
ghl fleet inventory --profiles ALL --scope all --out /tmp/inventory.json
ghl --dry-run fleet deploy --manifest fleet/locations.example.yaml --profile LOCATION_A
# agency
ghl agency snapshot list
ghl agency location create-from-snapshot --name "..." --timezone America/New_YorkMore recipes in docs/cli-cookbook.md.
Shell completion
# bash - add to ~/.bashrc
eval "$(_GHL_COMPLETE=bash_source ghl)"
# zsh - add to ~/.zshrc
eval "$(_GHL_COMPLETE=zsh_source ghl)"
# fish
_GHL_COMPLETE=fish_source ghl > ~/.config/fish/completions/ghl.fishCompletes group names, command names and flags, including the generated api tree.
Global flags
| Flag | Effect |
|---|---|
| --json | machine-readable output (use this for agents and pipes) |
| --profile <NAME> | pick a sub-account profile from .env (default DEFAULT) |
| --location-id <ID> | use this location ID instead of the profile's (the token still comes from the active profile) |
| --experimental | enable the internal-API commands |
| --dry-run | intercept every POST/PUT/DELETE, log the intended request, return synthetic success. Reads still hit the server. |
Command groups
--exp marks commands that additionally require --experimental.
| Group | Commands |
|---|---|
| contacts | list, get, create, update, delete, search, add-tag, remove-tag, notes (5), tasks (6) |
| conversations | list, get, create, update, delete, messages, get-email, send |
| opportunities | list, get, create, update, delete, pipelines, pipeline-create --exp, pipeline-update --exp, pipeline-delete --exp |
| calendars | list, get, create, update, delete, slots, appointments, book, groups |
| service-menu | services, categories, addons, locations, resources, booking-options, config (25 commands) |
| workflows | list, get --exp, create --exp, create-step --exp, create-n8n --exp, delete --exp, duplicate --exp, move --exp, add-opp-action --exp, lint, enroll, remove |
| snippets --exp | list, get, create, update, move, folders, folder-create, folder-delete |
| forms | list, submissions, delete --exp, delete-submissions --exp |
| locations | get, update, search, tags, custom-fields, custom-values (list/get/create/set/import/delete --exp/folder-create/folder-get/folder-delete), business-profile --exp |
| links | list, get, create, update, delete |
| reputation | widgets (list/get/create/update/delete) --exp |
| domains | list, get, add, update, delete, check, validate |
| ai | agents, bot --exp, kb, faqs, deploy-template |
| users | list, get |
| agency | snapshot (list/get/last-push), location (create-from-snapshot/mint-token) |
| fleet | inventory, deploy, verify, generate-manifest |
| doctor | pre-flight .env and token audit |
| api | the generated long tail - 627 operations across 42 resources (see below) |
ghl api - the generated long tail
Every group above except api is hand-written. Five more (payments, invoices, social,
emails, documents) were removed once ghl api shipped: they were thin wrappers over
the same endpoints, and the generated tree exposes far more of each - 45 social-planner
operations against the 3 they had, 42 invoice operations against 4. Everything else GoHighLevel documents is reachable
through ghl api, which builds its Click command tree at runtime from
ghl_cli/data/api_index.json - 627 operations across 42 resources, compiled straight from
GoHighLevel's official v3 OpenAPI specs.
ghl api --help # 42 resources
ghl api surveys --help # operations on one resource
ghl api surveys get-surveys
ghl --dry-run api surveys get-surveys # honoured here tooGenerated commands send Version: v3, go through the same public-API client as everything
else, and therefore inherit --dry-run and the ~/.ghl-cli/audit.log record for free.
Regenerate the index against newer specs with python scripts/build_api_index.py.
They are raw endpoint wrappers. No fetch-merge on full-replace PUTs, no read-back verification, no confirm gates - none of the Write safety machinery above. That machinery is per-endpoint knowledge and it only exists in the curated groups.
So: where both exist, use the curated command. A generated resource can never shadow a
curated group - api is registered last and lives under its own namespace - but the reverse
mistake is easy to make by hand. Where a curated group and a generated resource share a
name, the curated one wins - it is registered first and owns the name.
Reach for ghl api for the endpoints nothing else covers, and for reads, where there is
nothing to destroy.
Profiles
Every command takes --profile <NAME>, which resolves GHL_API_KEY_<NAME> and
GHL_LOCATION_ID_<NAME> out of the environment. Add a sub-account by adding two lines to
.env - there is no registry to update. fleet commands operate over the whole set from a
YAML manifest; see fleet/locations.example.yaml.
Working with AI agents
The CLI is one bash tool instead of a several-hundred-tool MCP server, --json on every
command, and --help at each level - so an agent discovers only what it needs, when it
needs it. See QUICKSTART.md for the pattern, and
give your agent --dry-run before its first write.
Development
python3 -m venv .venv && .venv/bin/pip install -e '.[dev]'
.venv/bin/python -m pytest tests/ # 285 tests, well under a second
.venv/bin/python -m ruff check .See CONTRIBUTING.md.
License
MIT - see LICENSE. Portions derive from an earlier project; see NOTICE.
