Xquik Go SDK: Twitter Search, Followers & X Automation

August 20, 2026 ยท View on GitHub

OpenSSF Best Practices Ask DeepWiki Skills.sh x-twitter-scraper Skill

Go Reference

Use the Xquik Go SDK for Twitter search, profiles, followers & media. Manage webhooks, bulk extractions & X automation with typed Go methods. The module calls the documented Xquik REST API.

Twitter API Alternative

This package does not call or emulate the official X API. Use it when the official X API does not fit your workflow.

Go Reference | REST API Docs | OpenAPI Spec | Context7 | Webhooks | OAuth-First MCP Guide

Common Twitter & X Tasks

TaskREST RouteWorkflow Note
Search tweets without the X APIGET /x/tweets/searchUse keyword or advanced operator queries.
Read an X profile timelineGET /x/users/{id}/tweetsPaginate bounded results.
Scrape Twitter followersGET /x/users/{id}/followersUse an extraction for complete datasets.
Scrape following accountsGET /x/users/{id}/followingUse an extraction for complete datasets.
Read a home timelineGET /x/timelineApprove this private read.
Export large X datasetsPOST /extractionsPoll status, then download results.
Download or upload media/x/media/*Use typed file helpers.
Monitor an accountPOST /monitorsDeliver events through HMAC webhooks.
Post or replyPOST /x/tweetsConfirm the account and payload.

Installation

import (
	"github.com/Xquik-dev/x-twitter-scraper-go" // imported as xtwitterscraper
)

Or to pin the version:

go get -u 'github.com/Xquik-dev/x-twitter-scraper-go@v0.18.1'

Verify a Release

Verify the matching source archive before upgrading:

release_tag=vVERSION
archive="x-twitter-scraper-go-$release_tag.zip"

gh release download "$release_tag" \
  --repo Xquik-dev/x-twitter-scraper-go \
  --pattern "$archive"

gh attestation verify "$archive" \
  --repo Xquik-dev/x-twitter-scraper-go \
  --signer-workflow Xquik-dev/x-twitter-scraper-go/.github/workflows/release-provenance.yml \
  --source-ref "refs/tags/$release_tag" \
  --deny-self-hosted-runners

Require the Xquik-dev repository and release workflow. GitHub verifies the digest, signer identity, and transparency proof.

Requirements

Use Go 1.26.6 or newer.

Usage

See api.md for the complete API.

package main

import (
	"context"
	"fmt"
	"os"

	"github.com/Xquik-dev/x-twitter-scraper-go"
	"github.com/Xquik-dev/x-twitter-scraper-go/option"
)

func main() {
	client := xtwitterscraper.NewClient(
		option.WithAPIKey(os.Getenv("X_TWITTER_SCRAPER_API_KEY")),
	)
	response, err := client.X.Tweets.Search(context.TODO(), xtwitterscraper.XTweetSearchParams{
		Q:     "from:elonmusk",
		Limit: xtwitterscraper.Int(10),
	})
	if err != nil {
		panic(err)
	}
	fmt.Printf("%+v\n", response)
}

Request Fields

Request structs follow Go's omitzero semantics. Fields tagged `api:"required"` serialize even when their value is zero. Optional primitives use param.Opt[T] and constructors such as xtwitterscraper.String. Maps, slices, structs & enums tagged `json:"...,omitzero"` omit zero values. Use param.IsOmitted(any) to test any omitzero field.

p := xtwitterscraper.ExampleParams{
	ID:   "id_xxx",                      // Required.
	Name: xtwitterscraper.String("..."), // Optional.

	Point: xtwitterscraper.Point{
		X: 0,                      // Serializes as 0.
		Y: xtwitterscraper.Int(1), // Serializes as 1.
	},

	Origin: xtwitterscraper.Origin{}, // Omitted because it is zero.
}

To send null instead of a param.Opt[T], use param.Null[T](). To send null instead of a struct T, use param.NullStruct[T]().

p.Name = param.Null[string]()       // 'null' instead of string
p.Point = param.NullStruct[Point]() // 'null' instead of struct

param.IsNull(p.Name)  // true
param.IsNull(p.Point) // true

Use .SetExtraFields(map[string]any) only with trusted data. Extra fields can overwrite matching struct fields.

To send a custom value instead of a struct, use param.Override[T](value).

// Override the documented integer type.
p.SetExtraFields(map[string]any{
	"x": 0.01,
})

// Send a number instead of an object.
custom := param.Override[xtwitterscraper.FooParams](12)

Request Unions

Each union variant uses an Of-prefixed field. Set only one field. The nonzero field is serialized. Getter methods return mutable pointers when present.

// Set only one variant.
type AnimalUnionParam struct {
	OfCat *Cat `json:",omitzero,inline`
	OfDog *Dog `json:",omitzero,inline`
}

animal := AnimalUnionParam{
	OfCat: &Cat{
		Name: "Whiskers",
		Owner: PersonParam{
			Address: AddressParam{Street: "3333 Coyote Hill Rd", Zip: 0},
		},
	},
}

// Mutate a present field.
if address := animal.GetOwner().GetAddress(); address != nil {
	address.ZipCode = 94304
}

Response Objects

Response fields use value types. The JSON field records decoding metadata.

type Animal struct {
	Name   string `json:"name,nullable"`
	Owners int    `json:"owners"`
	Age    int    `json:"age"`
	JSON   struct {
		Name        respjson.Field
		Owner       respjson.Field
		Age         respjson.Field
		ExtraFields map[string]respjson.Field
	} `json:"-"`
}

.Valid() reports whether a field was present, non-null & decoded successfully. Invalid or omitted fields keep their zero value.

raw := `{"owners": 1, "name": null}`

var res Animal
json.Unmarshal([]byte(raw), &res)

// Values.
res.Owners // 1
res.Name   // ""
res.Age    // 0

// Presence checks.
res.JSON.Owners.Valid() // true
res.JSON.Name.Valid()   // false
res.JSON.Age.Valid()    // false

// Raw JSON.
res.JSON.Owners.Raw()                  // "1"
res.JSON.Name.Raw() == "null"          // true
res.JSON.Name.Raw() == respjson.Null   // true
res.JSON.Age.Raw() == ""               // true
res.JSON.Age.Raw() == respjson.Omitted // true

JSON.ExtraFields retains response properties absent from the generated struct.

body := res.JSON.ExtraFields["my_unexpected_field"].Raw()

Response Unions

Response unions flatten fields from every object variant. Use .AsFooVariant() or .AsAny() to select a variant. Primitive variants use Of-prefixed fields tagged json:"...,inline".

type AnimalUnion struct {
	// From variants [Dog], [Cat]
	Owner Person `json:"owner"`
	// From variant [Dog]
	DogBreed string `json:"dog_breed"`
	// From variant [Cat]
	CatBreed string `json:"cat_breed"`
	// ...

	JSON struct {
		Owner respjson.Field
		// ...
	} `json:"-"`
}

// Validate a shared field.
if animal.Owner.Address.ZipCode == "" {
	panic("missing zip code")
}

// Select the variant.
switch variant := animal.AsAny().(type) {
case Dog:
case Cat:
default:
	panic("unexpected type")
}

RequestOptions

The option package returns RequestOption closures that update RequestConfig. Apply them to the client or an individual request.

client := xtwitterscraper.NewClient(
	// Add a header to every request.
	option.WithHeader("X-Some-Header", "custom_header_info"),
)

client.X.Tweets.Search(context.TODO(), ...,
	// Override the client header.
	option.WithHeader("X-Some-Header", "some_other_custom_header_info"),
	// Add a request field with sjson syntax.
	option.WithJSONSet("some.json.path", map[string]string{"my": "object"}),
)

option.WithDebugLog(nil) writes request and response content to the default logger. Use it only with safe local data.

See the full list of request options.

Pagination

.ListAutoPaging() iterates across every page. .List() fetches one page; call .GetNextPage() for the next.

Errors

Non-2xx responses return *xtwitterscraper.Error. It contains the status, request, response & decoded error body. Use errors.As to inspect those details:

_, err := client.X.Tweets.Search(context.TODO(), xtwitterscraper.XTweetSearchParams{
	Q:     "from:elonmusk",
	Limit: xtwitterscraper.Int(10),
})
if err != nil {
	var apierr *xtwitterscraper.Error
	if errors.As(err, &apierr) {
		fmt.Printf("Request failed with HTTP %d\n", apierr.StatusCode)
	}
	panic(err.Error()) // GET "/x/tweets/search": 400 Bad Request { ... }
}

Other errors remain unwrapped. Transport failures may return *url.Error wrapping *net.OpError.

Timeouts

Requests have no default timeout. Use a context for the full request lifecycle. Retries share that context deadline. Use option.WithRequestTimeout() for each attempt.

// Set one deadline across all attempts.
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute)
defer cancel()
client.X.Tweets.Search(
	ctx,
	xtwitterscraper.XTweetSearchParams{
		Q:     "from:elonmusk",
		Limit: xtwitterscraper.Int(10),
	},
	// Limit each attempt.
	option.WithRequestTimeout(20*time.Second),
)

File Uploads

Multipart file parameters accept io.Reader. The default filename is anonymous_file; the default media type is application/octet-stream. Implement Name() string or ContentType() string to override either value. os.File already supplies its disk filename through Name(). Use xtwitterscraper.File to wrap any reader with explicit metadata.

// Read from disk.
file, err := os.Open("/path/to/file")
xtwitterscraper.XMediaUploadParams{
	Account: "@elonmusk",
	File:    file,
}

// Read from a string.
xtwitterscraper.XMediaUploadParams{
	Account: "@elonmusk",
	File:    strings.NewReader("my file contents"),
}

// Set a filename and media type.
xtwitterscraper.XMediaUploadParams{
	Account: "@elonmusk",
	File:    xtwitterscraper.File(strings.NewReader(`{"hello": "foo"}`), "file.go", "application/json"),
}

Retries

The SDK retries connection errors & HTTP 408, 409, 429, and 5xx responses. It uses exponential backoff with 2 retries by default. Use WithMaxRetries to change or disable retries:

// Set the client default.
client := xtwitterscraper.NewClient(
	option.WithMaxRetries(0), // default is 2
)

// Override one request.
client.X.Tweets.Search(
	context.TODO(),
	xtwitterscraper.XTweetSearchParams{
		Q:     "from:elonmusk",
		Limit: xtwitterscraper.Int(10),
	},
	option.WithMaxRetries(5),
)

Raw Response Data

Use option.WithResponseInto() to inspect response headers and status codes.

var response *http.Response
paginatedTweets, err := client.X.Tweets.Search(
	context.TODO(),
	xtwitterscraper.XTweetSearchParams{
		Q:     "from:elonmusk",
		Limit: xtwitterscraper.Int(10),
	},
	option.WithResponseInto(&response),
)
if err != nil {
	panic(err)
}
fmt.Printf("%+v\n", paginatedTweets)

fmt.Printf("Status Code: %d\n", response.StatusCode)
fmt.Printf("Headers: %+#v\n", response.Header)

Custom Requests

Use client.Get, client.Post, or another verb for undocumented endpoints. These methods retain client options such as retries.

var (
	// Accepts io.Reader, []byte, JSON-compatible values, or SDK params.
	params map[string]any

	// Accepts []byte, *http.Response, JSON-compatible values, or SDK models.
	result *http.Response
)
err := client.Post(context.Background(), "/unspecified", params, &result)
if err != nil {
	panic(err)
}

Use option.WithQuerySet() or option.WithJSONSet() for undocumented parameters.

params := FooNewParams{
	ID: "id_xxxx",
	Data: FooNewParamsData{
		FirstName: xtwitterscraper.String("John"),
	},
}
client.Foo.New(context.Background(), params, option.WithJSONSet("data.last_name", "Doe"))

Use result.JSON.RawJSON() for the full body and .Foo.Raw() for one field. result.JSON.ExtraFields() returns fields absent from the generated response struct.

Middleware

Use option.WithMiddleware to wrap requests.

func Logger(req *http.Request, next option.MiddlewareNext) (res *http.Response, err error) {
	// Before the request.
	start := time.Now()
	LogReq(req)

	// Run the next handler.
	res, err = next(req)

	// After the request.
	LogRes(res, err, time.Since(start))

	return res, err
}

client := xtwitterscraper.NewClient(
	option.WithMiddleware(Logger),
)

Middleware runs left to right. Client middleware runs before request middleware. Use option.WithHTTPClient(client) to replace the current HTTP client. The replacement receives requests after middleware runs.

Semantic Versioning

This package follows SemVer. Before v1.0, minor releases may change undocumented internals. Open an issue before depending on them.

Contributing

Read the contribution guide.

Security & Project Health

Xquik is an independent third-party service. Not affiliated with X Corp. "Twitter" and "X" are trademarks of X Corp.