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), Pinterest, Instagram (images), and LinkedIn (images):
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"},})
// One post's latest per-platform metricsstats, err := client.Analytics.Post(ctx, "post_id")fmt.Println(stats.Data.Platforms["instagram"].Metrics)// Batch: up to 100 posts in one callbatch, err := client.Analytics.Posts(ctx, []string{"id1", "id2", "id3"})// Workspace-wide overviewoverview, 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"})
Social Inbox
Read and answer DMs, comments and mentions from your connected accounts. The API key
needs the opt-in inbox:read / inbox:write scopes. The lists use cursor pagination
(Cursor in, Pagination.NextCursor out).
Inbox.Next hands out the next conversation that still needs an answer, with the
thread and the post it belongs to. On a comment thread, set MessageID so the reply
goes under the comment you answer and not under the newest comment on the post.
Go
next, err := client.Inbox.Next(ctx, &omnisocials.InboxNextParams{})for err == nil && next.Data != nil { if !next.Data.ReplyWindow.Open { // An Instagram/Facebook DM past Meta's 24-hour window: answer it in the app if _, err = client.Inbox.MarkRead(ctx, next.Data.Message.ConversationID); err == nil { next, err = client.Inbox.Next(ctx, &omnisocials.InboxNextParams{}) } continue } var reply *omnisocials.InboxReplyResponse reply, err = client.Inbox.Reply(ctx, next.Data.Message.ConversationID, &omnisocials.InboxReplyParams{ Text: "Thanks! DM sent.", MessageID: next.Data.Message.ID, IncludeNext: true, }) if err == nil { next = &omnisocials.InboxNextResponse{Data: reply.Next} }}
See the Social Inbox guide for the platforms, the reply rules and the error
codes.
Pinterest product tags
Tag products on a Pin so people can shop the items in the image. List the product Pins
of the connected Pinterest account, then pass up to 24 of their Pin ids as
"product_tags" in the Pinterest options of the post. Only product Pins of your own
account can be tagged; products of other merchants cannot.
The list answers with HTTP 200 in both cases: Products on success, or an Error with
a Code (pinterest_not_connected, pinterest_catalog_access_required,
platform_error) when it could not be read. It reads the Pinterest catalog when the
connection has catalog access (given one time in the OmniSocials composer: Pinterest
options, Add products, Connect catalog), else the account's own Pins. The tags are added
right after the Pin is published. A product that Pinterest refuses never fails the post:
the post then carries Pinterest["product_tags_result"] with the tagged and the skipped
Pin ids. client.Pinterest.ValidateProduct(ctx, id) checks one Pin id or Pin link
before you post. On Posts.Update the Pinterest map replaces the stored one, so leave
product_tags out to remove the tags.
Verify webhooks
Go
package mainimport ( "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"]) case "post.rejected": approval, _ := data["approval"].(map[string]any) log.Println("Rejected:", data["post_id"], approval["reason"]) } w.WriteHeader(http.StatusOK) }) log.Fatal(http.ListenAndServe(":8080", nil))}
Events: post.scheduled, post.published, post.failed, post.approved and post.rejected.
The two approval events carry data.approval with status, decided_by (the approver's
user id) and reason (null on post.approved). To read the full review of a post (steps,
approvers, the rejection reason and the comment thread), call client.Posts.GetApproval(ctx, id).
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) }}