GoHighLevel API Integration: A Developer's Guide
A technical walkthrough of GoHighLevel's v2 API: choosing between a Private Integration Token and an OAuth marketplace app, working within the 100 req/10s and 200,000/day rate limits, verifying webhooks, and migrating off v1 before it's fully retired.

Key takeaways
- The first real decision isn't 'API key or OAuth' — it's whether you're building against your own agency's accounts (Private Integration Token) or your customers' accounts (full OAuth marketplace app), and the two aren't interchangeable.
- GoHighLevel's v2 API caps requests at 100 per 10 seconds and 200,000 per day, per app per resource — a 40k-contact backfill at roughly 3 calls per contact can burn 60% of a day's quota in one run if you don't batch it.
- API v1 stopped accepting new key generation and reached end-of-support on December 31, 2025; existing v1 connections still run but get no fixes or new endpoints, so any 2026 build should start on v2.
- Outbound webhooks have moved from legacy RSA-SHA256 signing to Ed25519 — code copied from an older tutorial may be verifying against the wrong algorithm entirely.
- The official @gohighlevel/api-client Node.js SDK handles OAuth token exchange and daily refresh for you, which removes one of the more common sources of silent integration breakage.
- Agency-level OAuth tokens don't automatically carry location-level permissions — sub-account access has to be explicitly requested and handled, and this is a frequent source of 401/403 errors.
GoHighLevel’s API has two real starting decisions, and most tutorials only cover one of them clearly. The first is authentication: a static Private Integration Token for your own agency’s accounts, or a full OAuth 2.0 marketplace app if you’re building something other agencies will install into their own accounts. The second is respecting the platform’s rate limits, which are generous for normal usage but easy to blow through on a historical data backfill if you’re not batching calls. This guide covers both, along with webhook verification, the v1-to-v2 migration deadline, the marketplace app model, custom objects, and where the official Node.js SDK fits in. If you’re deciding whether to build this yourself or bring in help, GoHighLevel’s implementation cost guide breaks down what agencies typically pay for custom integration work versus off-the-shelf automation.
Which auth method do you actually need: PIT or OAuth?
The question to ask first isn’t “API key or OAuth,” it’s “whose account is this running against.” A Private Integration Token (PIT) is a static credential you generate for accounts you or your agency directly control — no consent screen, no refresh cycle, rotated manually when needed. A full OAuth 2.0 marketplace app is what you need when other businesses, agencies you don’t own, install your integration into their own GoHighLevel account and grant it access through a consent flow.
GoHighLevel’s own developer portal frames this distinction as “your own accounts” versus “your customers’ accounts,” and it’s the single most useful mental model for a developer picking a starting point, largely because most competing tutorials still describe everything under one generic “API key” label. That framing matters because the two paths lead to genuinely different code.
The marketplace app model is worth understanding even if you never plan to publish one publicly. A developer registers an app on GoHighLevel’s marketplace, declares which OAuth scopes it needs (contacts, calendars, conversations, and so on), and chooses whether to keep it private or list it publicly for other agencies to find and install. Each customer or agency that installs the app then grants it access at either the location level or the company level, and that grant is what actually produces the access and refresh token pair your integration uses going forward. A private, unlisted marketplace app is a common middle ground — proper OAuth scoping and per-install token management, without the review process a public listing requires — which is useful if you’re building for a handful of client accounts rather than an open install base.
If you’re building a data sync for your own agency’s sub-accounts, start with a PIT. It’s issued directly in the GoHighLevel UI, following the process documented on the Marketplace developer portal, scoped to the agency or a specific sub-account, and doesn’t require you to stand up a redirect URI or handle a refresh cycle. If you’re building something you intend to list, or even just hand out, to other agencies, you need a registered marketplace app with declared OAuth scopes, because a PIT can’t be distributed to accounts you don’t own. Building and reselling a packaged GoHighLevel-based product to other businesses usually means the marketplace app and a SaaS-mode account setup get built together, so plan for both at once rather than bolting SaaS mode on afterward.
There’s a third, related distinction worth naming here too — an agency-level (company) OAuth token and a location-level (sub-account) token aren’t interchangeable. Getting an agency token doesn’t automatically grant access to a specific sub-account’s data; the target-user context has to be explicitly requested in the OAuth flow. Community threads point to this as a recurring cause of 401 and 403 errors, usually from a developer assuming agency-level access implies sub-account access when it doesn’t.
How the OAuth token exchange and refresh actually work
For a marketplace app, the supported flow is OAuth 2.0 Authorization Code grant, as laid out in HighLevel’s OAuth 2.0 documentation. A user authorizes your app, GoHighLevel redirects back with a code, and you exchange that code for an access token and a refresh token. The access token in HighLevel’s own documented example expires in 3,600 seconds, one hour, which is short enough that refresh handling isn’t optional for anything running unattended.
POST https://services.leadconnectorhq.com/oauth/token
Content-Type: application/x-www-form-urlencoded
grant_type=refresh_token
&client_id=YOUR_CLIENT_ID
&client_secret=YOUR_CLIENT_SECRET
&refresh_token=STORED_REFRESH_TOKEN
GoHighLevel can auto-refresh the access token on your behalf when a request comes back with a 401 — but only if auto-refresh stays enabled on the integration. Disable it, or build a custom token exchange that doesn’t handle the 401-then-refresh cycle, and the connection quietly breaks the first time a token expires mid-session, forcing a full re-authorization. Developer-forum reports of “integration stopped working overnight” during 2025 and 2026 trace back to this setting almost every time.
This is exactly the kind of bookkeeping the official SDK exists to remove. @gohighlevel/api-client, published on npm and requiring Node.js 18 or later, handles the OAuth exchange and refreshes tokens automatically rather than leaving it to application code. The source is open, published alongside GoHighLevel’s docs repo, so you can read the token-refresh logic directly rather than treating it as a black box. If you’re starting a new Node or TypeScript project against the v2 API, installing the SDK is a reasonable default rather than writing your own token-handling layer from scratch.
npm install @gohighlevel/api-client
What are GoHighLevel’s rate limits, and how do you budget for them?
GoHighLevel’s v2 API caps you at 100 requests per 10 seconds as a burst limit, and 200,000 requests per day, both scoped per marketplace app per resource, meaning per Location or per Company. Both numbers come from HighLevel’s Rate Limits documentation, not a third-party estimate. Every response also carries live rate-limit headers, X-RateLimit-Limit-Daily, X-RateLimit-Daily-Remaining, X-RateLimit-Interval-Milliseconds, X-RateLimit-Max, and X-RateLimit-Remaining, so you don’t have to hardcode assumptions about how much budget is left.
For steady-state usage, syncing contacts as they change, sending appointment confirmations, logging conversation events, 200,000 requests a day is a lot of headroom. The number that actually catches teams off guard is a one-time historical backfill. Say you’re bringing 40,000 existing contacts into a data warehouse, and pulling a full contact record plus its opportunities and recent conversation history takes roughly three API calls per contact. That’s about 120,000 calls — 60% of a single day’s quota — consumed by one migration job.
Two practical takeaways follow from that math. Batch reads wherever the endpoint supports it rather than issuing one call per field or per related object, and schedule large backfills overnight or during low-traffic windows so they don’t compete with real-time sync and webhook traffic running the rest of the day. Watch the X-RateLimit-Daily-Remaining header as your actual budget signal instead of estimating from a fixed number, since normal daily traffic (workflow triggers, inbound webhook processing, other integrations on the same app) is drawing from the same 200,000-request pool.
A few implementation details make that budgeting easier in practice. Use cursor-based pagination on list endpoints rather than pulling one record at a time, since a single paginated call covering fifty or a hundred contacts costs the same rate-limit unit as a call covering one. Build in exponential backoff on any 429 response rather than retrying immediately, both because an immediate retry storm makes the burst limit worse, not better, and because it tends to trip the 10-second window repeatedly instead of clearing it. And keep the backfill job itself idempotent — able to safely resume from where it stopped rather than re-pulling records it already synced — so a mid-run failure at request 80,000 doesn’t cost you another 80,000 calls the next day just to get back to where you were.
Why the v1-to-v2 migration is no longer optional
If you’re starting a new build in 2026, this part is simple: use v2. Per GoHighLevel’s official v1-to-v2 migration notice, the v1 API reached official end-of-support on December 31, 2025. New v1 API keys can no longer be generated, and while existing v1 connections keep running, they receive no further fixes and no new endpoints. Any tutorial, code sample, or SDK still referencing v1 API keys is out of date for anything you’re building today.
If you inherited an integration built on v1, the practical move is auditing which endpoints it calls, mapping each to its v2 equivalent, and re-authenticating with a PIT or OAuth app rather than a legacy v1 key. GoHighLevel’s official docs repository — github.com/GoHighLevel/highlevel-api-docs — publishes both markdown documentation and an OpenAPI spec for v2, maintained by GoHighLevel with community pull requests and issues, which is the more current reference to work from than most third-party tutorials.
What does the v2 API surface actually cover?
The v2 API exposes endpoints for contacts, opportunities (pipelines), calendars (appointments and scheduling), conversations (SMS, email, and messaging), workflows (trigger and list access, not full workflow-builder CRUD), payments, locations (the sub-account API, formerly called the “location” API), and custom objects. This is a representative list of the endpoints developers reach for most often, not an exhaustive reference; the full OpenAPI spec in GoHighLevel’s docs repo is significantly larger and worth checking directly for anything not covered here.
Custom objects deserve a specific mention because they’re one of the more under-documented parts of the API in third-party content, despite being genuinely useful. Per GoHighLevel’s own support portal on custom objects in workflows, custom objects can act as both triggers and actions inside workflows, meaning a change to a non-standard business entity, a piece of equipment, a membership record, a property listing, can kick off an automation the same way a standard contact-field change does. That makes custom objects a reasonable foundation for internal tooling that doesn’t map cleanly onto contacts or opportunities.
In practice, a custom object is defined with its own schema, fields, and relationships to standard records like contacts, and then exposed through the same v2 API surface as any built-in resource: you can create, read, update, and query custom-object records through dedicated endpoints, and associate a record with one or more contacts so it shows up in that contact’s timeline inside the GoHighLevel UI. The trigger side works the same way it does for a contact field change: a workflow can watch for a custom-object record being created or an attribute on it changing, then fire an automation, send a notification, or update a related contact or opportunity. The action side lets a workflow write to or create a custom-object record as a step, which is what makes them useful for things like equipment maintenance schedules, membership tiers, or property listings that don’t belong on a standard contact record but still need to drive automation. Because this piece of the API is thin in most third-party tutorials, it’s worth testing custom-object trigger behavior directly in a sandbox sub-account before building production automation around it, rather than assuming it mirrors contact-trigger behavior exactly.
Inbound webhooks (external systems calling into GoHighLevel) can trigger a workflow the same way a UI action does, and outbound webhooks (GoHighLevel calling out to your system) fire on the same underlying events regardless of whether the change originated from a user in the UI or from your own API writes. That second point is worth internalizing before building a sync loop: if your integration writes a contact update via the API and also listens for the outbound webhook on contact updates, you can end up processing your own write as an incoming event unless you deliberately de-duplicate or check the change source. If workflows built on these triggers stop firing as expected, GoHighLevel Workflow Not Triggering covers the more common causes.
How do you verify a GoHighLevel webhook is genuine?
Treat every incoming webhook as untrusted until its signature checks out. GoHighLevel’s outbound webhooks currently use Ed25519 asymmetric signing — a shift from an older RSA-SHA256 method some existing integrations and tutorials still reference. GoHighLevel holds the private key and signs each payload; your integration verifies the signature against a published public key rather than a shared secret.
Here’s the caveat worth being direct about: multiple practitioner sources and GoHighLevel’s own official PHP SDK repository (which supports verification against both the legacy RSA-SHA256 method and the current Ed25519 method) corroborate this Ed25519 shift, but it wasn’t independently confirmed against GoHighLevel’s own webhook-signature documentation page at the time of writing. Before shipping signature-verification code, pull the exact header name and verification steps from GoHighLevel’s current developer docs rather than trusting a copied code sample, since older tutorials built against the RSA-SHA256 era will verify against the wrong algorithm entirely and silently reject or, worse, silently accept payloads they shouldn’t.
When is the direct API worth it over Zapier or Make?
No-code platforms and direct API integration aren’t strictly either/or, and plenty of teams run both. Zapier and Make tend to be the better fit for connecting a handful of external apps quickly, for workflows a non-developer needs to maintain, and for lower request volume where hitting rate limits or building custom retry logic isn’t a real concern. If that’s your situation, autoesta’s Zapier and Make automation team builds exactly that kind of no-code connection between GoHighLevel and the rest of a client’s stack without needing custom API code at all.
Direct API and webhook integration earns its complexity at sustained high request volume, when you need custom data transformations that a no-code connector can’t express, when you’re building something a reusable product meant to be installed across multiple agencies rather than wired into one account, or when you need real-time webhook-driven response instead of polling on a schedule. Custom objects and granular conversation data are also cases where the Zapier app’s coverage tends to lag behind what the raw API exposes. There’s no single authoritative published breakeven point, in cost or request volume, between the two approaches; treat that decision as directional based on your own integration’s shape rather than looking for a universal threshold, because none has been credibly published.
The two approaches also differ in who maintains the result once it ships. A Zapier or Make scenario lives inside a visual editor that a marketing ops person can usually read and adjust without a developer, while a direct API integration is code, versioned, tested, and deployed the way any other application is. That’s not a knock on either approach — it’s a maintenance-cost question worth answering explicitly before you build: who owns this integration a year from now, and does that person work in a no-code canvas or a codebase?
A few integration patterns worth knowing about
Three patterns come up often enough in GoHighLevel integration work to be worth naming, presented here as illustrative shapes rather than case studies.
A data warehouse sync pulls contacts, opportunities, and conversation events, via the API or outbound webhooks, into BigQuery or Snowflake for reporting that GoHighLevel’s native dashboards don’t support. This pattern typically runs on a scheduled batch pull for historical and slowly-changing data (contact records, pipeline stages) combined with a webhook listener for anything that needs to show up in the warehouse close to real time, like a new conversation or an appointment booking. The rate-limit math from earlier applies directly here: the scheduled batch portion is where a poorly-paced backfill eats the daily quota, while the webhook portion barely touches it since inbound events don’t count against your outbound request budget the same way.
An external booking or billing sync uses the /calendars/ and /payments/ endpoints together with inbound webhooks to reconcile a GoHighLevel-run booking flow against an external billing system, or the reverse. The trickiest part of this pattern is usually not the API calls themselves but the reconciliation logic: deciding which system is the source of truth for a given field, and handling the case where a booking gets modified in both systems close together in time. Building an explicit “last write wins” or “GoHighLevel is authoritative for scheduling, the external system is authoritative for billing” rule up front avoids a class of subtle sync bugs that only show up under real usage.
Custom-object-driven internal tooling models business entities that don’t fit the contact or opportunity shape, then drives automations off record changes, the pattern GoHighLevel documents directly on its own support portal. Agencies serving equipment-heavy or membership-heavy verticals, such as HVAC service contracts or gym memberships, tend to reach for this pattern most, since a piece of equipment or a membership tier genuinely isn’t a contact or an opportunity but still needs to trigger reminders, renewals, or service workflows. For agencies building integrations across multiple clients, GoHighLevel Sub-Accounts Explained covers how that isolation boundary affects API scoping and access, which matters even more once a custom-object pattern has to behave the same way across every client’s sub-account.
Getting started without the common mistakes
Most of the friction in a GoHighLevel integration traces back to one of a handful of avoidable decisions: picking OAuth when a PIT would have done the job (or the reverse, trying to distribute a PIT-based integration to accounts you don’t own), disabling auto-refresh without building your own 401-handling logic, not budgeting rate limits before a backfill job, or building against v1 in 2026. None of these require deep platform expertise to avoid, just deciding on auth model and rate-limit strategy before writing the first request rather than after debugging a broken integration in production.
Not every team needs custom API code to get most of the value out of these patterns, either. If the actual goal is workflow automation inside GoHighLevel rather than a standalone integration product, HighLevel Automation Team builds those automations directly on the platform without a custom API layer in most cases, which is often the faster path when the requirement is “automate this process” rather than “connect this external system.” Worth checking a current GoHighLevel pricing breakdown against either approach before deciding what fits the budget.
If you’re scoping a GoHighLevel integration and want a second read on the auth model or data architecture before you build, aibrevo’s GoHighLevel implementation team works with the same v2 API day to day and can sanity-check an approach before it turns into a rebuild.