# 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 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:** ```json { "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 ```json { "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 ```go 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 Content-Type: application/json Version: 2021-04-15 ``` **Request Body:** ```json { "status": "delivered" } ``` | Status Value | Meaning | |-------------|---------| | `delivered` | Message delivered to recipient | | `failed` | Message failed to send | | `pending` | Message accepted, delivery in progress | **Response — 200 OK:** ```json { "success": true } ``` ### Add Inbound Message **`POST /conversations/messages/inbound`** Post an inbound (received) message into a GHL conversation. **Headers:** ``` Authorization: Bearer Content-Type: application/json Version: 2021-04-15 ``` **Request Body (SMS — default provider):** ```json { "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:** ```json { "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://hl.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: ```json { "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 |