cast-ghl-plugin/.claude/tasks/03-cast-client.md
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

2.3 KiB

Task 03: Cast API Client

Objective

Build internal/cast/client.go — a typed HTTP client for the Cast.ph SMS API.

Reference

Read CAST_API_REFERENCE.md for exact request/response shapes.

Types (internal/cast/types.go)

type SendRequest struct {
    To       string `json:"to"`
    Message  string `json:"message"`
    SenderID string `json:"sender_id,omitempty"`
}

type SendResponse struct {
    Success   bool   `json:"success"`
    MessageID string `json:"message_id,omitempty"`
    Parts     int    `json:"parts,omitempty"`
    Error     string `json:"error,omitempty"`
}

Client (internal/cast/client.go)

Client struct

type Client struct {
    baseURL    string
    apiKey     string
    senderID   string // default sender ID, can be empty
    httpClient *http.Client
}

func NewClient(baseURL, apiKey, senderID string) *Client

Methods

func (c *Client) SendSMS(ctx context.Context, to, message string) (*SendResponse, error)
  • POST to /api/sms/send
  • Set headers: X-API-Key, Content-Type: application/json
  • Body: { "to": to, "message": message, "sender_id": c.senderID } (omit sender_id if empty)
  • On non-200: return CastAPIError with status code and error message from body
  • On 200 with success: false: return CastAPIError with the error field
  • On 200 with success: true: return the response
  • Retry on 429: max 3 retries, read Retry-After header, backoff 1s/2s/4s

Error type

type CastAPIError struct {
    StatusCode int
    APIError   string
}

func (e *CastAPIError) Error() string {
    return fmt.Sprintf("cast api error (HTTP %d): %s", e.StatusCode, e.APIError)
}

Key behaviors

  • HTTP client timeout: 30 seconds
  • sender_id omitted from JSON when Client.senderID is empty
  • Response body always read and closed, even on errors
  • Retry on 429 only (not on 5xx — Cast should handle that)
  • Log retries with slog.Warn

Acceptance Criteria

  • go build ./cmd/server/ succeeds
  • SendSMS calls correct URL with correct headers
  • X-API-Key header set on every request
  • sender_id omitted from JSON when empty
  • CastAPIError returned on non-200 with statusCode + apiError
  • CastAPIError returned on 200 with success: false
  • Retry on 429 with backoff (max 3)
  • Context passed to HTTP request
  • HTTP client has 30s timeout