cast-ghl-plugin/cast-ghl-provider-plan.md
Head of Product & Engineering 877895f43e fix: correct domain from ghl.cast.ph to hl.cast.ph throughout
Update all active config and documentation files to use the correct
production domain hl.cast.ph (not ghl.cast.ph).

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-04-05 14:09:43 +02:00

322 lines
11 KiB
Markdown

# 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](https://github.com/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)
```json
{
"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`
```json
{
"to": "09171234567",
"message": "Hello from GHL",
"sender_id": "CAST"
}
```
Response:
```json
{
"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)
```env
# Server
PORT=3002
BASE_URL=https://hl.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)
```yaml
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:** `hl.cast.ph`
- **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 |
---
## 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) |