<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: AutogenCRM</title>
    <description>The latest articles on DEV Community by AutogenCRM (@autogencrm).</description>
    <link>https://dev.to/autogencrm</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4167738%2Ffcaeb54d-888f-41bf-8ed5-7f6fb75c79aa.png</url>
      <title>DEV Community: AutogenCRM</title>
      <link>https://dev.to/autogencrm</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/autogencrm"/>
    <language>en</language>
    <item>
      <title>Getting Started with the GoHighLevel API: Auth, Webhooks and Common Gotchas</title>
      <dc:creator>AutogenCRM</dc:creator>
      <pubDate>Wed, 07 Oct 2026 04:43:35 +0000</pubDate>
      <link>https://dev.to/autogencrm/getting-started-with-the-gohighlevel-api-auth-webhooks-and-common-gotchas-5fo3</link>
      <guid>https://dev.to/autogencrm/getting-started-with-the-gohighlevel-api-auth-webhooks-and-common-gotchas-5fo3</guid>
      <description>&lt;p&gt;If a client runs on GoHighLevel (HighLevel, also sold as LeadConnector), someone will eventually ask you to "just sync it" with another system. The API makes that possible, but a few details catch almost everyone the first time. Here's what to know about auth, a first request, webhooks and the common gotchas.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pick the right auth method first
&lt;/h2&gt;

&lt;p&gt;HighLevel's current API (V2 and the newer &lt;code&gt;v3&lt;/code&gt; version) supports two ways to authenticate. The old V1 API keys reached end of support on December 31, 2025, so don't start anything new with them.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Private Integration Token (PIT).&lt;/strong&gt; Best for your own scripts and internal tools that touch one agency or one sub-account. Create it under &lt;em&gt;Settings &amp;gt; Private Integrations&lt;/em&gt;, choose only the scopes you need, and copy it once. If the menu is missing, HighLevel's docs say to check that the feature is enabled in Labs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;OAuth 2.0.&lt;/strong&gt; Required when you're building a Marketplace app that many agencies or sub-accounts will install. Users approve scopes at install time, and you exchange the returned code for tokens.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Plan level matters too. Starter and Unlimited include basic, location-level access, while agency-level tokens and advanced OAuth features are tied to Agency Pro. Check this before designing anything agency-wide, such as creating sub-accounts.&lt;/p&gt;

&lt;h2&gt;
  
  
  Your first request
&lt;/h2&gt;

&lt;p&gt;Calls go to &lt;code&gt;https://services.leadconnectorhq.com&lt;/code&gt;, send JSON, and need two key headers: a bearer token and a &lt;code&gt;Version&lt;/code&gt;. Here's a contact upsert with placeholders:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST &lt;span class="s2"&gt;"https://services.leadconnectorhq.com/contacts/upsert"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer &amp;lt;YOUR_PRIVATE_INTEGRATION_TOKEN&amp;gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Version: &amp;lt;API_VERSION_FROM_DOCS&amp;gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{
    "locationId": "&amp;lt;YOUR_LOCATION_ID&amp;gt;",
    "firstName": "Jane",
    "email": "jane@example.com"
  }'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Upsert follows the sub-account's duplicate contact setting, matching on email or phone to decide whether to create or update. Test it against that setting before trusting it with real leads.&lt;/p&gt;

&lt;h2&gt;
  
  
  Gotcha 1: the Version header is not optional
&lt;/h2&gt;

&lt;p&gt;HighLevel versions the API per request through the &lt;code&gt;Version&lt;/code&gt; header. The docs list date-based values such as &lt;code&gt;2021-07-28&lt;/code&gt; and &lt;code&gt;2023-02-21&lt;/code&gt;, plus the named &lt;code&gt;v3&lt;/code&gt; released on June 11, 2026. Each version has its own docs pages, so it's easy to read one reference while sending another. Pick a version in the docs switcher, keep it in config, and send it on every call.&lt;/p&gt;

&lt;h2&gt;
  
  
  Gotcha 2: OAuth tokens expire, and refresh tokens are used up
&lt;/h2&gt;

&lt;p&gt;Access tokens last about 24 hours. The refresh token is valid for a year &lt;em&gt;or until it's used&lt;/em&gt;, so every refresh returns a new refresh token you must save. Reuse the old one and the next refresh fails. Refresh on the server, store refresh tokens encrypted, and stop two workers from refreshing the same install at once.&lt;/p&gt;

&lt;p&gt;Tokens come in two levels, Company (agency) and Location (sub-account). An agency token can request a location token through &lt;code&gt;/oauth/locationToken&lt;/code&gt;, handy when one app manages many clients.&lt;/p&gt;

&lt;p&gt;PITs don't expire, but HighLevel recommends rotating them every 90 days. During rotation the old and new tokens can both work for 7 days, so you can swap them without downtime.&lt;/p&gt;

&lt;h2&gt;
  
  
  Gotcha 3: verify webhooks against the raw body
&lt;/h2&gt;

&lt;p&gt;Marketplace apps can subscribe to webhook events such as &lt;code&gt;ContactCreate&lt;/code&gt; and &lt;code&gt;AppointmentCreate&lt;/code&gt;. HighLevel signs each payload in the &lt;code&gt;X-GHL-Signature&lt;/code&gt; header using Ed25519. The older RSA-based &lt;code&gt;X-WH-Signature&lt;/code&gt; header was scheduled for deprecation on September 1, 2026, so verifiers built only for that header need updating.&lt;/p&gt;

&lt;p&gt;The classic mistake is verifying parsed JSON. The signature covers the exact bytes sent, so use the raw body:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;express&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;express&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;crypto&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;node:crypto&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;express&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;GHL_PUBLIC_KEY&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;GHL_ED25519_PUBLIC_KEY&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// PEM from HighLevel's webhook guide&lt;/span&gt;

&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/webhooks/ghl&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;express&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;application/json&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;signature&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;x-ghl-signature&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;signature&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sendStatus&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;401&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;valid&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;crypto&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;verify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;                              &lt;span class="c1"&gt;// Ed25519 takes no separate digest&lt;/span&gt;
    &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;                          &lt;span class="c1"&gt;// raw Buffer, not parsed JSON&lt;/span&gt;
    &lt;span class="nx"&gt;GHL_PUBLIC_KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;Buffer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;signature&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;base64&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;valid&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sendStatus&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;401&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;utf8&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
  &lt;span class="c1"&gt;// hand the event to a queue, then acknowledge quickly&lt;/span&gt;
  &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sendStatus&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Don't confuse these with workflow webhooks. The &lt;em&gt;Inbound Webhook&lt;/em&gt; trigger and &lt;em&gt;Custom Webhook&lt;/em&gt; action live inside HighLevel workflows and are premium, per-execution features: useful no-code glue, but a separate system.&lt;/p&gt;

&lt;h2&gt;
  
  
  Gotcha 4: rate limits are per app, per resource
&lt;/h2&gt;

&lt;p&gt;For the public V2 APIs using OAuth, HighLevel lists a burst limit of 100 requests per 10 seconds and a daily limit of 200,000 requests. Both are counted per Marketplace app for each Location or Company, so every install gets its own budget. Responses include headers such as &lt;code&gt;X-RateLimit-Remaining&lt;/code&gt; and &lt;code&gt;X-RateLimit-Daily-Remaining&lt;/code&gt;. Back off before you hit a 429, especially during bulk imports.&lt;/p&gt;

&lt;h2&gt;
  
  
  Smaller things worth knowing
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;The calendar free-slots endpoint takes start and end dates as millisecond timestamps and can't span more than 31 days in one call.&lt;/li&gt;
&lt;li&gt;HighLevel support doesn't debug API code, so lean on the official docs (&lt;code&gt;marketplace.gohighlevel.com/docs&lt;/code&gt;) and the developer community.&lt;/li&gt;
&lt;li&gt;Build against a test sub-account, keep tokens out of front-end code and Git, and strip personal data and tokens from logs.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Wrapping up
&lt;/h2&gt;

&lt;p&gt;Pick the right token, pin your &lt;code&gt;Version&lt;/code&gt; header, save every new refresh token, verify the raw webhook body and watch the rate limit headers, and most first-week bugs never happen. For plan-by-plan access details, an endpoint table and a comparison of the API with Zapier, workflow webhooks and MCP, see &lt;a href="https://autogencrm.com/gohighlevel-api/" rel="noopener noreferrer"&gt;our longer GoHighLevel API guide&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Written by the team at &lt;a href="https://autogencrm.com/" rel="noopener noreferrer"&gt;AutogenCRM&lt;/a&gt;, a GoHighLevel setup and automation agency.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>automation</category>
      <category>api</category>
      <category>crm</category>
      <category>webdev</category>
    </item>
  </channel>
</rss>
