cast-ghl-plugin/GHL_API_REFERENCE.md
Head of Product & Engineering a40a4aa626
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
feat: initial implementation of Cast GHL Provider
Complete MVP implementation of the Cast GHL Conversation Provider bridge:
- Go module setup with chi router and mongo-driver dependencies
- Config loading with env var validation and defaults
- MongoDB token store with upsert, get, update, delete operations
- Cast.ph SMS client with 429 retry logic and typed errors
- Phone number normalization (E.164 ↔ Philippine local format)
- GHL OAuth 2.0 install/callback/refresh flow
- GHL webhook handler with ECDSA signature verification (async dispatch)
- GHL API client for message status updates and inbound message stubs
- Multi-stage Dockerfile, docker-compose with MongoDB, Woodpecker CI pipeline
- Unit tests for phone normalization, Cast client, GHL webhook, and OAuth handlers

Co-Authored-By: SideKx <sidekx.ai@sds.dev>
2026-04-04 17:27:05 +02:00

7.4 KiB

GHL Conversation Provider — API Reference

GHL API Base URL: https://services.leadconnectorhq.com GHL Marketplace: https://marketplace.gohighlevel.com API Version Header: Version: 2021-04-15


Overview

This document covers the GHL APIs used by the Cast GHL Provider bridge service. The bridge acts as a custom SMS Conversation Provider, receiving outbound webhooks from GHL and posting inbound messages + status updates back.


Authentication

OAuth 2.0

All GHL API calls require a Bearer token obtained via the OAuth 2.0 flow.

Header:

Authorization: Bearer <access_token>
Version: 2021-04-15

Access tokens expire after ~24 hours. Use the refresh token to obtain a new one.


OAuth Endpoints

1. Authorization URL (user-facing redirect)

GET https://marketplace.gohighlevel.com/oauth/chooselocation?
  response_type=code&
  redirect_uri={REDIRECT_URI}&
  client_id={CLIENT_ID}&
  scope={SCOPES}
Param Description
response_type Always code
redirect_uri Your OAuth callback URL (must match app config)
client_id From GHL Marketplace app settings
scope Space-separated list of scopes

Required scopes:

conversations/message.write conversations/message.readonly conversations.write conversations.readonly contacts.readonly contacts.write

After authorization, GHL redirects to: {redirect_uri}?code={authorization_code}

2. Token Exchange

POST /oauth/token

Exchange an authorization code for access + refresh tokens.

Content-Type: application/x-www-form-urlencoded

client_id={CLIENT_ID}&
client_secret={CLIENT_SECRET}&
grant_type=authorization_code&
code={CODE}&
redirect_uri={REDIRECT_URI}

Response — 200 OK:

{
  "access_token": "eyJhbGc...",
  "refresh_token": "def50200...",
  "expires_in": 86400,
  "token_type": "Bearer",
  "locationId": "GKAWb4yu7A4LSc0skQ6g",
  "companyId": "GK12345...",
  "userType": "Location"
}

3. Token Refresh

POST /oauth/token

Content-Type: application/x-www-form-urlencoded

client_id={CLIENT_ID}&
client_secret={CLIENT_SECRET}&
grant_type=refresh_token&
refresh_token={REFRESH_TOKEN}

Response: Same shape as token exchange.


Webhook: ProviderOutboundMessage

GHL sends this webhook to your Delivery URL whenever a user sends an SMS from GHL (Conversations, Workflows, Bulk Actions, Mobile App).

Method: POST Signature header: x-wh-signature (ECDSA P-256 + SHA-256, base64-encoded ASN.1 DER)

Payload Schema

{
  "contactId": "GKBhT6BfwY9mjzXAU3sq",
  "locationId": "GKAWb4yu7A4LSc0skQ6g",
  "messageId": "GKJxs4P5L8dWc5CFUITM",
  "type": "SMS",
  "phone": "+15864603685",
  "message": "The text message to be sent to the contact",
  "attachments": ["https://example.com/image.png"],
  "userId": "GK56r6wdJDrkUPd0xsmx"
}
Field Type Description
contactId string GHL contact ID
locationId string GHL sub-account (location) ID
messageId string GHL message ID — use for status updates
type string "SMS" or "Email"
phone string Recipient phone in E.164 format
message string Message body to send
attachments array URLs of attached media (MMS)
userId string GHL user who sent the message

Webhook Signature Verification

  1. Read the raw request body
  2. Compute SHA-256 hash of the body bytes
  3. Base64-decode the x-wh-signature header value
  4. Verify the ECDSA ASN.1 DER signature using the public key from your GHL app settings
hash := sha256.Sum256(body)
sigBytes, _ := base64.StdEncoding.DecodeString(signatureHeader)
valid := ecdsa.VerifyASN1(publicKey, hash[:], sigBytes)

The public key is provided in PEM format in your GHL Marketplace app settings under the webhook configuration.


Conversation APIs

Update Message Status

PUT /conversations/messages/{messageId}/status

Update the delivery status of an outbound message. Only the conversation provider's own marketplace app token can update status.

Headers:

Authorization: Bearer <access_token>
Content-Type: application/json
Version: 2021-04-15

Request Body:

{
  "status": "delivered"
}
Status Value Meaning
delivered Message delivered to recipient
failed Message failed to send
pending Message accepted, delivery in progress

Response — 200 OK:

{
  "success": true
}

Add Inbound Message

POST /conversations/messages/inbound

Post an inbound (received) message into a GHL conversation.

Headers:

Authorization: Bearer <access_token>
Content-Type: application/json
Version: 2021-04-15

Request Body (SMS — default provider):

{
  "type": "SMS",
  "message": "Reply from the recipient",
  "phone": "+639171234567"
}
Field Type Required Description
type string Yes "SMS" for default provider
message string Yes Inbound message body
phone string Yes Sender's phone in E.164 format
conversationProviderId string No Not required for default SMS provider

Response — 200 OK:

{
  "conversationId": "...",
  "messageId": "..."
}

Note: The phone number must match an existing contact in GHL. If no contact exists with that phone number, you may need to create one first via the Contacts API.


Conversation Provider Setup

Required Scopes

Scope Purpose
conversations/message.write Outbound webhook events, inbound messages, status updates
conversations/message.readonly Read message data, recordings, transcriptions
conversations.write Create/update/delete conversations
conversations.readonly Query conversations
contacts.readonly Look up contacts by phone
contacts.write Create contacts for unknown inbound numbers

Provider Configuration (in GHL Marketplace app)

  1. Type: SMS
  2. Name: Cast SMS
  3. Delivery URL: https://ghl.cast.ph/api/ghl/v1/webhook/messages
  4. Do NOT check "Is this a Custom Conversation Provider"

Enabling the Provider (per sub-account)

After OAuth install:

  1. Go to sub-account Settings → Phone Numbers → Advanced Settings
  2. Select "Cast SMS" as the SMS Provider
  3. Save

Supported GHL Features

Feature Supported
Conversations (web app) Yes
Conversations (mobile app) Yes
Workflows (SMS module) Yes
Bulk Actions Yes
Missed Call Text-Back No (falls back to Twilio)
Review Requests No (falls back to Twilio)
Internal SMS Notifications No (falls back to Twilio)
Conversational AI No (GHL limitation)

Rate Limits

GHL API rate limits vary by plan. The bridge should handle 429 responses gracefully:

  • Retry after the Retry-After header value
  • Max 3 retries per request
  • Log rate limit events for monitoring

Error Handling

GHL API errors return JSON with an error description:

{
  "statusCode": 401,
  "message": "Unauthorized"
}

Common errors:

Status Cause Action
401 Token expired or invalid Refresh token and retry
403 Insufficient scopes Check app scope configuration
404 Message or contact not found Log and skip
422 Invalid request body Log and fix request format
429 Rate limited Retry after Retry-After header