cast-ghl-plugin/cast-ghl-provider-plan.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

11 KiB

Cast GHL Provider — Project Plan

Overview

cast-ghl-provider is a GHL (GoHighLevel) Marketplace app that acts as a custom SMS Conversation Provider, replacing Twilio/LC-Phone with Cast.ph as the SMS backend. When a GHL user sends an SMS from Conversations, Workflows, or Bulk Actions, the message is routed through Cast.ph's API instead of Twilio.

Published to the GHL Marketplace as a free integration to drive Cast.ph SMS volume.

Reference repo: ampilares/selfhostsim — the ghl/ bridge service is used as an architectural reference (not a direct fork). We rewrite in Go.


Architecture

GHL Platform (Conversations / Workflows / Bulk Actions)
    ↓  ProviderOutboundMessage webhook (POST + x-wh-signature)
Cast GHL Bridge (Go, deployed on Vultr)
    ↓  HTTPS + X-API-Key
api.cast.ph (Cast SMS Backend)
    ↓  SMPP
Carrier → Recipient

Inbound (future):
Recipient → Carrier → Cast SIM Gateway → Cast GHL Bridge → GHL Add Inbound Message API

Message Flows

Outbound (GHL → Recipient):

  1. User sends SMS from GHL (Conversations, Workflows, Bulk Actions, Mobile App)
  2. GHL sends ProviderOutboundMessage webhook to the bridge's delivery URL
  3. Bridge verifies x-wh-signature using the webhook public key
  4. Bridge extracts phone, message, messageId, attachments from the payload
  5. Bridge normalizes the phone number from E.164 to Philippine local format
  6. Bridge calls POST https://api.cast.ph/api/sms/send with the message
  7. Bridge calls GHL Update Message Status API to report delivered or failed

Inbound (Recipient → GHL) — Phase 2:

  1. Recipient replies via SMS
  2. Cast SIM Gateway receives the MO message
  3. Gateway POSTs to the bridge's inbound webhook endpoint
  4. Bridge calls GHL Add Inbound Message API (type: "SMS") to insert into conversation

Language & Runtime

Go

  • Matches the entire Cast.ph backend stack (Go, Docker, Vultr)
  • Single binary deployment — no node_modules, no runtime deps
  • Lower memory footprint per instance — important for a public marketplace app handling webhooks for many GHL locations
  • The selfhostsim ghl/ service logic is ~300 lines of Express routes — straightforward to implement in Go with net/http + a MongoDB driver
  • Long-term maintainability aligns with team expertise

GHL Conversation Provider Contract

Provider Type: Default SMS (replaces Twilio/LC-Phone)

  • Do NOT check "Is this a Custom Conversation Provider"
  • Supports: Conversations, Workflows, Bulk Actions, Mobile App
  • conversationProviderId is NOT required for inbound messages
  • Standard SMS workflow modules are supported

Required Scopes

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

Outbound Webhook Payload (ProviderOutboundMessage)

{
  "contactId": "GKBhT6BfwY9mjzXAU3sq",
  "locationId": "GKAWb4yu7A4LSc0skQ6g",
  "messageId": "GKJxs4P5L8dWc5CFUITM",
  "type": "SMS",
  "phone": "+639171234567",
  "message": "The text message to send",
  "attachments": [],
  "userId": "GK56r6wdJDrkUPd0xsmx"
}

GHL API Endpoints Used

API Method URL
Update Message Status PUT https://services.leadconnectorhq.com/conversations/messages/{messageId}/status
Add Inbound Message POST https://services.leadconnectorhq.com/conversations/messages/inbound
Get Access Token POST https://services.leadconnectorhq.com/oauth/token

Cast.ph API Integration

Outbound SMS Endpoint

POST https://api.cast.ph/api/sms/send

{
  "to": "09171234567",
  "message": "Hello from GHL",
  "sender_id": "CAST"
}

Response:

{
  "success": true,
  "message_id": "abc123def456",
  "parts": 1
}

Key API Behaviors

  • Phone numbers: 11-digit Philippine format (09XXXXXXXXX) — bridge must normalize from E.164
  • Message limit: 450 characters max (3 SMS parts)
  • Auth: X-API-Key: cast_<64-hex-chars> header
  • Rate limit: 30 req/s, burst 50
  • Errors: { "success": false, "error": "..." }

Phone Number Normalization

GHL sends E.164 format. Cast API expects Philippine local format.

Direction Input Output
GHL → Cast +639171234567 09171234567
GHL → Cast 639171234567 09171234567
Cast → GHL 09171234567 +639171234567

Project Structure

cast-ghl-provider/
├── cmd/
│   └── server/
│       └── main.go              # Entry point: HTTP server, config, graceful shutdown
├── internal/
│   ├── config/
│   │   └── config.go            # Env var loading + validation
│   ├── ghl/
│   │   ├── oauth.go             # OAuth install flow, token exchange, refresh
│   │   ├── webhook.go           # Outbound webhook handler + signature verification
│   │   ├── api.go               # GHL API client (status update, inbound message)
│   │   └── types.go             # GHL request/response types
│   ├── cast/
│   │   ├── client.go            # Cast API HTTP client
│   │   └── types.go             # Cast request/response types
│   ├── phone/
│   │   └── normalize.go         # E.164 ↔ PH local format conversion
│   └── store/
│       └── mongo.go             # MongoDB token/session storage
├── Dockerfile
├── docker-compose.yaml
├── .env.example
├── go.mod
├── go.sum
├── CAST_API_REFERENCE.md        # Cast API docs (source of truth)
├── GHL_API_REFERENCE.md         # GHL conversation provider docs
├── CLAUDE.md                    # Claude Code project instructions
├── .claude/tasks/               # Sequential dev tasks
│   ├── 01-init.md
│   ├── 02-config-and-store.md
│   ├── 03-cast-client.md
│   ├── 04-phone-normalize.md
│   ├── 05-ghl-oauth.md
│   ├── 06-ghl-webhook.md
│   ├── 07-ghl-api.md
│   ├── 08-server-wiring.md
│   ├── 09-docker.md
│   └── 10-testing.md
├── .woodpecker.yml
├── .gitignore
└── README.md

Configuration (env vars)

# Server
PORT=3002
BASE_URL=https://ghl.cast.ph        # Public URL for OAuth redirects + webhooks

# GHL OAuth
GHL_CLIENT_ID=xxx
GHL_CLIENT_SECRET=xxx
GHL_WEBHOOK_PUBLIC_KEY=xxx           # For verifying x-wh-signature
GHL_CONVERSATION_PROVIDER_ID=xxx     # From GHL Marketplace app

# Cast.ph
CAST_API_KEY=cast_xxx
CAST_API_URL=https://api.cast.ph     # Optional override
CAST_SENDER_ID=CAST                  # Default sender ID

# MongoDB
MONGO_URI=mongodb://localhost:27017/cast-ghl

# Security
INBOUND_API_KEY=xxx                  # Shared secret for Cast gateway → bridge auth

Deployment

Docker Compose (production)

services:
  bridge:
    build: .
    ports:
      - "3002:3002"
    env_file: .env
    depends_on:
      - mongo
    restart: unless-stopped

  mongo:
    image: mongo:7
    volumes:
      - mongo-data:/data/db
    restart: unless-stopped

volumes:
  mongo-data:

Infrastructure

  • Host: Vultr (existing Cast infrastructure)
  • Reverse proxy: Nginx or Caddy with HTTPS
  • Domain: ghl.cast.ph (or similar)
  • CI/CD: Woodpecker CI at git.sds.dev

GHL Marketplace Listing

App Details

  • Name: Cast SMS
  • Type: Public (after development/testing as Private)
  • Category: SMS / Communication
  • Pricing: Free
  • Description: Send and receive SMS through Cast.ph's Philippine SMS gateway. Lower cost alternative to Twilio/LC-Phone for Philippine numbers.

What the user does

  1. Install "Cast SMS" from the GHL Marketplace
  2. Authorize the app (OAuth flow)
  3. Go to Settings → Phone Numbers → Advanced Settings → SMS Provider
  4. Select "Cast SMS" as the default provider
  5. Send SMS from Conversations — messages route through Cast.ph

Risks & Mitigations

Risk Impact Mitigation
GHL custom SMS providers lack parity (missed call text-back, review requests, internal notifications still route to Twilio) Some GHL features won't use Cast SMS Document limitations; track GHL feature requests
GHL Conversational AI doesn't work with custom SMS providers AI auto-replies won't use Cast SMS GHL platform limitation — no workaround
OAuth token refresh failures Messages stop flowing Robust refresh with retry + alerting
Phone number format mismatches Messages fail or go to wrong numbers Comprehensive normalizer with unit tests
Cast API downtime Outbound messages fail Report failed status to GHL; health checks
GHL Marketplace review rejection Can't go public Start as Private, iterate on feedback

Timeline

Phase Task File Duration Dependencies
Init & config 01, 02 1-2 days GHL Marketplace app created
Cast client + phone normalization 03, 04 1-2 days Cast API docs
GHL OAuth flow 05 2-3 days GHL credentials
Webhook handler + outbound 06 2-3 days OAuth working
GHL API client (status updates) 07 1-2 days Webhook handler
Server wiring 08 1 day All components
Docker + deployment 09 1-2 days Vultr access
Testing 10 2-3 days Everything
Total MVP ~2-3 weeks
Inbound SMS (Phase 2) Future 1 week Cast SIM gateway webhook
GHL Marketplace submission Future 1-2 weeks Stable MVP

Resource URL
GHL Conversation Providers https://marketplace.gohighlevel.com/docs/marketplace-modules/ConversationProviders
ProviderOutboundMessage webhook https://marketplace.gohighlevel.com/docs/webhook/ProviderOutboundMessage
Add Inbound Message API https://marketplace.gohighlevel.com/docs/ghl/conversations/add-an-inbound-message
Update Message Status API https://marketplace.gohighlevel.com/docs/ghl/conversations/update-message-status
GHL OAuth docs https://marketplace.gohighlevel.com/docs/Authorization/authorization_doc
GHL Scopes https://marketplace.gohighlevel.com/docs/oauth/Scopes
selfhostsim (reference) https://github.com/ampilares/selfhostsim
GHL Marketplace app template https://github.com/GoHighLevel/ghl-marketplace-app-template
Cast API docs CAST_API_REFERENCE.md (in repo)