> ## Documentation Index
> Fetch the complete documentation index at: https://docs.xquik.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Go SDK

> Use the Xquik Go SDK to search tweets, export JSON Lines, CSV, or XLSX, post media tweets, upload media, send DMs, and run Go X API workers.

<blockquote className="agent-llms-directive">
  For the complete documentation index, see <a href="/llms.txt">llms.txt</a>.
</blockquote>

Use the Go SDK when you want typed parameters, context-aware requests, generated response models, and Go-native error handling.

Use this page when you need a Go service to search tweets, scrape tweets to JSON Lines, CSV, or XLSX, export follower or profile data, post media tweets, upload media, send DMs, monitor tweets, or hand X data to another system through SDK calls.

## Install

```bash theme={null}
go get github.com/Xquik-dev/x-twitter-scraper-go
```

## Authenticate

```bash theme={null}
export X_TWITTER_SCRAPER_API_KEY="xq_YOUR_KEY_HERE"
```

```go theme={null}
client := xtwitterscraper.NewClient(
	option.WithAPIKey(os.Getenv("X_TWITTER_SCRAPER_API_KEY")),
)
```

## Basic Example

Search tweets and write durable JSON Lines handoff rows:

```go theme={null}
package main

import (
	"context"
	"encoding/json"
	"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")),
	)

	page, err := client.X.Tweets.Search(context.Background(), xtwitterscraper.XTweetSearchParams{
		Q:     "from:username webhook OR SDK",
		Limit: xtwitterscraper.Int(10),
	})
	if err != nil {
		panic(err)
	}

	encoder := json.NewEncoder(os.Stdout)
	for _, tweet := range page.Tweets {
		row := map[string]string{
			"tweet_id":        tweet.ID,
			"text":            tweet.Text,
			"author_username": tweet.Author.Username,
			"created_at":      tweet.CreatedAt,
		}

		if err := encoder.Encode(row); err != nil {
			panic(err)
		}
	}
}
```

## Workflow: Search Tweets to JSON Lines, CSV, or XLSX

This job is for backend workers that need searchable tweet data in a stable handoff format for queues, data lakes, analyst CSV files, or XLSX workbooks. It calls `GET /x/tweets/search` through `client.X.Tweets.Search`, requests the newest matching tweets, and writes each returned tweet as one JSON object per line.

```go theme={null}
package main

import (
	"context"
	"encoding/json"
	"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")),
	)

	out, err := os.Create("xquik-tweet-search.jsonl")
	if err != nil {
		panic(err)
	}
	defer out.Close()

	encoder := json.NewEncoder(out)
	cursor := ""

	for {
		params := xtwitterscraper.XTweetSearchParams{
			Q:         "from:username webhook OR SDK",
			QueryType: xtwitterscraper.XTweetSearchParamsQueryTypeLatest,
		}
		if cursor != "" {
			params.Cursor = xtwitterscraper.String(cursor)
		}

		page, err := client.X.Tweets.Search(context.Background(), params)
		if err != nil {
			panic(err)
		}

		for _, tweet := range page.Tweets {
			if err := encoder.Encode(tweet); err != nil {
				panic(err)
			}
		}

		if !page.HasNextPage || page.NextCursor == "" {
			break
		}
		cursor = page.NextCursor
	}
}
```

The example builds an `XTweetSearchParams` request so the Go fields stay aligned with `GET /x/tweets/search`. The generated params map directly to the REST endpoint:

<CardGroup cols={2}>
  <Card title="Q" icon="search">
    Go field `Q` maps to REST `q`. Use it for the required X search query with keywords, handles, hashtags, or operators.
  </Card>

  <Card title="Limit" icon="list">
    Go field `Limit` maps to REST `limit`. Use it as a 1 to 200 upper bound for a bounded pull. If `page.HasNextPage` is true, keep the same `Q`, filters, `QueryType`, and `Limit` when you continue with `page.NextCursor`.
  </Card>

  <Card title="Cursor" icon="arrow-right">
    Go field `Cursor` maps to REST `cursor`. Pass the opaque cursor from `NextCursor` to request the next page.
  </Card>

  <Card title="SinceTime" icon="clock">
    Go field `SinceTime` maps to REST `sinceTime`. Use it as the ISO 8601 lower time bound.
  </Card>

  <Card title="UntilTime" icon="clock">
    Go field `UntilTime` maps to REST `untilTime`. Use it as the ISO 8601 upper time bound.
  </Card>

  <Card title="QueryType" icon="sliders-horizontal">
    Go field `QueryType` maps to REST `queryType`. Use `Latest` for chronological results or `Top` for engagement-ranked results.
  </Card>
</CardGroup>

## Returned Data & Handoff

`client.X.Tweets.Search` returns `PaginatedTweets`:

<CardGroup cols={2}>
  <Card title="Tweets" icon="message-square-text">
    JSON field `tweets`. Contains tweet records with `ID`, `Text`, `Author`, `CreatedAt`, `LikeCount`, `ReplyCount`, `RetweetCount`, `QuoteCount`, `BookmarkCount`, `ViewCount`, and `IsNoteTweet` when available.
  </Card>

  <Card title="HasNextPage" icon="circle-check">
    Go field `HasNextPage`. JSON field `has_next_page`. Tells your worker whether another page exists.
  </Card>

  <Card title="NextCursor" icon="arrow-right">
    JSON field `next_cursor`. Store it only when `page.HasNextPage` is true. For bounded pulls that return fewer tweets than `Limit`, pass it back as `Cursor` with the same query, filters, `QueryType`, and `Limit`.
  </Card>
</CardGroup>

Write `Tweets` as JSON Lines to `xquik-tweet-search.jsonl` for queues and data lakes, transform the projected records into CSV for analysts, or produce XLSX from those rows when account teams need spreadsheets. Pass `ID`, `Text`, `Author.Username`, `CreatedAt`, and engagement counts into your CRM or enrichment pipeline. For explicit `Limit` pulls, resume with the same query, filters, `QueryType`, and `Limit`; only `Cursor` changes.

## Workflow: Follower Export to CSV, JSON, or XLSX

Use this workflow when a Go worker needs an owned follower list for a CRM import, warehouse load, analyst CSV file, XLSX workbook, or resumable JSON handoff. It calls `POST /extractions/estimate` through `client.Extractions.EstimateCost`, creates the job with `client.Extractions.Run`, reads saved rows with `client.Extractions.Get`, and downloads files with `client.Extractions.ExportResults`.

<Info>
  `client.Extractions.Run` returns the queued `202 Accepted` receipt from `POST /extractions`: REST `id`, `toolType`, and `status: "running"` as Go `job.ID`, `job.ToolType`, and `job.Status`. Store `job.ID` immediately, then poll `client.Extractions.Get` before reading pages or calling `client.Extractions.ExportResults`. Credit reservation happens after the job starts. If available credits changed since `EstimateCost`, the run can fetch only the affordable count before export or mark the job `failed` with `insufficient_credits`.
</Info>

```go theme={null}
package main

import (
	"context"
	"encoding/json"
	"io"
	"os"
	"time"

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

func main() {
	ctx := context.Background()
	client := xtwitterscraper.NewClient(
		option.WithAPIKey(os.Getenv("X_TWITTER_SCRAPER_API_KEY")),
	)
	targetUsername := "username"

	estimate, err := client.Extractions.EstimateCost(ctx, xtwitterscraper.ExtractionEstimateCostParams{
		ToolType:       xtwitterscraper.ExtractionEstimateCostParamsToolTypeFollowerExplorer,
		TargetUsername: xtwitterscraper.String(targetUsername),
	})
	if err != nil {
		panic(err)
	}
	if !estimate.Allowed {
		panic("insufficient credits for follower export")
	}

	job, err := client.Extractions.Run(ctx, xtwitterscraper.ExtractionRunParams{
		ToolType:       xtwitterscraper.ExtractionRunParamsToolTypeFollowerExplorer,
		TargetUsername: xtwitterscraper.String(targetUsername),
	})
	if err != nil {
		panic(err)
	}

	for {
		page, err := client.Extractions.Get(ctx, job.ID, xtwitterscraper.ExtractionGetParams{
			Limit: xtwitterscraper.Int(1000),
		})
		if err != nil {
			panic(err)
		}

		status, _ := page.Job["status"].(string)
		if status == "completed" {
			break
		}
		if status == "failed" {
			panic("follower export failed")
		}

		time.Sleep(10 * time.Second)
	}

	writeRows(ctx, client, job.ID, "xquik-followers.jsonl")
	writeExport(ctx, client, job.ID, xtwitterscraper.ExtractionExportResultsParamsFormatCsv, "xquik-followers.csv")
	writeExport(ctx, client, job.ID, xtwitterscraper.ExtractionExportResultsParamsFormatJson, "xquik-followers.json")
	writeExport(ctx, client, job.ID, xtwitterscraper.ExtractionExportResultsParamsFormatXlsx, "xquik-followers.xlsx")
}

func writeRows(
	ctx context.Context,
	client xtwitterscraper.Client,
	jobID string,
	filename string,
) {
	out, err := os.Create(filename)
	if err != nil {
		panic(err)
	}
	defer out.Close()

	encoder := json.NewEncoder(out)
	cursor := ""

	for {
		params := xtwitterscraper.ExtractionGetParams{
			Limit: xtwitterscraper.Int(1000),
		}
		if cursor != "" {
			params.After = xtwitterscraper.String(cursor)
		}

		page, err := client.Extractions.Get(ctx, jobID, params)
		if err != nil {
			panic(err)
		}

		for _, row := range page.Results {
			if err := encoder.Encode(row); err != nil {
				panic(err)
			}
		}

		if !page.HasMore || page.NextCursor == "" {
			break
		}
		cursor = page.NextCursor
	}
}

func writeExport(
	ctx context.Context,
	client xtwitterscraper.Client,
	jobID string,
	format xtwitterscraper.ExtractionExportResultsParamsFormat,
	filename string,
) {
	response, err := client.Extractions.ExportResults(ctx, jobID, xtwitterscraper.ExtractionExportResultsParams{
		Format: format,
	})
	if err != nil {
		panic(err)
	}
	defer response.Body.Close()

	out, err := os.Create(filename)
	if err != nil {
		panic(err)
	}
	defer out.Close()

	if _, err := io.Copy(out, response.Body); err != nil {
		panic(err)
	}
}
```

`follower_explorer` requires `TargetUsername`. Persist `job.ID`, `targetUsername`, `estimate.EstimatedResults`, and `estimate.Source` before polling so a worker restart can resume the same follower export. `client.Extractions.Get` returns `Results`, `HasMore`, and `NextCursor`; pass `NextCursor` back as `After` when you need stored JSON pages before exporting files. Use `xquik-followers.jsonl` for queue replay or warehouse loads, `xquik-followers.json` for app ingestion, `xquik-followers.csv` for CRM import, and `xquik-followers.xlsx` for analyst handoff. Map exported `User ID` or row `xUserId` as the CRM unique key. Cost: 1 credit per follower extracted or returned. Exports are free after the extraction job exists.

## Workflow: Tweet Replies to CSV, JSON, or XLSX

Use this workflow when a Go worker, queue consumer, or agent service needs every reply under one tweet as a saved extraction, JSON Lines handoff, or CSV/JSON/XLSX file export. It calls `POST /extractions/estimate` through `client.Extractions.EstimateCost`, creates the job with `client.Extractions.Run`, reads rows with `client.Extractions.Get`, and downloads files with `client.Extractions.ExportResults`.

Reuse the polling, `writeRows`, and `writeExport` helpers from the follower workflow. Only the tool type, target field, and output filenames change:

```go theme={null}
targetTweetID := "1893704267862470862"

estimate, err := client.Extractions.EstimateCost(ctx, xtwitterscraper.ExtractionEstimateCostParams{
	ToolType:      xtwitterscraper.ExtractionEstimateCostParamsToolTypeReplyExtractor,
	TargetTweetID: xtwitterscraper.String(targetTweetID),
})
if err != nil {
	panic(err)
}
if !estimate.Allowed {
	panic("insufficient credits for reply extraction")
}

job, err := client.Extractions.Run(ctx, xtwitterscraper.ExtractionRunParams{
	ToolType:      xtwitterscraper.ExtractionRunParamsToolTypeReplyExtractor,
	TargetTweetID: xtwitterscraper.String(targetTweetID),
})
if err != nil {
	panic(err)
}

// Run the same polling loop from the follower workflow before exporting rows.
writeRows(ctx, client, job.ID, "xquik-replies.jsonl")
writeExport(ctx, client, job.ID, xtwitterscraper.ExtractionExportResultsParamsFormatCsv, "xquik-replies.csv")
writeExport(ctx, client, job.ID, xtwitterscraper.ExtractionExportResultsParamsFormatJson, "xquik-replies.json")
writeExport(ctx, client, job.ID, xtwitterscraper.ExtractionExportResultsParamsFormatXlsx, "xquik-replies.xlsx")
```

`reply_extractor` requires `TargetTweetID`. `client.Extractions.Get` returns `Results`, `HasMore`, and `NextCursor`; the shared `writeRows` helper passes `NextCursor` back as `After` until all stored rows are written. Use `xquik-replies.jsonl` for queue replay or warehouse loads, `xquik-replies.json` for app ingestion, `xquik-replies.csv` for CRM import, and `xquik-replies.xlsx` for analyst handoff. `client.Extractions.ExportResults` supports CSV, JSON, and XLSX for file handoff. Cost: 1 credit per reply extracted or returned.

## Workflow: Post Media Tweets and DM Attachments

Use this workflow when a Go worker, queue consumer, or agent service needs to post a media-backed tweet, reply with media, or send one uploaded media item in a DM.

Tweet and reply media posts use public media URLs directly on `client.X.Tweets.New`. Send up to 4 image URLs or exactly 1 MP4 video URL up to 100 MB. Do not mix video with other media. Do not upload first when the media URL is already public.

```go theme={null}
type writeActionBilling struct {
	Charged        bool   `json:"charged"`
	ChargedCredits string `json:"chargedCredits"`
}

type writeActionRequest struct {
	Hash *string `json:"hash"`
}

type writeActionResult struct {
	ID string `json:"id"`
}

type tweetCreatePayload struct {
	ID          string             `json:"id"`
	Status      string             `json:"status"`
	Terminal    bool               `json:"terminal"`
	SafeToRetry bool               `json:"safeToRetry"`
	StatusURL   string             `json:"statusUrl"`
	Request     writeActionRequest `json:"request"`
	Billing     writeActionBilling `json:"billing"`
	Result      *writeActionResult `json:"result"`
	TweetID     string             `json:"tweetId"`
}

func createTweetHandoff(action tweetCreatePayload, base map[string]any) map[string]any {
	var resultID any
	if action.Result != nil {
		resultID = action.Result.ID
	} else if action.TweetID != "" {
		resultID = action.TweetID
	}
	poll := any(nil)
	if !action.Terminal {
		poll = action.StatusURL
	}
	handoff := map[string]any{
		"status":          action.Status,
		"terminal":        action.Terminal,
		"safe_to_retry":   action.SafeToRetry,
		"write_action_id": action.ID,
		"request_hash":    action.Request.Hash,
		"tweet_id":        resultID,
		"charged":         action.Billing.Charged,
		"charged_credits": action.Billing.ChargedCredits,
		"poll":            poll,
	}

	for key, value := range base {
		handoff[key] = value
	}

	return handoff
}
```

Use `option.WithResponseBodyInto` to capture the durable write action. Send a unique `Idempotency-Key`, store `id`, `request.hash`, `billing`, `result`, and `statusUrl`, then poll while `terminal` is false. Retry only when `safeToRetry` is true, using a new key.

```go theme={null}
ctx := context.Background()
var tweetPayload tweetCreatePayload
_, err := client.X.Tweets.New(ctx, xtwitterscraper.XTweetNewParams{
	Account: "@username",
	Text:    xtwitterscraper.String("New demo video is live."),
	Media:   []string{"https://example.com/product-demo.mp4"},
}, option.WithResponseBodyInto(&tweetPayload))
if err != nil {
	panic(err)
}

tweetHandoff := createTweetHandoff(tweetPayload, map[string]any{
	"account": "@username",
	"media":   []string{"https://example.com/product-demo.mp4"},
})
if err := json.NewEncoder(os.Stdout).Encode(tweetHandoff); err != nil {
	panic(err)
}
```

To post an image reply, add `ReplyToTweetID`:

```go theme={null}
var replyPayload tweetCreatePayload
_, err := client.X.Tweets.New(ctx, xtwitterscraper.XTweetNewParams{
	Account:        "@username",
	Text:           xtwitterscraper.String("Here is the requested screenshot."),
	ReplyToTweetID: xtwitterscraper.String("1893704267862470862"),
	Media:          []string{"https://example.com/export-preview.png"},
}, option.WithResponseBodyInto(&replyPayload))
if err != nil {
	panic(err)
}

replyHandoff := createTweetHandoff(replyPayload, map[string]any{
	"account":           "@username",
	"reply_to_tweet_id": "1893704267862470862",
	"media":             []string{"https://example.com/export-preview.png"},
})
if err := json.NewEncoder(os.Stdout).Encode(replyHandoff); err != nil {
	panic(err)
}
```

For DM attachments, upload the local file first and pass the returned `media.MediaID` as the only `MediaIDs` item:

```go theme={null}
file, err := os.Open("./handoff.png")
if err != nil {
	panic(err)
}
defer file.Close()

media, err := client.X.Media.Upload(context.Background(), xtwitterscraper.XMediaUploadParams{
	Account: "@username",
	File:    file,
})
if err != nil {
	panic(err)
}

dm, err := client.X.Dm.Send(context.Background(), "44196397", xtwitterscraper.XDmSendParams{
	Account:  "@username",
	Text:     "Here is the asset.",
	MediaIDs: []string{media.MediaID},
})
if err != nil {
	panic(err)
}

dmHandoff := map[string]any{
	"status":     "sent",
	"message_id": dm.MessageID,
	"media_id":   media.MediaID,
	"account":    "@username",
	"user_id":    "44196397",
}
if err := json.NewEncoder(os.Stdout).Encode(dmHandoff); err != nil {
	panic(err)
}
```

`client.X.Tweets.New` returns `TweetID` for confirmed posts. Raw create responses can also include the pending write fields above when confirmation is still running. `client.X.Media.Upload` returns `media.MediaID` for DM attachments, and `client.X.Dm.Send` returns `dm.MessageID` for support tickets, CRM records, queue jobs, or agent memory.

Keep DM body text in private systems. Shared logs, public artifacts, queue status, and agent handoffs should store `message_id`, optional `media_id`, `account`, `user_id`, and send status instead of full DM bodies. Leave `ReplyToMessageID` unset even if generated SDK params expose it; the REST endpoint rejects DM reply threading.

Text-only tweet and reply writes cost 30 credits. Tweet media adds 2 credits per started MB across attached files. Uploading media costs 10 credits, and sending the DM costs 10 credits. Do not pass uploaded `MediaID` values to `client.X.Tweets.New`; that method uses `Media` with public media URLs.

## Cost, Limits & Retries

Tweet search costs 1 credit per tweet returned. If remaining credits cannot cover the requested page, the API can return fewer tweets than `Limit`; if 0 paid results are affordable, it returns `402 insufficient_credits`. Read calls are rate-limited, and `429` responses include `Retry-After`.

Generated Go methods return `error` for connection failures and non-success responses. Retry connection errors, 408, 409, 429, and 5xx responses with backoff. Do not retry 400, 401, 403, 404, or 422 until the request, authentication, permission, or input issue is fixed.

## Error Handling

Generated Go methods return `error` for connection failures and non-success API responses. Check the concrete error type from the SDK when you need status-code specific handling, and use the [error handling guide](/guides/error-handling) for response semantics.

Common retryable cases are connection errors, 408, 409, 429, and 5xx responses.

## Pagination

List and search responses expose generated pagination fields such as `HasNextPage`. Pass cursor parameters from the previous response when the endpoint supports cursor pagination.

```go theme={null}
if tweets.HasNextPage {
	fmt.Println("More results are available")
}
```

## Webhooks & References

* [Search Tweets](/api-reference/x/search-tweets)
* [Create Tweet](/api-reference/x-write/create-tweet)
* [Get Write Action Status](/api-reference/x-write/get-write-action-status)
* [Upload Media](/api-reference/x-write/upload-media)
* [Send Direct Message](/api-reference/x-write/send-dm)
* [Webhook Overview](/webhooks/overview)
* [Verify HMAC Signatures](/webhooks/verification)
* [REST API Overview](/api-reference/overview)
* [OpenAPI JSON](https://xquik.com/openapi.json)
* [Source Repository](https://github.com/Xquik-dev/x-twitter-scraper-go)
