Skip to content

Twitter API in Go (Golang): working code

To get Twitter/X data in Go, send HTTPS requests with net/http, decode the JSON into typed structs and follow the cursor page by page. X ships no official Go SDK, and the official X API bills $0.005 per post read.

Short answer: X publishes official SDKs only for Python and TypeScript, so Go code calls the official X API through net/http or a community library such as gotwi, with a developer account and prepaid credits: $0.005 per post. For public data, use Sorsa API from net/http: one key, 20 tweets per search request from $0.07 per 1,000; 100 free requests cover 2,000.

Endpoint
POST /search-tweets
Page size
20 tweets per request
Price
from $0.07 per 1,000 tweets

#Does X publish an official Go SDK?

No. X's developer documentation lists official SDKs for Python and TypeScript only and files Go under community libraries that X doesn't maintain (X's tools and libraries page (opens in a new tab)). X's one official Go project is xurl, a command-line tool you run in a terminal, not a package you import.

That leaves the community clients, and only one of them still ships releases. The dates below come from each project's GitHub history:

Go clientLatest releaseState on November 2, 2026
gotwiv0.18.5, August 21, 2026Maintained; covers most v2 endpoints with OAuth 1.0a, a bearer token or PKCE
go-twitter v2 (g8rswimmer)v2.1.5, December 29, 2022No release in almost four years
go-twitter (dghubble)archived in 2022Written for the retired v1.1 API
  • gotwi

    Latest release
    v0.18.5, August 21, 2026
    State on November 2, 2026
    Maintained; covers most v2 endpoints with OAuth 1.0a, a bearer token or PKCE
  • go-twitter v2 (g8rswimmer)

    Latest release
    v2.1.5, December 29, 2022
    State on November 2, 2026
    No release in almost four years
  • go-twitter (dghubble)

    Latest release
    archived in 2022
    State on November 2, 2026
    Written for the retired v1.1 API

A library only shapes the requests. Every call still spends X's prepaid credits: $0.005 per post and $0.010 per user, up to 3M post reads a month (X's price table (opens in a new tab)). From November 22, 2026 the authors and media that come back through expansions are billed at the same rates (X's notice of October 23 (opens in a new tab)), so the search most Go code sends costs up to three times the post price:

x-api.go
q := url.Values{
	"query":       {"golang lang:en"},
	"max_results": {"100"},
	"expansions":  {"author_id"},
}
req, _ := http.NewRequestWithContext(ctx, http.MethodGet,
	"https://api.x.com/2/tweets/search/recent?"+q.Encode(), nil)
req.Header.Set("Authorization", "Bearer "+os.Getenv("X_BEARER_TOKEN"))
// 100 posts: $0.50. From Nov 22, 2026, with authors: up to $1.50

What the official route needs before the first call

  • A developer account at console.x.com, opened with an X account, with X's Developer Agreement accepted.
  • An app, and its bearer token, which X shows only once.
  • Credits bought in advance and spent on every post and user a response returns.
  • OAuth user tokens for anything that posts, likes or follows.

Reading public posts, profiles and followers needs none of these. The same choice for Tweepy and xdk is laid out in the Twitter API in Python guide; the rest of this page does the job in Go.

#Get tweets in Go with one API key

To read X data from Go, use Sorsa API, a Twitter (X) API and scraper: one ApiKey header instead of OAuth, 20 requests per second on every plan, free accounts included, search back to 2006, and search results from $0.07 per 1,000 tweets with the author's full profile inside each one. The standard library is the whole client: net/http sends the request and encoding/json reads the reply, so go.mod gains no dependency. Sorsa API never asks for your X login, so your X account carries no risk.

  1. Get a key. Sign up and copy the API key from the dashboard. The first 100 requests are free, need no card and never expire.

  2. Start a module and store the key. Keep the key in an environment variable and call the API from server-side code only, never from a browser or an app you hand to users. The code in this guide needs Go 1.25 or newer.

    Terminal
    go mod init example.com/xdata
    export SORSA_API_KEY="your_key"  # PowerShell: $env:SORSA_API_KEY="your_key"
    
  3. Search for tweets. Send a query in X's search syntax to POST /search-tweets (opens in a new tab) with the key in the ApiKey header, then run go run ..

    package main
    
    import (
    	"bytes"
    	"encoding/json"
    	"fmt"
    	"log"
    	"net/http"
    	"os"
    	"time"
    )
    
    func main() {
    	key := os.Getenv("SORSA_API_KEY") // or "YOUR_API_KEY" to try it
    	body, _ := json.Marshal(map[string]string{
    		"query": "golang lang:en",
    		"order": "latest",
    	})
    	req, err := http.NewRequest(http.MethodPost,
    		"https://api.sorsa.io/v3/search-tweets", bytes.NewReader(body))
    	if err != nil {
    		log.Fatal(err)
    	}
    	req.Header.Set("ApiKey", key)
    	req.Header.Set("Content-Type", "application/json")
    
    	client := &http.Client{Timeout: 30 * time.Second}
    	resp, err := client.Do(req)
    	if err != nil {
    		log.Fatal(err)
    	}
    	defer resp.Body.Close()
    	if resp.StatusCode != http.StatusOK {
    		log.Fatalf("sorsa: %s", resp.Status)
    	}
    
    	var page struct {
    		Tweets []struct {
    			ID       string `json:"id"`
    			FullText string `json:"full_text"`
    			User     struct {
    				Username string `json:"username"`
    			} `json:"user"`
    		} `json:"tweets"`
    		NextCursor string `json:"next_cursor"`
    	}
    	if err := json.NewDecoder(resp.Body).Decode(&page); err != nil {
    		log.Fatal(err)
    	}
    	for _, t := range page.Tweets {
    		fmt.Println(t.User.Username, t.FullText)
    	}
    }
    
  4. Read the page. Each response holds up to 20 tweets and a next_cursor for the next page. The values below are demo data in the documented shape.

    Response200 OK
    {
      "tweets": [
        {
          "id": "1985611093417201664",
          "full_text": "Rewrote our ingest worker with range-over-func iterators.",
          "created_at": "2026-11-01T21:14:52Z",
          "lang": "en",
          "likes_count": 57,
          "retweet_count": 9,
          "reply_count": 4,
          "quote_count": 2,
          "view_count": 4120,
          "bookmark_count": 11,
          "is_reply": false,
          "quoted_status": null,
          "user": {
            "id": "1601739518260654080",
            "username": "gopher_ingest_notes",
            "display_name": "Gopher Ingest Notes",
            "created_at": "2022-12-11T09:03:18Z",
            "followers_count": 2310
          }
        }
      ],
      "next_cursor": "DAABCgABF7Zq0xLwAAA"
    }
    

Every tweet arrives with its counts and its author, so the request sets no expansions or field lists, and each request costs the same whatever it returns.

#Model the response with typed structs

Define one Tweet and one User struct for the fields your code reads, and every Sorsa API endpoint that returns tweets or users decodes into them, because the API uses the same two shapes everywhere. Put them in a small package, here sorsa, next to the client from the next section.

sorsa/types.go
package sorsa

import "time"

type Tweet struct {
	ID              string    `json:"id"`
	FullText        string    `json:"full_text"`
	CreatedAt       time.Time `json:"created_at"`
	Lang            string    `json:"lang"`
	LikesCount      int       `json:"likes_count"`
	RetweetCount    int       `json:"retweet_count"`
	ReplyCount      int       `json:"reply_count"`
	QuoteCount      int       `json:"quote_count"`
	ViewCount       int       `json:"view_count"`
	IsReply         bool      `json:"is_reply"`
	User            User      `json:"user"`
	QuotedStatus    *Tweet    `json:"quoted_status"`
	RetweetedStatus *Tweet    `json:"retweeted_status"`
}

type User struct {
	ID              string    `json:"id"`
	Username        string    `json:"username"`
	DisplayName     string    `json:"display_name"`
	Description     string    `json:"description"`
	CreatedAt       time.Time `json:"created_at"`
	FollowersCount  int       `json:"followers_count"`
	FollowingsCount int       `json:"followings_count"`
	TweetsCount     int       `json:"tweets_count"`
	Verified        bool      `json:"verified"`
}

// TweetsPage and UsersPage are the two list shapes. NextCursor
// is empty on the last page.
type TweetsPage struct {
	Tweets     []Tweet `json:"tweets"`
	NextCursor string  `json:"next_cursor"`
}

type UsersPage struct {
	Users      []User `json:"users"`
	NextCursor string `json:"next_cursor"`
}

Three choices in these structs matter more than the rest. IDs have 19 digits and arrive as JSON strings, so they decode into string: an int64 field stops the decoder with "cannot unmarshal string into Go struct field". Dates arrive in ISO 8601 UTC, which time.Time reads with no custom code. A quoted or reposted tweet is a tweet inside a tweet, so it needs a pointer: Go rejects a struct that contains itself by value, and the pointer stays nil when the post quotes nothing.

Sorsa API tweet JSON mapped to a Go Tweet struct SORSA API JSON GO STRUCT FIELD "id": "1985611093417201664" string, not int64 ID string "created_at": "2026-11-01T21:14:52Z" ISO 8601, UTC CreatedAt time.Time "likes_count": 57 tag matches the key LikesCount int "user": { "username": ... } nested struct User User "quoted_status": null nil when absent QuotedStatus *Tweet One tweet object; the same User shape comes back from every user endpoint
Strings for IDs, time.Time for dates and a pointer for the nested tweet: the three types that decide whether a struct decodes.

A tag that matches no JSON key is not an error in Go: the field just stays zero. Code moved from the official X API shows it first, because X names the count like_count inside public_metrics while Sorsa API returns likes_count on the tweet itself. Pick the fields you need below and copy a struct whose tags match the API reference.

Tweet fields
Author fields (user)
types.go
import "time"

type Tweet struct {
	ID         string    `json:"id"`
	FullText   string    `json:"full_text"`
	CreatedAt  time.Time `json:"created_at"`
	LikesCount int       `json:"likes_count"`
	ViewCount  int       `json:"view_count"`
	User       User      `json:"user"`
}

type User struct {
	ID             string `json:"id"`
	Username       string `json:"username"`
	FollowersCount int    `json:"followers_count"`
}

The tags and types follow Sorsa API's live OpenAPI reference; unticked fields are simply skipped by encoding/json.

#Build one client with retries and a pacer

Put the key, the HTTP client, the retry rules and the rate pacer in one Client, and share that one value across every goroutine: http.Client is safe for concurrent use, and so is the ticker that paces the starts. Every later example calls its Do method.

sorsa/client.go
package sorsa

import (
	"bytes"
	"context"
	"encoding/json"
	"fmt"
	"io"
	"net/http"
	"net/url"
	"time"
)

const baseURL = "https://api.sorsa.io/v3"

// Client is safe for concurrent use: share one per API key.
type Client struct {
	key  string
	http *http.Client
	pace *time.Ticker
}

// New paces requests at 19 starts a second, one under the limit.
// Pass the key from os.Getenv("SORSA_API_KEY"), or "YOUR_API_KEY".
func New(key string) *Client {
	return &Client{
		key:  key,
		http: &http.Client{Timeout: 30 * time.Second},
		pace: time.NewTicker(time.Second / 19),
	}
}

// APIError is a reply other than 200, with the API's message.
type APIError struct {
	Status  int    `json:"-"`
	Message string `json:"message"`
}

func (e *APIError) Error() string {
	return fmt.Sprintf("sorsa: %d %s", e.Status, e.Message)
}

// Do sends one request and decodes the reply into out. It retries
// 429 and 5xx up to 4 times, after 1, 2, 4 and 8 seconds.
func (c *Client) Do(ctx context.Context, method, path string,
	query url.Values, body, out any) error {
	var payload []byte
	if body != nil {
		var err error
		if payload, err = json.Marshal(body); err != nil {
			return err
		}
	}
	u := baseURL + path
	if len(query) > 0 {
		u += "?" + query.Encode()
	}
	for attempt := 0; ; attempt++ {
		select { // wait for a start slot
		case <-c.pace.C:
		case <-ctx.Done():
			return ctx.Err()
		}
		req, err := http.NewRequestWithContext(ctx, method, u,
			bytes.NewReader(payload))
		if err != nil {
			return err
		}
		req.Header.Set("ApiKey", c.key)
		req.Header.Set("Content-Type", "application/json")
		resp, err := c.http.Do(req)
		if err != nil {
			return err
		}
		if resp.StatusCode == http.StatusOK {
			defer resp.Body.Close()
			return json.NewDecoder(resp.Body).Decode(out)
		}
		apiErr := &APIError{Status: resp.StatusCode}
		_ = json.NewDecoder(io.LimitReader(resp.Body, 4096)).Decode(apiErr)
		resp.Body.Close()
		retry := resp.StatusCode == http.StatusTooManyRequests ||
			resp.StatusCode >= 500
		if !retry || attempt == 4 {
			return apiErr
		}
		select {
		case <-time.After(time.Second << attempt):
		case <-ctx.Done():
			return ctx.Err()
		}
	}
}

A 429 means more than 20 requests started within one second on your key, and a 5xx is a temporary fault on the server side; both are worth a retry. A 401 or 403 is not: the key is wrong or missing, or its requests are used up, and Message says which. Failed requests are never charged, so a retry costs nothing, and every Sorsa API endpoint only reads, so retrying a POST such as /search-tweets can't create anything twice.

The ticker hands out one start every 1/19 of a second, and every attempt waits for one, retries included. At 19 a second the pacer leaves one start of headroom, so a tick saved while the program sat idle can never push a second past Sorsa API's limit of 20. The context ends both waits at once, so a cancelled job stops without sending the requests it had queued. Callers tell errors apart with errors.As:

Go
var apiErr *sorsa.APIError
if errors.As(err, &apiErr) && apiErr.Status == http.StatusForbidden {
	log.Fatal("check the key or its remaining requests: ", apiErr.Message)
}

#Page through results with an iterator

A cursor is an opaque key to the next page of results: send next_cursor back unchanged and stop when a response comes without one. Search and timelines return up to 20 tweets per page, and a page can hold fewer while more remain, so the item count never tells you that you reached the end. Since Go 1.23 a function can be the thing a for range loop walks, which keeps the whole cursor loop in one method:

sorsa/pages.go
package sorsa

import (
	"context"
	"iter"
	"net/http"
	"net/url"
)

// SearchPages yields pages of tweets until the cursor runs out
// or maxPages pages have been read.
func (c *Client) SearchPages(ctx context.Context, query string,
	maxPages int) iter.Seq2[[]Tweet, error] {
	return func(yield func([]Tweet, error) bool) {
		body := map[string]string{"query": query, "order": "latest"}
		for range maxPages {
			var page TweetsPage
			err := c.Do(ctx, http.MethodPost, "/search-tweets",
				nil, body, &page)
			if !yield(page.Tweets, err) || err != nil {
				return
			}
			next := page.NextCursor
			if next == "" || next == body["next_cursor"] {
				return
			}
			body["next_cursor"] = next
		}
	}
}

// Followers yields an account's followers, up to 200 a page.
func (c *Client) Followers(ctx context.Context, username string,
	maxPages int) iter.Seq2[[]User, error] {
	return func(yield func([]User, error) bool) {
		q := url.Values{"username": {username}}
		for range maxPages {
			var page UsersPage
			err := c.Do(ctx, http.MethodGet, "/followers",
				q, nil, &page)
			if !yield(page.Users, err) || err != nil {
				return
			}
			next := page.NextCursor
			if next == "" || next == q.Get("next_cursor") {
				return
			}
			q.Set("next_cursor", next)
		}
	}
}

The caller reads pages the way it reads any slice, and a break stops the requests on the spot:

main.go
client := sorsa.New(os.Getenv("SORSA_API_KEY"))
ctx := context.Background()

for tweets, err := range client.SearchPages(ctx, "golang lang:en", 50) {
	if err != nil {
		log.Fatal(err)
	}
	for _, t := range tweets {
		fmt.Println(t.ID, t.CreatedAt.Format(time.DateOnly), t.User.Username)
	}
}

POST endpoints take the cursor in the JSON body and GET endpoints take it as a query parameter, which is the only difference between the two methods. GET /followers (opens in a new tab) returns up to 200 profiles a page, so a list of one million followers takes about 5,000 requests; the guide to export Twitter followers writes such a list to CSV. Treat maxPages as a budget, so a broad query never spends more than you planned, and keep the query unchanged while you follow one cursor. The check against a repeated cursor is a cheap guard that ends a loop which would otherwise spend the whole budget on one page.

#Fetch 100 tweets or profiles per request

When you already hold IDs or usernames, send them in batches. POST /tweet-info-bulk (opens in a new tab) takes up to 100 tweet IDs or links, and GET /info-batch (opens in a new tab) takes up to 100 usernames, each in one request. slices.Chunk cuts the list for you:

Go
var tweets []sorsa.Tweet
for batch := range slices.Chunk(tweetIDs, 100) {
	var page sorsa.TweetsPage
	body := map[string][]string{"tweet_links": batch}
	err := client.Do(ctx, http.MethodPost, "/tweet-info-bulk",
		nil, body, &page)
	if err != nil {
		return err
	}
	tweets = append(tweets, page.Tweets...)
}

var users []sorsa.User
for batch := range slices.Chunk(usernames, 100) {
	var page sorsa.UsersPage
	q := url.Values{"usernames": batch} // usernames=a&usernames=b
	err := client.Do(ctx, http.MethodGet, "/info-batch", q, nil, &page)
	if err != nil {
		return err
	}
	users = append(users, page.Users...)
}

Every request costs the same whatever it returns, so a batch of 100 tweets costs what one tweet costs: from $0.014 per 1,000 tweets, more than 250x less than the official X API's $5.00. url.Values is a map of string slices, so a batch drops in as is and goes out as the repeated parameter GET /info-batch reads. Batch responses have no cursor: one response holds the whole batch, under tweets or under users.

#Run goroutines within 20 requests per second

Sorsa API allows 20 requests per second per key on every plan, and a Go program reaches that rate with a small pool of goroutines once the work splits into independent cursor chains. One chain can't be split, because each page needs the cursor from the page before. So give each goroutine whole chains, one account or one day of a search at a time, and let the shared client pace them all:

sorsa/collect.go
package sorsa

import (
	"context"
	"errors"
	"fmt"
	"sync"
)

// Collect runs every query through a pool of workers. They share
// one Client, so its pacer caps the key's total rate.
func Collect(ctx context.Context, c *Client, queries []string,
	workers int) ([]Tweet, error) {
	jobs := make(chan string)
	var (
		mu   sync.Mutex
		all  []Tweet
		errs []error
		wg   sync.WaitGroup
	)
	for range workers {
		wg.Go(func() {
			for q := range jobs {
				for tweets, err := range c.SearchPages(ctx, q, 100) {
					mu.Lock()
					if err != nil {
						errs = append(errs, fmt.Errorf("%s: %w", q, err))
					} else {
						all = append(all, tweets...)
					}
					mu.Unlock()
				}
			}
		})
	}
	for _, q := range queries {
		jobs <- q
	}
	close(jobs)
	wg.Wait()
	return all, errors.Join(errs...)
}

Split a long search into days with the since: and until: operators and hand the days to the pool:

main.go
start := time.Date(2026, 10, 1, 0, 0, 0, 0, time.UTC)
var days []string
for d := range 31 { // October 1 to November 1
	day := start.AddDate(0, 0, d)
	days = append(days, fmt.Sprintf("golang lang:en since:%s until:%s",
		day.Format(time.DateOnly), day.AddDate(0, 0, 1).Format(time.DateOnly)))
}
tweets, err := sorsa.Collect(ctx, client, days, 20)
A Go worker pool sharing one pacer under Sorsa API's 20 requests per second JOBS CHANNEL 20 GOROUTINES, ONE CHAIN EACH ONE SHARED CLIENT since:10-01 since:10-02 since:10-03 since:10-31 one query per day goroutine 1 goroutine 2 goroutine 3 goroutine 20 pages joined by next_cursor c.pace 19 starts a second for all workers Sorsa API 20 per second per API key all []Tweet guarded by mu
Workers set how many chains run at once; the one ticker in the shared Client sets how fast they run.

Twenty workers are plenty: most of their time goes to waiting for the next tick, and more workers only wait longer. wg.Go, added in Go 1.25, starts each goroutine and counts it in one call. Keep one Client per key and per process: two processes with a client each can start twice the limit between them, so give each process its own key or half the rate. At 20 requests a second, batches of 100 move up to 2,000 tweets a second, follower pages 4,000 profiles and search pages 400 tweets.

Good to know

Collect keeps going when one day fails: it saves the error with the query that caused it and returns all of them through errors.Join at the end. To stop the whole run at the first failure, wrap the context with context.WithCancel and call cancel() where the error is saved; the pacer and the retry waits both return as soon as the context ends.

#What a Go job costs

Sorsa API charges per request, whatever the endpoint returns: tweets fetched in batches of 100 cost from $0.014 per 1,000, search results from $0.07, followers from $0.007, and failed requests are never charged. The official X API charges per object instead: $5.00 per 1,000 posts, and from November 22, 2026 up to $15.00 per 1,000 when the request expands each post's author.

Tweets your Go service reads by ID

Official X API

$5,000

for 1M tweets

$0.005 per post

Sorsa API

from$14

for 1M tweets

from $0.014 per 1,000

more than 250x cheaper

The rate per 1,000 requests runs from $4.90 on Starter to $1.40 on Hyperscale, and a plan applies it to every endpoint alike. A service can watch its balance for free: GET /key-usage-info (opens in a new tab) never counts against your requests.

Go
var usage struct {
	Remaining  int       `json:"remaining_requests"`
	ValidUntil time.Time `json:"valid_until"`
}
err := client.Do(ctx, http.MethodGet, "/key-usage-info", nil, nil, &usage)
if err == nil {
	fmt.Println(usage.Remaining, "left until", usage.ValidUntil)
}

#Moving gotwi or go-twitter code to Sorsa API

gotwi and go-twitter are thin layers over the official X API's v2 paths, so their calls map to Sorsa API by path. Each path below has one Sorsa API endpoint, and the code gets shorter: no expansions and no field lists, because every tweet already carries its author and counts.

Official X API v2 pathSorsa API endpointWhat changes
GET /2/tweets/search/recentPOST /search-tweetsAny date back to 2006, not only the last 7 days
GET /2/tweets/search/allPOST /search-tweetsThe same endpoint with since: and until:
GET /2/users/:id/tweetsPOST /user-tweets (opens in a new tab)A username is enough; no 3,200-post cap
GET /2/tweetsPOST /tweet-info-bulk100 IDs or links per request
GET /2/users/byGET /info-batch100 usernames per request
GET /2/users/:id/followersGET /followers200 followers per page
GET /2/tweets/:id/retweeted_byPOST /retweeters (opens in a new tab)The same cursor loop
POST /2/tweetsnoneRead-only: post through the official X API
  • GET /2/tweets/search/recent

    Sorsa API endpoint
    POST /search-tweets
    What changes
    Any date back to 2006, not only the last 7 days
  • GET /2/tweets/search/all

    Sorsa API endpoint
    POST /search-tweets
    What changes
    The same endpoint with since: and until:
  • GET /2/users/:id/tweets

    What changes
    A username is enough; no 3,200-post cap
  • GET /2/tweets

    Sorsa API endpoint
    POST /tweet-info-bulk
    What changes
    100 IDs or links per request
  • GET /2/users/by

    Sorsa API endpoint
    GET /info-batch
    What changes
    100 usernames per request
  • GET /2/users/:id/followers

    Sorsa API endpoint
    GET /followers
    What changes
    200 followers per page
  • GET /2/tweets/:id/retweeted_by

    What changes
    The same cursor loop
  • POST /2/tweets

    Sorsa API endpoint
    none
    What changes
    Read-only: post through the official X API

Field names change with the paths: text becomes full_text, public_metrics.like_count becomes likes_count, author_id with includes.users becomes the nested user, and meta.next_token becomes next_cursor. Rate-limit handling shrinks too, because X's 15-minute windows give way to one limit that resets every second. To move a whole codebase, every v2 endpoint and response field is mapped in the guide to migrating from the Twitter/X API.

#Run your first search from Go today

Getting X data into Go takes the standard library, one key and four pieces: typed structs, a Client with retries and a pacer, iterators over the cursor, and a worker pool for many chains. Build on Sorsa API, the Twitter API for reading public X data at scale: 20 tweets per search request from $0.07 per 1,000, batches of 100 from $0.014, 20 requests per second on every plan, and 100 free requests that need no card and never expire. The same calls in other languages are in the guides to the Twitter API in Node.js and the Twitter API in PHP.

#FAQ

Is there a free Twitter API for Go?

The official X API has no free tier for new developers: gotwi and the other Go clients are free, but every read spends prepaid credits at $0.005 per post. Sorsa API gives every new account 100 free requests that never expire and need no card. From Go they collect 2,000 tweets by search, or up to 10K tweets in batches of 100.

Which Twitter API should I use with Go?

For reading X data from Go, use Sorsa API: the standard library's net/http with one key instead of OAuth, search results from $0.07 per 1,000 tweets and batches from $0.014, 20 requests per second on every plan for goroutines to share, search back to 2006, and no X login, so your account is never at risk. To post as well, pair it with the official X API.

Can I get X data in Go without a developer account?

Yes. Sorsa API needs only its own key: sign up, set the ApiKey header and call any of its 45 endpoints from Go, with no X developer account, app review or OAuth. Go libraries that read X's website instead log in with an account's cookies or a pool of accounts, which puts those accounts at risk. The options are compared in Twitter API without a developer account.

Can I post tweets from Go with Sorsa API?

No. Sorsa API is read-only by design: it reads public posts, profiles and followers and never posts, likes, follows or sends messages, so a key can't act on any X account. To post from Go, call the official X API's POST /2/tweets with an OAuth user token, directly or through gotwi, at $0.015 a post, and keep Sorsa API for the reads.

Is there a Go SDK or module for Sorsa API?

No module is needed: Sorsa API is plain REST with JSON, and the client in this guide is about 100 lines of the standard library. The live OpenAPI reference at api.sorsa.io/v3/swagger.json lists all 45 endpoints with their fields, so a generator such as oapi-codegen can produce typed request and response code if you prefer generated types.

#How we checked

We wrote every request in this guide from Sorsa API's live OpenAPI reference (45 endpoints) and its documentation on rate limits and pagination, read on November 2, 2026, and every Go construct from the standard library's documentation and release notes (iterators and slices.Chunk from Go 1.23, WaitGroup.Go from Go 1.25; the current release is Go 1.27). We had no Sorsa API key and no Go toolchain on our machine for this pass, so the response shows demo values in the documented shape, and we checked each code block line by line against those documents instead of running it. The same day we read the official X API's price table, X's notice on expansion billing and its list of official SDKs and community libraries on docs.x.com, and the release history of gotwi, both go-twitter projects and xurl on GitHub.

By the Sorsa API team. We build and run Sorsa API, the read-only Twitter/X API and scraper, in production since 2022 (formerly TweetScout API) with 6B+ requests served. About Sorsa API

What changed
  • Rewritten from scratch: the state of Go clients for the official X API with their release dates, today's official X API prices with the November 22 expansion billing, and a Go path with one API key: typed structs, a client with retries and a pacer, iterator pagination, batches of 100, a worker pool at 20 requests per second and a v2 path map.
  • First published.