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-go

This 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 varUsed for
FOPOST_API_KEYAPI key, when the one passed to New is empty
FOPOST_BASE_URLAPI 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

ServiceMethods
client.PostsList, Each, ListAll, Get, Create, Update, Delete, Duplicate, Publish, Retry, Cancel, Preflight, Deliveries, PublishRuns, Analytics, BulkShift, BulkLabel, BulkDelete, ValidateBulkImport, CommitBulkImport, RollbackBulkImport
client.WorkspacesList, Get, Create, Update, Delete, Analytics
client.AccountsList, Get, Create, Delete, SetPrimary, Validate, Health, HealthSummary, RefreshToken, Analytics
client.CommunitiesList, Sync, Search, Add, Remove
client.LabelsList, Get, Create, Update, Delete
client.WebhooksList, Create, Update, Delete, Test
client.AnalyticsOverview, TimeSeries, TopPosts, Labels, PostsTable, PostingStreak, Demographics, Collect
client.AutomationsList, Get, Create, Update, Delete, Toggle, Runs, Run, Trigger, Stats
client.MediaList, 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)
	}
}
StatusPredicateMeaning
401IsUnauthorizedmissing, invalid, or expired key
402IsPaymentRequiredno active subscription, or credits exhausted
403IsForbiddenvalid key, but no scope or workspace access
404IsNotFoundno such resource, or outside the key's reach
409IsConflictthe resource's state forbids the change
429IsRateLimitedover 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.

Was this helpful?

On this page