Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
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>
11 KiB
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):
- User sends SMS from GHL (Conversations, Workflows, Bulk Actions, Mobile App)
- GHL sends
ProviderOutboundMessagewebhook to the bridge's delivery URL - Bridge verifies
x-wh-signatureusing the webhook public key - Bridge extracts
phone,message,messageId,attachmentsfrom the payload - Bridge normalizes the phone number from E.164 to Philippine local format
- Bridge calls
POST https://api.cast.ph/api/sms/sendwith the message - Bridge calls GHL
Update Message StatusAPI to reportdeliveredorfailed
Inbound (Recipient → GHL) — Phase 2:
- Recipient replies via SMS
- Cast SIM Gateway receives the MO message
- Gateway POSTs to the bridge's inbound webhook endpoint
- Bridge calls GHL
Add Inbound MessageAPI (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 withnet/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
conversationProviderIdis 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
- Install "Cast SMS" from the GHL Marketplace
- Authorize the app (OAuth flow)
- Go to Settings → Phone Numbers → Advanced Settings → SMS Provider
- Select "Cast SMS" as the default provider
- 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 |
Reference Links
| 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) |