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>
322 lines
11 KiB
Markdown
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://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)
|
|
|
|
```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:** `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 |
|
|
|
|
---
|
|
|
|
## 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) |
|