Skip to main content

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.

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 for a bounded request from 1 to 200. Omit it for cursor loops.

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 with the job checkpoint and pass it back as Cursor.
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.

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.
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.
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")),
	)
	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)
	}

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

		status, _ := statusPage.Job["status"].(string)
		if status == "completed" {
			break
		}
		if status == "failed" {
			panic("reply extraction failed")
		}

		time.Sleep(3 * time.Second)
	}

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

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

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

		page, err := client.Extractions.Get(ctx, job.ID, 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
	}

	writeReplyExport(ctx, client, job.ID, xtwitterscraper.ExtractionExportResultsParamsFormatCsv, "xquik-replies.csv")
	writeReplyExport(ctx, client, job.ID, xtwitterscraper.ExtractionExportResultsParamsFormatJson, "xquik-replies.json")
	writeReplyExport(ctx, client, job.ID, xtwitterscraper.ExtractionExportResultsParamsFormatXlsx, "xquik-replies.xlsx")
}

func writeReplyExport(
	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)
	}
}
reply_extractor requires TargetTweetID. client.Extractions.Get returns Results, HasMore, and NextCursor; store those values when you need resumable JSON pagination. 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. 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 20, 2026