RelayAPI

Go SDK

Official Go client library for RelayAPI. Post to 22 platforms with idiomatic, fully-typed Go.

Official Go SDK for RelayAPI. Post to 22 platforms, manage accounts, upload media, and track analytics.

The Go client is generated and released separately from the RelayAPI monorepo. Check the relay-go repository for the surface and version in the release you install.

Setup

Install

go get -u github.com/relayapi-dev/relay-go

Requires Go 1.22 or later.

Authenticate

import (
    relaygo "github.com/relayapi-dev/relay-go"
    "github.com/relayapi-dev/relay-go/option"
)

client := relaygo.NewClient() // reads RELAY_API_KEY from environment

Or pass the key directly:

client := relaygo.NewClient(
    option.WithAPIKey("rlay_live_..."),
)

Get your API key from relayapi.dev under Settings > API Keys.

Quick Examples

Request fields are wrapped with relaygo.F(...) so the SDK can distinguish between an unset field and a zero value.

Create and Publish a Post

post, err := client.Posts.New(context.TODO(), relaygo.PostNewParams{
    Content:     relaygo.F("Hello from the Go SDK!"),
    Targets:     relaygo.F([]string{"twitter", "linkedin"}),
    ScheduledAt: relaygo.F("now"),
})
if err != nil {
    panic(err.Error())
}
fmt.Printf("Post created: %s — status: %s\n", post.ID, post.Status)

Schedule a Post

post, err := client.Posts.New(context.TODO(), relaygo.PostNewParams{
    Content:     relaygo.F("This will go out tomorrow morning"),
    Targets:     relaygo.F([]string{"twitter", "linkedin"}),
    ScheduledAt: relaygo.F("2025-01-15T09:00:00Z"),
    Timezone:    relaygo.F("America/New_York"),
})

Save as Draft

post, err := client.Posts.New(context.TODO(), relaygo.PostNewParams{
    Content:     relaygo.F("Work in progress..."),
    Targets:     relaygo.F([]string{"instagram"}),
    ScheduledAt: relaygo.F("draft"),
})

List Connected Accounts

accounts, err := client.Accounts.List(context.TODO(), relaygo.AccountListParams{})
if err != nil {
    panic(err.Error())
}
for _, acc := range accounts.Data {
    fmt.Printf("%s: %s (%s)\n", acc.Platform, acc.DisplayName, acc.ID)
}

Upload Media

import (
    "context"
    "net/http"
    "net/url"
    "os"
    "strings"
)

// Get a presigned upload URL
presign, err := client.Media.GetPresignURL(context.TODO(), relaygo.MediaGetPresignURLParams{
    Filename:    relaygo.F("photo.jpg"),
    ContentType: relaygo.F("image/jpeg"),
})
if err != nil {
    panic(err.Error())
}

// PUT the bytes once using both headers required by the presign contract
file, err := os.Open("photo.jpg")
if err != nil {
    panic(err.Error())
}
defer file.Close()
request, err := http.NewRequestWithContext(context.TODO(), http.MethodPut, presign.UploadURL, file)
if err != nil {
    panic(err.Error())
}
request.Header.Set("Content-Type", "image/jpeg")
request.Header.Set("If-None-Match", "*")
upload, err := http.DefaultClient.Do(request)
if err != nil {
    panic(err.Error())
}
defer upload.Body.Close()
if upload.StatusCode < 200 || upload.StatusCode >= 300 {
    panic(upload.Status)
}

// Confirm the object so the pending upload becomes ready
mediaURL, err := url.Parse(presign.URL)
if err != nil {
    panic(err.Error())
}
storageKey := strings.TrimPrefix(mediaURL.Path, "/")
confirmed, err := client.Media.Confirm(context.TODO(), relaygo.MediaConfirmParams{
    StorageKey: relaygo.F(storageKey),
})
if err != nil {
    panic(err.Error())
}
if confirmed.ID != presign.ID {
    panic("confirmed the wrong media intent")
}

// Posts take a media object with the canonical URL, not the media ID
post, err := client.Posts.New(context.TODO(), relaygo.PostNewParams{
    Content: relaygo.F("Check out this photo!"),
    Targets: relaygo.F([]string{"instagram"}),
    Media: relaygo.F([]relaygo.PostNewParamsMedia{{
        URL:  relaygo.F(presign.URL),
        Type: relaygo.F(relaygo.PostNewParamsMediaTypeImage),
    }}),
    ScheduledAt: relaygo.F("now"),
})

Cross-Post with Per-Platform Content

post, err := client.Posts.New(context.TODO(), relaygo.PostNewParams{
    Content: relaygo.F("Default content for all platforms"),
    Targets: relaygo.F([]string{"twitter", "linkedin", "bluesky"}),
    TargetOptions: relaygo.F(map[string]map[string]interface{}{
        "twitter":  {"content": "Short version for Twitter"},
        "linkedin": {"content": "Longer professional version for LinkedIn"},
    }),
    ScheduledAt: relaygo.F("now"),
})

Get Post Analytics

analytics, err := client.Analytics.Get(context.TODO(), relaygo.AnalyticsGetParams{
    PostID: relaygo.F("post_abc123"),
})
fmt.Printf("%+v\n", analytics)

Check Account Health

// All accounts
health, err := client.Accounts.Health.List(context.TODO())

// A single account by ID
acc, err := client.Accounts.Health.Get(context.TODO(), "acc_twitter_123")

Error Handling

When the API returns a non-success status code, the error can be inspected as a *relaygo.Error:

_, err := client.Posts.List(context.TODO(), relaygo.PostListParams{})
if err != nil {
    var apierr *relaygo.Error
    if errors.As(err, &apierr) {
        println(string(apierr.DumpRequest(true)))
        println(string(apierr.DumpResponse(true)))
    }
    panic(err.Error())
}

Configuration

Set your API key via the RELAY_API_KEY environment variable, or pass option.WithAPIKey(...) / option.WithBaseURL(...) to NewClient.

Timeouts

Pass a context with a deadline to bound any request:

ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute)
defer cancel()
client.Posts.List(ctx, relaygo.PostListParams{})

Found something wrong? Help us improve this page.

On this page