0.0
The project is in a healthy, maintained state
A Ruby SDK for the HighLevel (GoHighLevel) API, generated from the official OpenAPI spec with runtime behavior validated against the official TypeScript SDK.
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
2025
2026
 Dependencies

Runtime

 Project Readme

gohighlevel

A Ruby SDK for the HighLevel (GoHighLevel) API.

  • Resource methods generated from the official OpenAPI spec — 40 apps, 700+ endpoints.
  • Runtime behaviour (auth resolution, OAuth refresh, webhook verification) hand-written and tested against the canonical docs.
  • Faraday 2 transport, Zeitwerk loader, immutable per-client configuration — no global mutable state.
  • Ruby 3.3+.

Installation

# Gemfile
gem "gohighlevel"
bundle add gohighlevel

The storage-backend gems (redis, activerecord, mongo) are not runtime dependencies — add whichever you need to your own Gemfile. The default in-memory store needs nothing.

Quickstart

require "gohighlevel"

client = HighLevel::Client.new(private_integration_token: "pit-xxxxxxxx")

# Every app is a resource on the client.
contacts = client.contacts.search_contacts_advanced(
  body: { locationId: "loc-123", pageLimit: 20 }
)

tasks = client.contacts.get_all_tasks(contact_id: "contact-456")

Resource method names are the spec's operationId in snake_case. Path parameters are required keyword arguments; query parameters are optional keyword arguments; a request body (when the endpoint declares one) is the body: keyword argument.

Authentication

HighLevel::Client.new accepts one of four credential modes. Configuration is immutable and per-instance.

# 1. Private Integration Token — simplest, always wins when present.
HighLevel::Client.new(private_integration_token: "pit-xxxx")

# 2. Agency access token.
HighLevel::Client.new(agency_access_token: "agency-xxxx")

# 3. Location access token.
HighLevel::Client.new(location_access_token: "loc-xxxx")

# 4. OAuth app credentials — required for the OAuth flows + 401 refresh.
HighLevel::Client.new(client_id: "xxxx", client_secret: "xxxx")

You can also pass a prebuilt HighLevel::Configuration:

config = HighLevel::Configuration.new(
  private_integration_token: "pit-xxxx",
  api_version: "2021-07-28",
  session_storage: HighLevel::Storage::Memory.new,
  instrumenter: ActiveSupport::Notifications
)
HighLevel::Client.new(config)

For OAuth-secured endpoints, the SDK resolves the right token per request from the operation's declared security requirements (Agency-Access, Location-Access, bearer, ...), falling back to session storage when no direct token is configured.

OAuth flows

client = HighLevel::Client.new(
  client_id: "xxxx",
  client_secret: "xxxx",
  redirect_uri: "https://your.app/oauth/callback"
)

# 1. Send the user here to authorize.
url = client.oauth.authorization_url(scope: "contacts.readonly contacts.write")

# 2. Exchange the code that comes back on your callback.
tokens = client.oauth.exchange_code(code: params[:code], user_type: "Location")

# 3. Refresh later.
tokens = client.oauth.refresh_token(refresh_token: stored_refresh, user_type: "Location")

# 4. Derive a location token from an agency token.
tokens = client.oauth.get_location_access_token(company_id: "co-1", location_id: "loc-1")

When a request returns 401, the SDK transparently attempts a refresh (using the session in storage), retries once, and — for location tokens — falls back to re-deriving from the company token. A second 401 is propagated, not looped.

Storage backends

Session storage holds OAuth tokens keyed by resource (company/location) id. HighLevel::Storage::Memory is the default and needs no configuration.

Backend When to use Extra gem
Storage::Memory Tests, single-process apps — (default)
Storage::ActiveRecord Rails apps with an existing database activerecord
Storage::Redis Multi-process workers sharing tokens redis
Storage::Mongo Parity with TS SDK consumers sharing a Mongo store mongo
# Redis
store = HighLevel::Storage::Redis.new(url: "redis://localhost:6379/0")
HighLevel::Client.new(client_id: "x", client_secret: "y", session_storage: store)

# ActiveRecord — once, in a migration:
HighLevel::Storage::ActiveRecord::Migration.create_table!(connection)
store = HighLevel::Storage::ActiveRecord.new

All backends satisfy the same contract (test/support/session_storage_contract.rb). Writing your own: subclass HighLevel::Storage::Base and implement the seven methods.

Webhook verification

HighLevel signs webhooks with its private key and publishes the public key — verification is asymmetric, not HMAC. Two schemes are supported: :rsa (signature on the x-wh-signature header) and :ed25519 (x-ghl-signature).

# Sinatra
post "/webhooks/highlevel" do
  body = request.body.read
  HighLevel::Webhooks.verify(
    payload: body,
    signature: request.env["HTTP_X_WH_SIGNATURE"],
    public_key: ENV.fetch("HIGHLEVEL_WEBHOOK_PUBLIC_KEY"),
    scheme: :rsa
  )
  # raises HighLevel::Webhooks::InvalidSignatureError if it doesn't verify
  process(JSON.parse(body))
  status 200
rescue HighLevel::Webhooks::InvalidSignatureError
  halt 401
end
# Rails controller
def highlevel
  HighLevel::Webhooks.verify(
    payload: request.raw_post,
    signature: request.headers["X-Wh-Signature"],
    public_key: Rails.application.credentials.highlevel_webhook_public_key
  )
  ProcessWebhookJob.perform_later(request.raw_post)
  head :ok
rescue HighLevel::Webhooks::InvalidSignatureError
  head :unauthorized
end

Pass the raw request body bytes — re-serializing a parsed JSON object changes the canonical form and breaks verification.

Pagination

The HighLevel API has no uniform pagination convention, so nothing is auto-paginated. HighLevel::Pagination is opt-in.

# A proc that routes pagination params wherever the endpoint wants them.
fetch = ->(**page) do
  client.contacts.search_contacts_advanced(body: { locationId: "loc-1" }.merge(page))
end

# Each page (raw response):
HighLevel::Pagination.each_page(fetch, cursor_field: :skip, items_field: "contacts") do |page|
  puts page["contacts"].size
end

# Each item, flattened across pages:
HighLevel::Pagination.each_item(fetch, cursor_field: :skip, items_field: "contacts") do |contact|
  puts contact["id"]
end

# Without a block, you get an Enumerator:
enum = HighLevel::Pagination.each_item(fetch, cursor_field: :skip, items_field: "contacts")
enum.lazy.select { |c| c["type"] == "lead" }.first(10)

cursor_field is the parameter the endpoint advances on (:offset, :skip, ...). limit_field defaults to :limit; page_size defaults to 100.

Error handling

Every non-2xx response raises a typed exception carrying #status, #response_body, and #request_id.

begin
  client.contacts.get_all_tasks(contact_id: "missing")
rescue HighLevel::NotFoundError => e
  e.status        # => 404
  e.response_body # => parsed JSON body
  e.request_id    # => the x-request-id header, if present
end
Exception Trigger
HighLevel::BadRequestError 400
HighLevel::UnauthorizedError 401
HighLevel::ForbiddenError 403
HighLevel::NotFoundError 404
HighLevel::UnprocessableEntityError 422
HighLevel::RateLimitError 429
HighLevel::ServerError 5xx
HighLevel::Error other non-2xx
HighLevel::ConfigurationError bad/missing credentials
HighLevel::NetworkError transport-level failure

All inherit from HighLevel::Error, so rescue HighLevel::Error catches everything.

Instrumentation

The SDK emits no logs. Pass any object responding to #instrument(name, payload, &block)ActiveSupport::Notifications is the obvious one — as instrumenter: and subscribe to the request.high_level event.

client = HighLevel::Client.new(
  private_integration_token: "pit-xxxx",
  instrumenter: ActiveSupport::Notifications
)

ActiveSupport::Notifications.subscribe("request.high_level") do |*, payload|
  Rails.logger.info("HighLevel #{payload[:method].upcase} #{payload[:url]}#{payload[:status]}")
end

With no instrumenter configured, the instrumentation middleware is a transparent pass-through.

Differences from the official TypeScript SDK

  • Naming — methods are snake_case; arguments are keyword arguments.
  • No global state — every collaborator is injected through HighLevel::Configuration; there is no global mutable config. Thread-safe by construction.
  • Webhook verification raisesHighLevel::Webhooks.verify returns true or raises InvalidSignatureError, rather than returning a boolean.
  • Pagination is explicitHighLevel::Pagination is opt-in; resource methods never auto-paginate.
  • Storage gems are optionalredis/activerecord/mongo are lazy-required, not runtime dependencies.
  • Generated code is committedbundle add gohighlevel gives you a working library; there is no install-time codegen.

Contributing

See CONTRIBUTING.md for the spec-sync, regeneration, and drift-check workflows.

License

MIT — see LICENSE.txt.