Go SDK
The official Go client for the FoPost API.
github.com/fopost/fopost-go is the official Go client. It needs Go 1.22 or newer and has no dependencies beyond the standard library.
go get github.com/fopost/fopost-goThis is a 0.x release. The surface is still settling and a minor version may break something. Pin an exact version if that matters to you.
Published on pkg.go.dev. Source and issues: github.com/fopost/fopost-go. MIT licensed.
Quick start
package main
import (
"context"
"log"
"github.com/fopost/fopost-go"
)
func main() {
client, err := fopost.New("fp_...") // or leave it empty and set FOPOST_API_KEY
if err != nil {
log.Fatal(err)
}
ctx := context.Background()
workspaces, err := client.Workspaces.List(ctx)
if err != nil {
log.Fatal(err)
}
workspace := workspaces[0]
accounts, err := client.Accounts.List(ctx, workspace.ID)
if err != nil {
log.Fatal(err)
}
ids := make([]string, 0, len(accounts))
for _, account := range accounts {
ids = append(ids, account.ID)
}
post, err := client.Posts.Create(ctx, &fopost.CreatePostRequest{
WorkspaceID: workspace.ID,
Accounts: ids,
Content: fopost.Text("Hello from Go"),
})
if err != nil {
log.Fatal(err)
}
if _, err := client.Posts.Publish(ctx, post.ID, nil); err != nil {
log.Fatal(err)
}
}Every method takes a context.Context first, and the client is safe for concurrent use. Publish returns once delivery is queued, not once it is live. Read Posts.Deliveries or subscribe to webhooks for the result.
Threads and media
Text builds a single-block post and a []ContentBlock builds one block per entry. Media is attached per block:
client.Posts.Create(ctx, &fopost.CreatePostRequest{
WorkspaceID: workspace.ID,
Accounts: ids,
Content: []fopost.ContentBlock{
{Text: "First post in the thread"},
{
Text: "Second one, with an image",
Media: []fopost.MediaItem{
{Type: "image", Name: "chart.png", URL: "https://.../chart.png"},
},
},
},
})Uploading through the media library gives you an item that drops straight in:
file, _ := os.Open("chart.png")
defer file.Close()
uploaded, err := client.Media.Upload(ctx, workspace.ID, fopost.File{Name: "chart.png", Content: file})
content := []fopost.ContentBlock{{
Text: "Numbers are in",
Media: []fopost.MediaItem{uploaded[0].AsMediaItem()},
}}Scheduling and publishing
Status is draft or scheduled, and a scheduled post needs ScheduleAt. Nothing reaches a platform without one of those two.
at := time.Date(2026, 9, 1, 10, 0, 0, 0, time.UTC)
post, err := client.Posts.Create(ctx, &fopost.CreatePostRequest{
WorkspaceID: workspace.ID,
Accounts: ids,
Content: fopost.Text("Scheduled with the SDK"),
Status: fopost.PostStatusScheduled,
ScheduleAt: fopost.NewTime(at),
})Times are UTC. Preflight reports per-account blockers and advisory signals without publishing, and Publish with DryRun validates the whole delivery plan:
check, _ := client.Posts.Preflight(ctx, post.ID)
for _, account := range check.Accounts {
fmt.Println(account.Platform, account.Ready, account.Issues)
}
plan, _ := client.Posts.Publish(ctx, post.ID, &fopost.PublishOptions{DryRun: true})
fmt.Println(plan.DryRun, plan.HealthWarnings)After publishing, Deliveries is the per-account state, Retry re-sends only what failed, and Cancel stops what has not gone out yet.
Pagination
Posts.List returns one page with its Meta. Each walks every page for you, and returning an error from the callback stops the walk:
page, _ := client.Posts.List(ctx, &fopost.ListPostsParams{
WorkspaceID: workspace.ID,
Status: fopost.PostStatusPublished,
PerPage: 50,
})
fmt.Printf("%d published posts\n", page.Meta.Total)
err := client.Posts.Each(ctx, &fopost.ListPostsParams{WorkspaceID: workspace.ID}, func(post fopost.Post) error {
fmt.Println(post.ID, post.Status)
return nil
})Zero-valued parameters are not sent, so the API applies its own defaults.
Configuration
client, err := fopost.New(apiKey,
fopost.WithBaseURL("http://localhost:8080/v1"), // point at another deployment
fopost.WithTimeout(30*time.Second), // per request
fopost.WithMaxRetries(3), // total attempts
fopost.WithHTTPClient(myClient), // bring your own transport
fopost.WithUserAgent("my-app/2.0"), // prefixed to the SDK's
)| Env var | Used for |
|---|---|
FOPOST_API_KEY | API key, when the one passed to New is empty |
FOPOST_BASE_URL | API root, when WithBaseURL is not given |
A 429 is retried automatically, waiting the interval the API asks for in Retry-After (capped at 60 seconds). 5xx responses and transport errors back off exponentially. MaxRetries counts total attempts, so the default of 3 means two retries; a cancelled context stops the wait immediately.
What is on the client
| Service | Methods |
|---|---|
client.Posts | List, Each, ListAll, Get, Create, Update, Delete, Duplicate, Publish, Retry, Cancel, Preflight, Deliveries, PublishRuns, Analytics, BulkShift, BulkLabel, BulkDelete, ValidateBulkImport, CommitBulkImport, RollbackBulkImport |
client.Workspaces | List, Get, Create, Update, Delete, Analytics |
client.Accounts | List, Get, Create, Delete, SetPrimary, Validate, Health, HealthSummary, RefreshToken, Analytics |
client.Communities | List, Sync, Search, Add, Remove |
client.Labels | List, Get, Create, Update, Delete |
client.Webhooks | List, Create, Update, Delete, Test |
client.Analytics | Overview, TimeSeries, TopPosts, Labels, PostsTable, PostingStreak, Demographics, Collect |
client.Automations | List, Get, Create, Update, Delete, Toggle, Runs, Run, Trigger, Stats |
client.Media | List, Upload, Delete |
For an endpoint the SDK does not wrap yet, Do sends an authenticated request and decodes the body as it came:
var body map[string]any
err := client.Do(ctx, "GET", "/platforms", nil, nil, &body)Errors
Every non-2xx response is an *fopost.Error carrying the API's Status, Code, and Message, plus the rate-limit headers that came with it.
if _, err := client.Posts.Publish(ctx, postID, nil); err != nil {
switch {
case fopost.IsPaymentRequired(err):
apiErr, _ := fopost.APIError(err)
log.Printf("upgrade at %s", apiErr.UpgradeURL())
case fopost.IsRateLimited(err):
apiErr, _ := fopost.APIError(err)
log.Printf("rate limited, retry in %s", apiErr.RetryAfter)
default:
log.Printf("api error: %v", err)
}
}| Status | Predicate | Meaning |
|---|---|---|
| 401 | IsUnauthorized | missing, invalid, or expired key |
| 402 | IsPaymentRequired | no active subscription, or credits exhausted |
| 403 | IsForbidden | valid key, but no scope or workspace access |
| 404 | IsNotFound | no such resource, or outside the key's reach |
| 409 | IsConflict | the resource's state forbids the change |
| 429 | IsRateLimited | over the plan's per-minute ceiling |
StatusOf and CodeOf read the same fields off any error, and (*Error).Field decodes the extra context some responses carry.
Next
Related documentation
- SDKs & Integrations
Official FoPost clients for TypeScript, Python, PHP, Ruby, Go, Rust, Java, .NET, Swift, Kotlin, Dart, and Elixir, framework integrations from Laravel to Next.js, and tooling for the CLI, CI, Terraform, and the automation platforms.
- SDKs Overview
Every official FoPost client, what it covers, and how to pick one.
- TypeScript SDK
The official TypeScript and Node.js client for the FoPost API.
- Python SDK
The official Python client for the FoPost API.
- PHP SDK
The official PHP client for the FoPost API, with no framework and no HTTP library.
- Ruby SDK
The official Ruby client for the FoPost API, with no runtime dependencies.
- Rust SDK
The official Rust client for the FoPost API, async and built on reqwest.
- Java SDK
The official Java client for the FoPost API, on the JDK's own HTTP client.