# Go Social Media API SDK

The official Go SDK. Standard library only, Go 1.21+, context-first API with typed
errors and generics for response envelopes.

## Install

```bash
go get github.com/OmniSocials/omnisocials-go
```

## Authentication

```go
client, err := omnisocials.NewClient(omnisocials.WithAPIKey("omsk_live_..."))
```

Reads `OMNISOCIALS_API_KEY` if no option is passed. A missing key returns
`*omnisocials.AuthenticationError`.

## Create a post

```go
res, err := client.Posts.Create(ctx, &omnisocials.PostCreateParams{
	Content:     "New drop this Friday",
	Channels:    []string{"instagram", "facebook", "linkedin"},
	ScheduledAt: "2026-08-01T09:00:00Z",
	MediaURLs:   []string{"https://example.com/teaser.jpg"},
})
fmt.Println(res.Data.ID, res.Data.Status)
```

`MediaURLs` / `MediaIDs` entries can also carry per-media alt text (max 1500
chars) via `MediaURLEntry` / `MediaIDEntry`, delivered to Mastodon, Bluesky,
X (photos/GIFs), and Pinterest:

```go
MediaURLs: []omnisocials.MediaURLEntry{
	{URL: "https://example.com/teaser.jpg", Alt: "Red sneaker on a white background"},
},
```

Publish immediately:

```go
res, err := client.Posts.CreateAndPublish(ctx, &omnisocials.PostCreateParams{
	Content:  "Going live right now",
	Channels: []string{"x", "bluesky"},
})
```

## Upload media

```go
upload, err := client.Media.UploadFromURL(ctx, &omnisocials.MediaUploadFromURLParams{
	URL:    "https://example.com/launch-video.mp4",
	Name:   "launch-video-v2",
	Folder: "Campaigns",
})
fmt.Println(upload.Data.ID, upload.Compatibility)
```

Local files via multipart (any `io.Reader`):

```go
file, err := os.Open("./photos/product.jpg")
if err != nil {
	log.Fatal(err)
}
defer file.Close()

res, err := client.Media.Upload(ctx, &omnisocials.MediaUploadParams{
	File:     file,
	Filename: "product.jpg",
	Name:     "product-hero",
})
```

## Analytics

```go
// One post's latest per-platform metrics
stats, err := client.Analytics.Post(ctx, "post_id")
fmt.Println(stats.Data.Platforms["instagram"].Metrics)

// Batch: up to 100 posts in one call
batch, err := client.Analytics.Posts(ctx, []string{"id1", "id2", "id3"})

// Workspace-wide overview
overview, err := client.Analytics.Overview(ctx, &omnisocials.AnalyticsOverviewParams{Period: "30d"})
fmt.Println(overview.Data.TotalImpressions, overview.Data.TotalEngagement)

// Account-level stats (followers etc)
accountStats, err := client.Analytics.Accounts(ctx, &omnisocials.AccountAnalyticsParams{Platform: "instagram"})
```

## Verify webhooks

```go
package main

import (
	"errors"
	"io"
	"log"
	"net/http"
	"os"
	"time"

	omnisocials "github.com/OmniSocials/omnisocials-go"
)

func main() {
	secret := os.Getenv("OMNISOCIALS_WEBHOOK_SECRET")

	http.HandleFunc("/omnisocials/webhook", func(w http.ResponseWriter, r *http.Request) {
		payload, err := io.ReadAll(r.Body) // keep the raw body!
		if err != nil {
			http.Error(w, "failed to read body", http.StatusBadRequest)
			return
		}

		event, err := omnisocials.VerifyWebhookSignature(
			payload,
			r.Header.Get("X-OmniSocials-Signature"),
			secret,
			5*time.Minute, // tolerance; pass 0 for the default (5 minutes)
		)
		if err != nil {
			var verr *omnisocials.WebhookVerificationError
			if errors.As(err, &verr) {
				http.Error(w, "invalid signature", http.StatusBadRequest)
				return
			}
			http.Error(w, "server error", http.StatusInternalServerError)
			return
		}

		data, _ := event["data"].(map[string]any)
		switch event["type"] {
		case "post.published":
			log.Println("Published:", data["post_id"], data["targets"])
		case "post.failed":
			log.Println("Failed:", data["post_id"])
		}
		w.WriteHeader(http.StatusOK)
	})

	log.Fatal(http.ListenAndServe(":8080", nil))
}
```

## Errors

Typed errors matched with `errors.As`. The `*APIError` wrapper has `Status`,
`Code`, `Message`, and `Body`. Types: `*ValidationError` (400/422),
`*AuthenticationError` (401), `*PermissionDeniedError` (403), `*NotFoundError`
(404), `*RateLimitError` (429, `RetryAfter` is a `time.Duration`), `*ServerError`
(5xx), `*ConnectionError`, and `*WebhookVerificationError`.

```go
_, err := client.Posts.Create(ctx, &omnisocials.PostCreateParams{
	Content:  "Hi",
	Channels: []string{"instagram"},
})
if err != nil {
	var rateLimited *omnisocials.RateLimitError
	var validation *omnisocials.ValidationError
	var connection *omnisocials.ConnectionError
	var apiErr *omnisocials.APIError

	switch {
	case errors.As(err, &rateLimited):
		log.Printf("rate limited, retry in %s", rateLimited.RetryAfter)
	case errors.As(err, &validation):
		log.Printf("bad request (%s): %s", validation.Code, validation.Message)
	case errors.As(err, &connection):
		log.Printf("network problem: %v", connection)
	case errors.As(err, &apiErr):
		log.Printf("API error %d (%s): %s", apiErr.Status, apiErr.Code, apiErr.Message)
	default:
		log.Fatal(err)
	}
}
```

## Configuration

```go
client, err := omnisocials.NewClient(
	omnisocials.WithAPIKey("omsk_live_..."),
	omnisocials.WithBaseURL("https://api.omnisocials.com/v1"), // default
	omnisocials.WithTimeout(30*time.Second),                   // per-request timeout (default 30s)
	omnisocials.WithMaxRetries(2),                             // retries on 429 / 5xx / network errors (default 2)
	omnisocials.WithHTTPClient(customHTTPClient),              // optional custom *http.Client
)
```

## Notes

- Every method takes a `context.Context` as its first argument for deadlines and cancellation
- Generic response envelopes: `ItemResponse[T]` and `ListResponse[T]`
- Nullable helpers: `omnisocials.Bool(false)`, `omnisocials.String(...)`, and the `omnisocials.Null` sentinel for clearing fields
- Source: [github.com/OmniSocials/omnisocials-go](https://github.com/OmniSocials/omnisocials-go)
