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>
3.3 KiB
3.3 KiB
Task 07: GHL API Client (Status Updates)
Objective
Build internal/ghl/api.go — client for calling GHL APIs (update message status, post inbound messages).
Reference
- Update Message Status: https://marketplace.gohighlevel.com/docs/ghl/conversations/update-message-status
- Add Inbound Message: https://marketplace.gohighlevel.com/docs/ghl/conversations/add-an-inbound-message
Types (add to internal/ghl/types.go)
type MessageStatusUpdate struct {
Status string `json:"status"` // "delivered", "failed", "pending"
ErrorCode string `json:"error_code,omitempty"`
ErrorMessage string `json:"error_message,omitempty"`
}
type InboundMessage struct {
Type string `json:"type"` // "SMS"
Message string `json:"message"`
Phone string `json:"phone"` // E.164
ConversationProviderID string `json:"conversationProviderId,omitempty"`
}
type InboundMessageResponse struct {
ConversationID string `json:"conversationId"`
MessageID string `json:"messageId"`
}
APIClient struct
type APIClient struct {
baseURL string
httpClient *http.Client
}
func NewAPIClient() *APIClient {
return &APIClient{
baseURL: "https://services.leadconnectorhq.com",
httpClient: &http.Client{Timeout: 30 * time.Second},
}
}
Methods
UpdateMessageStatus
func (c *APIClient) UpdateMessageStatus(ctx context.Context, accessToken, messageID, status string) error
- Build URL:
{baseURL}/conversations/messages/{messageID}/status - Body:
{ "status": status } - Headers:
Authorization: Bearer {accessToken}Content-Type: application/jsonVersion: 2021-04-15(GHL API version header)
- PUT request
- On non-2xx: return error with status code + body
- On success: return nil
PostInboundMessage (Phase 2 — stub for now)
func (c *APIClient) PostInboundMessage(ctx context.Context, accessToken string, msg *InboundMessage) (*InboundMessageResponse, error)
- Build URL:
{baseURL}/conversations/messages/inbound - Body: JSON of InboundMessage
- Headers: same as above
- POST request
- Parse and return response
Note: Implement as a working stub — the full inbound flow is Phase 2, but the API client method should be ready.
Key behaviors
- GHL API version header:
Version: 2021-04-15— required on all GHL API calls - Bearer auth: Use the OAuth access token for the specific locationId
- Status values:
"delivered","failed","pending"— GHL expects these exact strings - Error on status update is non-fatal — log it but don't cascade. The SMS was already sent (or failed to send). The status update is best-effort.
- Retry on 401 — if status update returns 401, the token may have expired mid-request. The caller (webhook handler) should refresh and retry once.
Acceptance Criteria
go build ./cmd/server/succeedsUpdateMessageStatussends PUT to correct URLAuthorization: Bearerheader setVersion: 2021-04-15header setContent-Type: application/jsonheader set- Non-2xx returns descriptive error
PostInboundMessagestub implemented with correct URL and method- HTTP client has 30s timeout
- Context passed to all HTTP requests