# 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`) ```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 ```go 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 ```go 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 ```go 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