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

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

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
  1. Build URL: {baseURL}/conversations/messages/{messageID}/status
  2. Body: { "status": status }
  3. Headers:
    • Authorization: Bearer {accessToken}
    • Content-Type: application/json
    • Version: 2021-04-15 (GHL API version header)
  4. PUT request
  5. On non-2xx: return error with status code + body
  6. On success: return nil

PostInboundMessage (Phase 2 — stub for now)

func (c *APIClient) PostInboundMessage(ctx context.Context, accessToken string, msg *InboundMessage) (*InboundMessageResponse, error)
  1. Build URL: {baseURL}/conversations/messages/inbound
  2. Body: JSON of InboundMessage
  3. Headers: same as above
  4. POST request
  5. 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/ succeeds
  • UpdateMessageStatus sends PUT to correct URL
  • Authorization: Bearer header set
  • Version: 2021-04-15 header set
  • Content-Type: application/json header set
  • Non-2xx returns descriptive error
  • PostInboundMessage stub implemented with correct URL and method
  • HTTP client has 30s timeout
  • Context passed to all HTTP requests