Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 29 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,16 +43,18 @@ async def oauth_example():
)
print(f"Visit: {auth_url}")

# Step 2: Exchange code for tokens (after user authorization)
# Step 2: Exchange code for tokens (after user authorization).
# The request body uses camelCase keys.
token_data = await client.oauth.get_access_token({
"client_id": "your_client_id",
"client_secret": "your_client_secret",
"grant_type": "authorization_code",
"clientId": "your_client_id",
"clientSecret": "your_client_secret",
"grantType": "authorization_code",
"code": "authorization_code_from_callback",
"redirect_uri": "https://your-app.com/callback"
})

# Tokens are automatically stored in session storage

# token_data is the raw token response, with camelCase keys:
# accessToken, refreshToken, expiresIn, userType, and locationId or companyId.
# Persist it via your session storage to authenticate later requests.
print("OAuth flow completed successfully!")

asyncio.run(oauth_example())
Expand Down Expand Up @@ -115,6 +117,26 @@ async def handle_ghl_webhook():
return jsonify({"status": "success"}), 200
```

### Webhook Signature Verification

Incoming webhooks are verified before they are processed. Configure the public key
(available in your app settings) as an environment variable. If no supported signature
header and matching key are present, the middleware logs a warning and skips the
webhook **without processing it** — so configuring a key is required for webhooks to work.

| Scheme | Header | Environment variable | Notes |
|--------|--------|----------------------|-------|
| Ed25519 | `x-ghl-signature` | `WEBHOOK_SIGNATURE_PUBLIC_KEY` | Preferred. Used when present. |
| RSA-SHA256 | `x-wh-signature` | `WEBHOOK_PUBLIC_KEY` | Legacy fallback. |

```bash
# CLIENT_ID is also required — the webhook's appId is matched against it before processing.
export CLIENT_ID="your_client_id"
export WEBHOOK_SIGNATURE_PUBLIC_KEY="your_ed25519_public_key"
# Optional legacy fallback:
export WEBHOOK_PUBLIC_KEY="your_rsa_public_key"
```

## Documentation

- [Official API Documentation](http://marketplace.gohighlevel.com/docs/)
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"

[project]
name = "gohighlevel-api-client"
version = "1.0.0-beta.1"
version = "3.0.0"
description = "GoHighLevel Python SDK - Official API client for GoHighLevel platform"
readme = "README.md"
license = {text = "MIT"}
Expand Down
3 changes: 3 additions & 0 deletions src/highlevel/error.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
# @generated
# File generated from our OpenAPI spec

from typing import Any, Optional


Expand Down
48 changes: 34 additions & 14 deletions src/highlevel/highlevel.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,26 @@
# @generated
# File generated from our OpenAPI spec

from typing import Any, Dict, Optional, Awaitable
import asyncio
import inspect
import copy
import httpx
import time
import json
from .services.ad_publishing.ad_publishing import AdPublishing
from .services.affiliate_manager.affiliate_manager import AffiliateManager
from .services.agent_studio.agent_studio import AgentStudio
from .services.associations.associations import Associations
from .services.blogs.blogs import Blogs
from .services.brand_boards.brand_boards import BrandBoards
from .services.businesses.businesses import Businesses
from .services.calendars.calendars import Calendars
from .services.campaigns.campaigns import Campaigns
from .services.chat_widget.chat_widget import ChatWidget
from .services.companies.companies import Companies
from .services.contacts.contacts import Contacts
from .services.conversation_ai.conversation_ai import ConversationAi
from .services.conversations.conversations import Conversations
from .services.courses.courses import Courses
from .services.custom_fields.custom_fields import CustomFields
Expand All @@ -21,6 +30,7 @@
from .services.forms.forms import Forms
from .services.funnels.funnels import Funnels
from .services.invoices.invoices import Invoices
from .services.knowledge_base.knowledge_base import KnowledgeBase
from .services.links.links import Links
from .services.locations.locations import Locations
from .services.marketplace.marketplace import Marketplace
Expand All @@ -32,9 +42,9 @@
from .services.phone_system.phone_system import PhoneSystem
from .services.products.products import Products
from .services.proposals.proposals import Proposals
from .services.saas_api.saas_api import SaasApi
from .services.saas.saas import Saas
from .services.snapshots.snapshots import Snapshots
from .services.social_media_posting.social_media_posting import SocialMediaPosting
from .services.social_planner.social_planner import SocialPlanner
from .services.store.store import Store
from .services.surveys.surveys import Surveys
from .services.users.users import Users
Expand All @@ -45,12 +55,20 @@
from .logging import Logger
from .webhook import WebhookManager
from .constants import UserType
from importlib.metadata import version as _pkg_version, PackageNotFoundError

# SDK version, read from the installed package metadata and sent in each request
try:
SDK_VERSION = _pkg_version("gohighlevel-api-client")
except PackageNotFoundError:
SDK_VERSION = "unknown"


class HighLevel:
"""HighLevel SDK Client"""

BASE_URL = "https://services.leadconnectorhq.com"
API_VERSION = "v3"

def __init__(
self,
Expand All @@ -60,8 +78,7 @@ def __init__(
agency_access_token: Optional[str] = None,
location_access_token: Optional[str] = None,
session_storage: Optional[SessionStorage] = None,
log_level: Optional[str] = None,
api_version: Optional[str] = None
log_level: Optional[str] = None
):
# Validate configuration
if not private_integration_token and (not client_id or not client_secret):
Expand All @@ -74,7 +91,6 @@ def __init__(

# Set default configuration
self.config = {
"api_version": api_version or "2021-07-28",
"private_integration_token": private_integration_token,
"agency_access_token": agency_access_token,
"location_access_token": location_access_token,
Expand Down Expand Up @@ -114,7 +130,8 @@ def _get_default_headers(self) -> Dict[str, str]:
"""Generate default headers for HTTP requests"""
headers = {
"Content-Type": "application/json",
"Version": self.config["api_version"]
"Version": self.API_VERSION,
"GHL-SDK-Version": f"python/{SDK_VERSION}",
}

# Priority 1: private_integration_token
Expand Down Expand Up @@ -214,7 +231,7 @@ async def _refresh_token_if_needed(self, resource_id: str, session_data: ISessio
})

self.logger.info(f"Token refreshed successfully for {resource_id}")
return f"Bearer {new_token_data['access_token']}"
return f"Bearer {new_token_data.get('access_token') or new_token_data.get('accessToken')}"

except Exception as error:
self.logger.error(f"Failed to refresh token for {resource_id}: {error}")
Expand Down Expand Up @@ -276,7 +293,7 @@ async def _handle_location_token_fallback(self, location_id: str, location_sessi
})

self.logger.info(f"Location token fetched successfully using company token fallback for location_id: {location_id}")
return f"Bearer {new_location_token_data['access_token']}"
return f"Bearer {new_location_token_data.get('access_token') or new_location_token_data.get('accessToken')}"

except Exception as error:
self.logger.error(f"Failed to handle location token fallback for location_id: {location_id}: {error}")
Expand Down Expand Up @@ -388,13 +405,19 @@ def extract_resource_id(

def _initialize_services(self) -> None:
"""Initialize all service instances with the HighLevel instance"""
self.ad_publishing = AdPublishing(self)
self.affiliate_manager = AffiliateManager(self)
self.agent_studio = AgentStudio(self)
self.associations = Associations(self)
self.blogs = Blogs(self)
self.brand_boards = BrandBoards(self)
self.businesses = Businesses(self)
self.calendars = Calendars(self)
self.campaigns = Campaigns(self)
self.chat_widget = ChatWidget(self)
self.companies = Companies(self)
self.contacts = Contacts(self)
self.conversation_ai = ConversationAi(self)
self.conversations = Conversations(self)
self.courses = Courses(self)
self.custom_fields = CustomFields(self)
Expand All @@ -404,6 +427,7 @@ def _initialize_services(self) -> None:
self.forms = Forms(self)
self.funnels = Funnels(self)
self.invoices = Invoices(self)
self.knowledge_base = KnowledgeBase(self)
self.links = Links(self)
self.locations = Locations(self)
self.marketplace = Marketplace(self)
Expand All @@ -415,9 +439,9 @@ def _initialize_services(self) -> None:
self.phone_system = PhoneSystem(self)
self.products = Products(self)
self.proposals = Proposals(self)
self.saas_api = SaasApi(self)
self.saas = Saas(self)
self.snapshots = Snapshots(self)
self.social_media_posting = SocialMediaPosting(self)
self.social_planner = SocialPlanner(self)
self.store = Store(self)
self.surveys = Surveys(self)
self.users = Users(self)
Expand Down Expand Up @@ -551,10 +575,6 @@ def get_client_secret(self) -> Optional[str]:
"""Get current client secret"""
return self.config.get("client_secret")

def set_api_version(self, version: str) -> None:
"""Set API version"""
self.update_config({"api_version": version})

def get_config(self) -> Dict[str, Any]:
"""Get current configuration"""
return {**self.config}
Expand Down
5 changes: 5 additions & 0 deletions src/highlevel/services/ad_publishing/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
"""AdPublishing Service"""

from .ad_publishing import AdPublishing

__all__ = ['AdPublishing']
Loading