Skip to main content
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

go get github.com/Xquik-dev/x-twitter-scraper-go

Authenticate

export X_TWITTER_SCRAPER_API_KEY="xq_YOUR_KEY_HERE"
client := xtwitterscraper.NewClient(
	option.WithAPIKey(os.Getenv("X_TWITTER_SCRAPER_API_KEY")),
)

Basic Example

Search tweets and write durable JSON Lines handoff rows:
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:xquikcom 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.
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:xquikcom 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:

Q

Go field Q maps to REST q. Use it for the required X search query with keywords, handles, hashtags, or operators.

Limit

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.

Cursor

Go field Cursor maps to REST cursor. Pass the opaque cursor from NextCursor to request the next page.

SinceTime

Go field SinceTime maps to REST sinceTime. Use it as the ISO 8601 lower time bound.

UntilTime

Go field UntilTime maps to REST untilTime. Use it as the ISO 8601 upper time bound.

QueryType

Go field QueryType maps to REST queryType. Use Latest for chronological results or Top for engagement-ranked results.

Returned Data & Handoff

client.X.Tweets.Search returns PaginatedTweets:

Tweets

JSON field tweets. Contains tweet records with ID, Text, Author, CreatedAt, LikeCount, ReplyCount, RetweetCount, QuoteCount, BookmarkCount, ViewCount, and IsNoteTweet when available.

HasNextPage

Go field HasNextPage. JSON field has_next_page. Tells your worker whether another page exists.

NextCursor

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.
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.
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.
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 := "xquikcom"

	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:
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.
type tweetCreatePayload struct {
	Success        bool   `json:"success"`
	TweetID        string `json:"tweetId"`
	Error          string `json:"error"`
	Status         string `json:"status"`
	WriteActionID  string `json:"writeActionId"`
	Charged        bool   `json:"charged"`
	ChargedCredits string `json:"chargedCredits"`
	Retryable      bool   `json:"retryable"`
}

func createTweetHandoff(payload tweetCreatePayload, base map[string]any) map[string]any {
	handoff := map[string]any{
		"status":          "posted",
		"tweet_id":        payload.TweetID,
		"charged":         payload.Charged,
		"charged_credits": payload.ChargedCredits,
	}

	if payload.Error == "x_write_unconfirmed" && payload.Status == "pending_confirmation" {
		handoff = map[string]any{
			"status":          "pending_confirmation",
			"write_action_id": payload.WriteActionID,
			"charged":         payload.Charged,
			"charged_credits": payload.ChargedCredits,
			"retryable":       payload.Retryable,
			"poll":            "GET /x/write-actions/{id}",
		}
	}

	if payload.WriteActionID != "" {
		handoff["write_action_id"] = payload.WriteActionID
	}

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

	return handoff
}
The generated Go response model covers confirmed TweetID responses. Use option.WithResponseBodyInto when a write worker must branch on the REST 202 x_write_unconfirmed response, store writeActionId and chargedCredits, and poll Get Write Action Status before sending another write.
ctx := context.Background()
var tweetPayload tweetCreatePayload
_, err := client.X.Tweets.New(ctx, xtwitterscraper.XTweetNewParams{
	Account: "@xquikcom",
	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": "@xquikcom",
	"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:
var replyPayload tweetCreatePayload
_, err := client.X.Tweets.New(ctx, xtwitterscraper.XTweetNewParams{
	Account:        "@xquikcom",
	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":           "@xquikcom",
	"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:
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: "@xquikcom",
	File:    file,
})
if err != nil {
	panic(err)
}

dm, err := client.X.Dm.Send(context.Background(), "44196397", xtwitterscraper.XDmSendParams{
	Account:  "@xquikcom",
	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":    "@xquikcom",
	"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 10 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 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.
if tweets.HasNextPage {
	fmt.Println("More results are available")
}

Webhooks & References

Last modified on May 25, 2026