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 C# SDK for typed .NET Standard 2.0+ access to Xquik from services, workers, console tools, and ASP.NET backends. It is useful when a .NET job needs to search tweets, scrape tweets or replies to JSON Lines, CSV, or XLSX, export followers, monitor tweets, post media tweets, upload media, send direct messages, or hand X API data to queues, CRMs, and reporting systems.

Install

dotnet add package XTwitterScraper

Authenticate

export X_TWITTER_SCRAPER_API_KEY="xq_YOUR_KEY_HERE"
new XTwitterScraperClient() reads X_TWITTER_SCRAPER_API_KEY, X_TWITTER_SCRAPER_BEARER_TOKEN, and X_TWITTER_SCRAPER_BASE_URL.

Basic Example

Search tweets and write durable JSON Lines handoff rows:
using System.Text.Json;
using XTwitterScraper;
using XTwitterScraper.Models;
using XTwitterScraper.Models.X.Tweets;

XTwitterScraperClient client = new();

TweetSearchParams parameters = new()
{
    Q = "from:xquikcom webhook OR SDK",
    Limit = 10,
};

PaginatedTweets page = await client.X.Tweets.Search(parameters);

foreach (SearchTweet tweet in page.Tweets)
{
    var row = new
    {
        tweet_id = tweet.ID,
        text = tweet.Text,
        author_username = tweet.Author?.Username,
        created_at = tweet.CreatedAt,
    };

    await Console.Out.WriteLineAsync(JsonSerializer.Serialize(row));
}

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

Use this workflow when a .NET worker, console job, or ASP.NET background service needs tweet search results in a durable handoff format for a queue, data lake, analyst CSV export, XLSX workbook, or CRM enrichment step. client.X.Tweets.Search calls GET /x/tweets/search. Build a TweetSearchParams object with the same query parameters the REST API accepts: Q, Limit, Cursor, SinceTime, UntilTime, and QueryType.
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text.Json;
using XTwitterScraper;
using XTwitterScraper.Models;
using XTwitterScraper.Models.X.Tweets;

XTwitterScraperClient client = new();

string query = "from:xquikcom webhook OR SDK";
string? cursor = null;
int pageIndex = 0;
string[] headers = new[]
{
    "source",
    "query",
    "tweet_id",
    "text",
    "author_id",
    "author_username",
    "author_name",
    "created_at",
    "like_count",
    "reply_count",
    "retweet_count",
    "quote_count",
    "view_count",
    "bookmark_count",
    "is_note_tweet",
    "page_index",
    "page_cursor",
    "next_cursor",
    "has_next_page",
};

using StreamWriter jsonlWriter = File.CreateText("xquik-tweet-search.jsonl");
using StreamWriter csvWriter = File.CreateText("xquik-tweet-search.csv");
await WriteCsvRow(csvWriter, headers);

do
{
    string? pageCursor = cursor;
    PaginatedTweets page = await client.X.Tweets.Search(
        new TweetSearchParams
        {
            Q = query,
            Cursor = cursor,
            QueryType = QueryType.Latest,
        }
    );

    foreach (SearchTweet tweet in page.Tweets)
    {
        Dictionary<string, object?> row = new()
        {
            ["source"] = "xquik.csharp.search",
            ["query"] = query,
            ["tweet_id"] = tweet.ID,
            ["text"] = tweet.Text,
            ["author_id"] = tweet.Author?.ID,
            ["author_username"] = tweet.Author?.Username,
            ["author_name"] = tweet.Author?.Name,
            ["created_at"] = tweet.CreatedAt,
            ["like_count"] = tweet.LikeCount ?? 0,
            ["reply_count"] = tweet.ReplyCount ?? 0,
            ["retweet_count"] = tweet.RetweetCount ?? 0,
            ["quote_count"] = tweet.QuoteCount ?? 0,
            ["view_count"] = tweet.ViewCount ?? 0,
            ["bookmark_count"] = tweet.BookmarkCount ?? 0,
            ["is_note_tweet"] = tweet.IsNoteTweet ?? false,
            ["page_index"] = pageIndex,
            ["page_cursor"] = pageCursor,
            ["next_cursor"] = page.HasNextPage ? page.NextCursor : null,
            ["has_next_page"] = page.HasNextPage,
        };

        await jsonlWriter.WriteLineAsync(JsonSerializer.Serialize(row));
        await WriteCsvRow(csvWriter, headers.Select(header => row[header]));
    }

    cursor = page.HasNextPage ? page.NextCursor : null;
    pageIndex++;
} while (!string.IsNullOrEmpty(cursor));

static async Task WriteCsvRow(StreamWriter writer, IEnumerable<object?> values)
{
    static string Escape(object? value)
    {
        string cell = value?.ToString() ?? "";
        return "\"" + cell.Replace("\"", "\"\"") + "\"";
    }

    await writer.WriteLineAsync(string.Join(",", values.Select(Escape)));
}

Request Mapping

Q

C# property Q maps to REST q. Use it for an X search query such as from:username, a keyword, hashtag, or boolean operator query.

Limit

C# property Limit maps to REST limit. Use it for a bounded request from 1 to 200. Omit it for cursor loops.

Cursor

C# property Cursor maps to REST cursor. Pass the opaque cursor from page.NextCursor to request the next page.

SinceTime

C# property SinceTime maps to REST sinceTime. Use it as the ISO 8601 lower bound for tweet creation time.

UntilTime

C# property UntilTime maps to REST untilTime. Use it as the ISO 8601 upper bound for tweet creation time.

QueryType

C# property QueryType maps to REST queryType. Use QueryType.Latest for chronological search or QueryType.Top for engagement-ranked search.

Returned Data & Handoff

client.X.Tweets.Search returns PaginatedTweets. Use page.Tweets for the tweet array, page.HasNextPage to decide whether another page exists, and page.NextCursor as the checkpoint for the next request. Each SearchTweet includes typed properties such as ID, Text, Author, CreatedAt, LikeCount, ReplyCount, RetweetCount, QuoteCount, ViewCount, BookmarkCount, and IsNoteTweet when available. Project page.Tweets into JSON Lines and CSV rows with tweet_id, author_username, engagement counts, page_index, page_cursor, next_cursor, and has_next_page so workers can resume safely or load the same records into XLSX, CRM, warehouse, or agent workflows. Tweet search costs 1 credit per tweet returned. If remaining credits cannot cover a bounded Limit request, the API can return fewer tweets; if 0 paid results are affordable, it returns 402 insufficient_credits. The SDK retries connection errors, 408, 409, 429, and 5xx responses 2 times by default. For longer workers, set MaxRetries on the client and persist NextCursor after each page.

Workflow: Follower Export to CSV, JSON, or XLSX

Use this workflow when a .NET worker, console job, or ASP.NET background service needs an owned follower list for a CRM import, warehouse load, analyst CSV file, XLSX workbook, or resumable JSON handoff. client.Extractions.EstimateCost and client.Extractions.Run map to the extraction workflow. For follower exports, use ExtractionEstimateCostParamsToolType.FollowerExplorer and ExtractionRunParamsToolType.FollowerExplorer; follower_explorer requires TargetUsername. client.Extractions.Retrieve returns Results, HasMore, and NextCursor for pagination. client.Extractions.ExportResults returns an HttpResponse; read CSV and JSON as strings, and copy XLSX from the response stream.
using System;
using System.IO;
using System.Text.Json;
using System.Threading.Tasks;
using XTwitterScraper;
using XTwitterScraper.Core;
using XTwitterScraper.Models.Extractions;

XTwitterScraperClient client = new();

string targetUsername = "xquikcom";
ExtractionEstimateCostResponse estimate = await client.Extractions.EstimateCost(
    new ExtractionEstimateCostParams
    {
        ToolType = ExtractionEstimateCostParamsToolType.FollowerExplorer,
        TargetUsername = targetUsername,
    }
);

if (!estimate.Allowed)
{
    throw new InvalidOperationException("Insufficient credits for follower export.");
}

ExtractionRunResponse job = await client.Extractions.Run(
    new ExtractionRunParams
    {
        ToolType = ExtractionRunParamsToolType.FollowerExplorer,
        TargetUsername = targetUsername,
    }
);

while (true)
{
    ExtractionRetrieveResponse statusPage = await client.Extractions.Retrieve(
        job.ID,
        new ExtractionRetrieveParams { Limit = 1 }
    );

    string? status = statusPage.Job.TryGetValue("status", out JsonElement statusValue)
        ? statusValue.GetString()
        : null;

    if (status == "completed")
    {
        break;
    }

    if (status == "failed")
    {
        throw new InvalidOperationException("Follower export failed.");
    }

    await Task.Delay(TimeSpan.FromSeconds(10));
}

string? after = null;
using StreamWriter writer = File.CreateText("xquik-followers.jsonl");

do
{
    ExtractionRetrieveResponse page = await client.Extractions.Retrieve(
        job.ID,
        new ExtractionRetrieveParams { After = after, Limit = 1000 }
    );

    foreach (var follower in page.Results)
    {
        await writer.WriteLineAsync(JsonSerializer.Serialize(follower));
    }

    after = page.HasMore ? page.NextCursor : null;
} while (!string.IsNullOrEmpty(after));

using HttpResponse csvResponse = await client.Extractions.ExportResults(
    job.ID,
    new ExtractionExportResultsParams { Format = Format.Csv }
);
await File.WriteAllTextAsync("xquik-followers.csv", await csvResponse.ReadAsString());

using HttpResponse jsonResponse = await client.Extractions.ExportResults(
    job.ID,
    new ExtractionExportResultsParams { Format = Format.Json }
);
await File.WriteAllTextAsync("xquik-followers.json", await jsonResponse.ReadAsString());

using HttpResponse xlsxResponse = await client.Extractions.ExportResults(
    job.ID,
    new ExtractionExportResultsParams { Format = Format.Xlsx }
);
using Stream xlsxStream = await xlsxResponse.ReadAsStream();
using FileStream xlsxFile = File.Create("xquik-followers.xlsx");
await xlsxStream.CopyToAsync(xlsxFile);
Cost: 1 credit per follower extracted or returned. Persist job.ID, targetUsername, estimate.EstimatedResults, and estimate.Source before polling so a queue retry, Windows service restart, or worker restart can resume the same follower export. Keep page.NextCursor as the checkpoint when you stream followers to JSON Lines, and map exported User ID or row xUserId as the CRM unique key. 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. Exports are free after the extraction job exists.

Workflow: Tweet Replies to CSV, JSON, or XLSX

Use this workflow when a .NET worker needs every reply on a public tweet in a durable file for moderation review, customer support triage, analyst export, or a warehouse loader. client.Extractions.EstimateCost and client.Extractions.Run map to the extraction workflow. For tweet replies, use ExtractionEstimateCostParamsToolType.ReplyExtractor and ExtractionRunParamsToolType.ReplyExtractor; reply_extractor requires TargetTweetID. client.Extractions.Retrieve returns Results, HasMore, and NextCursor for pagination. client.Extractions.ExportResults returns an HttpResponse; read CSV and JSON as strings, and copy XLSX from the response stream.
using System;
using System.IO;
using System.Text.Json;
using System.Threading.Tasks;
using XTwitterScraper;
using XTwitterScraper.Core;
using XTwitterScraper.Models.Extractions;

XTwitterScraperClient client = new();

string targetTweetID = "1893704267862470862";
ExtractionEstimateCostResponse estimate = await client.Extractions.EstimateCost(
    new ExtractionEstimateCostParams
    {
        ToolType = ExtractionEstimateCostParamsToolType.ReplyExtractor,
        TargetTweetID = targetTweetID,
    }
);

if (!estimate.Allowed)
{
    throw new InvalidOperationException("Insufficient credits for reply extraction.");
}

ExtractionRunResponse job = await client.Extractions.Run(
    new ExtractionRunParams
    {
        ToolType = ExtractionRunParamsToolType.ReplyExtractor,
        TargetTweetID = targetTweetID,
    }
);

while (true)
{
    ExtractionRetrieveResponse statusPage = await client.Extractions.Retrieve(
        job.ID,
        new ExtractionRetrieveParams { Limit = 1 }
    );

    string? status = statusPage.Job.TryGetValue("status", out JsonElement statusValue)
        ? statusValue.GetString()
        : null;

    if (status == "completed")
    {
        break;
    }

    if (status == "failed")
    {
        throw new InvalidOperationException("Reply extraction failed.");
    }

    await Task.Delay(TimeSpan.FromSeconds(3));
}

string? after = null;
using StreamWriter writer = File.CreateText("xquik-replies.jsonl");

do
{
    ExtractionRetrieveResponse page = await client.Extractions.Retrieve(
        job.ID,
        new ExtractionRetrieveParams { After = after, Limit = 1000 }
    );

    foreach (var reply in page.Results)
    {
        await writer.WriteLineAsync(JsonSerializer.Serialize(reply));
    }

    after = page.HasMore ? page.NextCursor : null;
} while (!string.IsNullOrEmpty(after));

using HttpResponse csvResponse = await client.Extractions.ExportResults(
    job.ID,
    new ExtractionExportResultsParams { Format = Format.Csv }
);
await File.WriteAllTextAsync("xquik-replies.csv", await csvResponse.ReadAsString());

using HttpResponse jsonResponse = await client.Extractions.ExportResults(
    job.ID,
    new ExtractionExportResultsParams { Format = Format.Json }
);
await File.WriteAllTextAsync("xquik-replies.json", await jsonResponse.ReadAsString());

using HttpResponse xlsxResponse = await client.Extractions.ExportResults(
    job.ID,
    new ExtractionExportResultsParams { Format = Format.Xlsx }
);
using Stream xlsxStream = await xlsxResponse.ReadAsStream();
using FileStream xlsxFile = File.Create("xquik-replies.xlsx");
await xlsxStream.CopyToAsync(xlsxFile);
Cost: 1 credit per reply extracted or returned. Store job.ID on the queue job, ticket, or warehouse batch before polling so another worker can resume with client.Extractions.Retrieve. Keep page.NextCursor as the checkpoint when you stream replies to JSON Lines. 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.

Workflow: Post Media Tweets and DM Attachments

Use this workflow when a .NET worker needs to publish a media tweet, reply with media, or send a direct message with an uploaded local file. client.X.Tweets.Create maps to POST /x/tweets; pass public media URLs through Media. Send up to 4 image URLs or exactly 1 MP4 video URL up to 100 MB, and do not mix video with other media. For replies, set ReplyToTweetID to the parent tweet ID. client.X.Media.Upload maps to POST /x/media; use media.MediaID only for the one-item MediaIds handoff on client.X.Dm.Send.
using System;
using System.Collections.Generic;
using System.IO;
using System.Net;
using System.Text.Json;
using System.Threading.Tasks;
using XTwitterScraper;
using XTwitterScraper.Core;
using XTwitterScraper.Models.X.Dm;
using XTwitterScraper.Models.X.Media;
using XTwitterScraper.Models.X.Tweets;

XTwitterScraperClient client = new();
string parentTweetID = "1893704267862470862";

using HttpResponse<TweetCreateResponse> tweetResponse =
    await client.X.Tweets.WithRawResponse.Create(
        new TweetCreateParams
        {
            Account = "@xquikcom",
            Text = "Shipping the weekly X API video.",
            Media = new[] { "https://static.example.com/reports/x-api-export.mp4" },
        }
    );

Dictionary<string, object?> tweetHandoff = await CreateTweetHandoff(
    tweetResponse,
    new Dictionary<string, object?>
    {
        ["account"] = "@xquikcom",
        ["media_url"] = "https://static.example.com/reports/x-api-export.mp4",
    }
);

using HttpResponse<TweetCreateResponse> replyResponse =
    await client.X.Tweets.WithRawResponse.Create(
        new TweetCreateParams
        {
            Account = "@xquikcom",
            Text = "Here is the chart behind the update.",
            Media = new[] { "https://static.example.com/reports/reply-chart.png" },
            ReplyToTweetID = parentTweetID,
        }
    );

Dictionary<string, object?> replyHandoff = await CreateTweetHandoff(
    replyResponse,
    new Dictionary<string, object?>
    {
        ["account"] = "@xquikcom",
        ["reply_to_tweet_id"] = parentTweetID,
        ["media_url"] = "https://static.example.com/reports/reply-chart.png",
    }
);

await using FileStream localFile = File.OpenRead("handoff.png");
MediaUploadResponse media = await client.X.Media.Upload(
    new MediaUploadParams
    {
        Account = "@xquikcom",
        File = localFile,
    }
);

DmSendResponse dm = await client.X.Dm.Send(
    "44196397",
    new DmSendParams
    {
        Account = "@xquikcom",
        Text = "Here is the requested asset.",
        MediaIds = new[] { media.MediaID },
    }
);

Dictionary<string, object?> dmHandoff = new()
{
    ["message_id"] = dm.MessageID,
    ["media_id"] = media.MediaID,
    ["user_id"] = "44196397",
    ["account"] = "@xquikcom",
};

await Console.Out.WriteLineAsync(JsonSerializer.Serialize(tweetHandoff));
await Console.Out.WriteLineAsync(JsonSerializer.Serialize(replyHandoff));
await Console.Out.WriteLineAsync(JsonSerializer.Serialize(dmHandoff));

static async Task<Dictionary<string, object?>> CreateTweetHandoff(
    HttpResponse<TweetCreateResponse> response,
    Dictionary<string, object?> row
)
{
    string body = await response.ReadAsString();
    using JsonDocument document = JsonDocument.Parse(body);
    JsonElement payload = document.RootElement;

    if (
        response.StatusCode == HttpStatusCode.Accepted &&
        payload.TryGetProperty("error", out JsonElement error) &&
        error.GetString() == "x_write_unconfirmed" &&
        payload.TryGetProperty("status", out JsonElement status) &&
        status.GetString() == "pending_confirmation"
    )
    {
        row["status"] = "pending_confirmation";
        row["write_action_id"] = payload.GetProperty("writeActionId").GetString();
        row["charged"] = payload.GetProperty("charged").GetBoolean();
        row["charged_credits"] = payload.GetProperty("chargedCredits").GetString();
        row["retryable"] = payload.GetProperty("retryable").GetBoolean();
        row["poll"] = "GET /x/write-actions/{id}";
        return row;
    }

    TweetCreateResponse tweet =
        JsonSerializer.Deserialize<TweetCreateResponse>(body)
        ?? throw new InvalidOperationException("Tweet response body was empty.");

    row["status"] = "sent";
    row["tweet_id"] = tweet.TweetID;
    row["charged"] = payload.GetProperty("charged").GetBoolean();
    row["charged_credits"] = payload.GetProperty("chargedCredits").GetString();
    if (payload.TryGetProperty("writeActionId", out JsonElement writeActionId))
    {
        row["write_action_id"] = writeActionId.GetString();
    }
    return row;
}
Use client.X.Tweets.WithRawResponse.Create when a write worker must branch on the REST 202 x_write_unconfirmed response. Store chargedCredits as charged_credits for both confirmed and pending responses. If the API returns pending confirmation, store writeActionId as write_action_id and poll Get Write Action Status before scheduling follow-up work. Do not retry-send the same write while status is pending. Store confirmed tweet_id values on the CMS, queue job, or agent state, and store dm.MessageID plus media.MediaID on the support ticket or CRM note. 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 media.MediaID values to client.X.Tweets.Create; that method uses Media with public media URLs.

Error Handling

The C# SDK throws generated exception types for non-success responses and connection failures.

400 Bad Request

Throws XTwitterScraperBadRequestException.

401 Unauthorized

Throws XTwitterScraperUnauthorizedException.

403 Forbidden

Throws XTwitterScraperForbiddenException.

404 Not Found

Throws XTwitterScraperNotFoundException.

422 Validation Error

Throws XTwitterScraperUnprocessableEntityException.

429 Rate Limited

Throws XTwitterScraperRateLimitException.

5xx Server Error

Throws XTwitterScraper5xxException.
Use the error handling guide for response body fields and retry recommendations.

Pagination

Paginated responses expose generated fields such as HasNextPage. Pass cursor parameters from the previous response when an endpoint supports cursor pagination.
if (tweets.HasNextPage)
{
    await Console.Error.WriteLineAsync("More results are available");
}

Webhooks & References

Last modified on May 20, 2026