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 Java SDK for generated JVM models, builders, sync calls, async calls, typed exceptions, retries, and file upload helpers in Java 8+ applications. It is useful when a JVM service, Spring worker, queue consumer, or cron job needs to search tweets, scrape tweets to JSON Lines, CSV, or XLSX, export followers, monitor tweets, post media tweets, upload media, send direct messages, or hand X API data to analytics and CRM systems.

Install

Maven Central publication is pending. Build from source until the com.x_twitter_scraper.api:x-twitter-scraper-java artifact resolves in Maven Central.
git clone https://github.com/Xquik-dev/x-twitter-scraper-java.git
cd x-twitter-scraper-java
./gradlew build
For local Maven testing:
./gradlew publishToMavenLocal -PpublishLocal
Before restoring Maven Central install snippets, verify:
curl -f https://repo1.maven.org/maven2/com/x_twitter_scraper/api/x-twitter-scraper-java/maven-metadata.xml

Authenticate

export X_TWITTER_SCRAPER_API_KEY="xq_YOUR_KEY_HERE"
XTwitterScraperOkHttpClient.fromEnv() 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:
import com.fasterxml.jackson.databind.ObjectMapper;
import com.x_twitter_scraper.api.client.XTwitterScraperClient;
import com.x_twitter_scraper.api.client.okhttp.XTwitterScraperOkHttpClient;
import com.x_twitter_scraper.api.models.PaginatedTweets;
import com.x_twitter_scraper.api.models.SearchTweet;
import com.x_twitter_scraper.api.models.x.tweets.TweetSearchParams;
import java.util.LinkedHashMap;
import java.util.Map;

XTwitterScraperClient client = XTwitterScraperOkHttpClient.fromEnv();
ObjectMapper objectMapper = new ObjectMapper();

TweetSearchParams params = TweetSearchParams.builder()
    .q("from:xquikcom webhook OR SDK")
    .limit(10L)
    .build();

PaginatedTweets page = client.x().tweets().search(params);

for (SearchTweet tweet : page.tweets()) {
    Map<String, Object> row = new LinkedHashMap<>();
    row.put("tweet_id", tweet.id());
    row.put("text", tweet.text());
    row.put("author_username", tweet.author().map(SearchTweet.Author::username).orElse(null));
    row.put("created_at", tweet.createdAt().orElse(null));

    System.out.println(objectMapper.writeValueAsString(row));
}

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

Use this workflow when a Java service, Spring Batch job, scheduled worker, or queue consumer needs tweet search results in a durable handoff file for a data lake, CRM enrichment step, analyst CSV export, XLSX workbook, or downstream processor. client.x().tweets().search calls GET /x/tweets/search. Build TweetSearchParams with the same query parameters the REST API accepts: .q(), .limit(), .cursor(), .sinceTime(), .untilTime(), and .queryType().
import com.fasterxml.jackson.databind.ObjectMapper;
import com.x_twitter_scraper.api.client.XTwitterScraperClient;
import com.x_twitter_scraper.api.client.okhttp.XTwitterScraperOkHttpClient;
import com.x_twitter_scraper.api.models.PaginatedTweets;
import com.x_twitter_scraper.api.models.SearchTweet;
import com.x_twitter_scraper.api.models.x.tweets.TweetSearchParams;
import com.x_twitter_scraper.api.models.x.tweets.TweetSearchParams.QueryType;
import java.io.BufferedWriter;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

XTwitterScraperClient client = XTwitterScraperOkHttpClient.fromEnv();
ObjectMapper objectMapper = new ObjectMapper();

String query = "from:xquikcom webhook OR SDK";
String cursor = null;
int pageIndex = 0;
List<String> headers = Arrays.asList(
    "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"
);

try (
    BufferedWriter jsonlWriter = Files.newBufferedWriter(
        Paths.get("xquik-tweet-search.jsonl"),
        StandardCharsets.UTF_8
    );
    BufferedWriter csvWriter = Files.newBufferedWriter(
        Paths.get("xquik-tweet-search.csv"),
        StandardCharsets.UTF_8
    )
) {
    writeCsvRow(csvWriter, headers);

    do {
        String pageCursor = cursor;
        TweetSearchParams.Builder builder = TweetSearchParams.builder()
            .q(query)
            .queryType(QueryType.LATEST);

        if (cursor != null && !cursor.isEmpty()) {
            builder.cursor(cursor);
        }

        PaginatedTweets page = client.x().tweets().search(builder.build());

        for (SearchTweet tweet : page.tweets()) {
            Map<String, Object> row = new LinkedHashMap<>();
            row.put("source", "xquik.java.search");
            row.put("query", query);
            row.put("tweet_id", tweet.id());
            row.put("text", tweet.text());
            row.put("author_id", tweet.author().map(SearchTweet.Author::id).orElse(null));
            row.put("author_username", tweet.author().map(SearchTweet.Author::username).orElse(null));
            row.put("author_name", tweet.author().map(SearchTweet.Author::name).orElse(null));
            row.put("created_at", tweet.createdAt().orElse(null));
            row.put("like_count", tweet.likeCount().orElse(0L));
            row.put("reply_count", tweet.replyCount().orElse(0L));
            row.put("retweet_count", tweet.retweetCount().orElse(0L));
            row.put("quote_count", tweet.quoteCount().orElse(0L));
            row.put("view_count", tweet.viewCount().orElse(0L));
            row.put("bookmark_count", tweet.bookmarkCount().orElse(0L));
            row.put("is_note_tweet", tweet.isNoteTweet().orElse(false));
            row.put("page_index", pageIndex);
            row.put("page_cursor", pageCursor);
            row.put("next_cursor", page.hasNextPage() ? page.nextCursor() : null);
            row.put("has_next_page", page.hasNextPage());

            List<Object> csvRow = new ArrayList<>();
            for (String header : headers) {
                csvRow.add(row.get(header));
            }

            jsonlWriter.write(objectMapper.writeValueAsString(row));
            jsonlWriter.newLine();
            writeCsvRow(csvWriter, csvRow);
        }

        pageIndex++;
        cursor = page.hasNextPage() ? page.nextCursor() : null;
    } while (cursor != null && !cursor.isEmpty());
}

static void writeCsvRow(BufferedWriter writer, List<?> values) throws IOException {
    for (int index = 0; index < values.size(); index++) {
        if (index > 0) {
            writer.write(",");
        }

        Object value = values.get(index);
        String cell = value == null ? "" : String.valueOf(value);
        writer.write("\"" + cell.replace("\"", "\"\"") + "\"");
    }

    writer.newLine();
}

Request Mapping

.q()

Java builder method .q() maps to REST q. Use it for an X search query such as from:username, a keyword, hashtag, or boolean operator query.

.limit()

Java builder method .limit() maps to REST limit. Use it for a bounded request from 1 to 200. Omit it for cursor loops.

.cursor()

Java builder method .cursor() maps to REST cursor. Pass the opaque cursor from page.nextCursor() to request the next page.

.sinceTime()

Java builder method .sinceTime() maps to REST sinceTime. Use it as the ISO 8601 lower bound for tweet creation time.

.untilTime()

Java builder method .untilTime() maps to REST untilTime. Use it as the ISO 8601 upper bound for tweet creation time.

.queryType()

Java builder method .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 list, page.hasNextPage() to decide whether another page exists, and page.nextCursor() as the checkpoint for the next request. Each SearchTweet includes generated accessors 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 supports withOptions() for retry and request settings. For longer workers, set an explicit retry limit and persist page.nextCursor() after each page.

Workflow: Follower Export to CSV, JSON, or XLSX

Use this workflow when a JVM worker needs an owned follower list for CRM import, warehouse loading, account scoring, analyst CSV, XLSX workbook delivery, or a resumable JSON handoff. client.extractions().estimateCost maps to POST /extractions/estimate, client.extractions().run maps to POST /extractions, client.extractions().retrieve maps to GET /extractions/{id}, and client.extractions().exportResults maps to GET /extractions/{id}/export. For follower exports, follower_explorer requires targetUsername.
import com.fasterxml.jackson.databind.ObjectMapper;
import com.x_twitter_scraper.api.client.XTwitterScraperClient;
import com.x_twitter_scraper.api.client.okhttp.XTwitterScraperOkHttpClient;
import com.x_twitter_scraper.api.core.JsonValue;
import com.x_twitter_scraper.api.core.http.HttpResponse;
import com.x_twitter_scraper.api.models.extractions.ExtractionEstimateCostParams;
import com.x_twitter_scraper.api.models.extractions.ExtractionEstimateCostResponse;
import com.x_twitter_scraper.api.models.extractions.ExtractionExportResultsParams;
import com.x_twitter_scraper.api.models.extractions.ExtractionRetrieveParams;
import com.x_twitter_scraper.api.models.extractions.ExtractionRetrieveResponse;
import com.x_twitter_scraper.api.models.extractions.ExtractionRunParams;
import com.x_twitter_scraper.api.models.extractions.ExtractionRunResponse;
import java.io.BufferedWriter;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.nio.file.StandardCopyOption;

XTwitterScraperClient client = XTwitterScraperOkHttpClient.fromEnv();
ObjectMapper objectMapper = new ObjectMapper();
String targetUsername = "xquikcom";

ExtractionEstimateCostResponse estimate = client.extractions().estimateCost(
    ExtractionEstimateCostParams.builder()
        .toolType(ExtractionEstimateCostParams.ToolType.FOLLOWER_EXPLORER)
        .targetUsername(targetUsername)
        .build()
);

if (!estimate.allowed()) {
    throw new IllegalStateException(
        "Follower export requires " + estimate.creditsRequired() + " credits."
    );
}

ExtractionRunResponse job = client.extractions().run(
    ExtractionRunParams.builder()
        .toolType(ExtractionRunParams.ToolType.FOLLOWER_EXPLORER)
        .targetUsername(targetUsername)
        .build()
);

boolean completed = false;
for (int attempt = 0; attempt < 120; attempt++) {
    ExtractionRetrieveResponse statusPage = client.extractions().retrieve(job.id());
    JsonValue statusValue = statusPage.job()._additionalProperties().get("status");
    String status = statusValue == null ? "unknown" : statusValue.asString().orElse("unknown");

    if ("completed".equals(status)) {
        completed = true;
        break;
    }

    if ("failed".equals(status)) {
        throw new IllegalStateException("Follower export failed.");
    }

    Thread.sleep(10_000L);
}

if (!completed) {
    throw new IllegalStateException("Follower export did not complete before timeout.");
}

String after = null;
try (BufferedWriter writer = Files.newBufferedWriter(
    Paths.get("xquik-followers.jsonl"),
    StandardCharsets.UTF_8
)) {
    do {
        ExtractionRetrieveParams.Builder pageParams = ExtractionRetrieveParams.builder()
            .id(job.id())
            .limit(1000L);

        if (after != null && !after.isEmpty()) {
            pageParams.after(after);
        }

        ExtractionRetrieveResponse page = client.extractions().retrieve(pageParams.build());

        for (ExtractionRetrieveResponse.Result row : page.results()) {
            writer.write(objectMapper.writeValueAsString(row._additionalProperties()));
            writer.newLine();
        }

        after = page.hasMore() ? page.nextCursor().orElse(null) : null;
    } while (after != null && !after.isEmpty());
}

try (HttpResponse export = client.extractions().exportResults(
    ExtractionExportResultsParams.builder()
        .id(job.id())
        .format(ExtractionExportResultsParams.Format.CSV)
        .build()
)) {
    Files.copy(
        export.body(),
        Paths.get("xquik-followers.csv"),
        StandardCopyOption.REPLACE_EXISTING
    );
}

try (HttpResponse export = client.extractions().exportResults(
    ExtractionExportResultsParams.builder()
        .id(job.id())
        .format(ExtractionExportResultsParams.Format.JSON)
        .build()
)) {
    Files.copy(
        export.body(),
        Paths.get("xquik-followers.json"),
        StandardCopyOption.REPLACE_EXISTING
    );
}

try (HttpResponse export = client.extractions().exportResults(
    ExtractionExportResultsParams.builder()
        .id(job.id())
        .format(ExtractionExportResultsParams.Format.XLSX)
        .build()
)) {
    Files.copy(
        export.body(),
        Paths.get("xquik-followers.xlsx"),
        StandardCopyOption.REPLACE_EXISTING
    );
}
Persist job.id(), targetUsername, estimate.estimatedResults(), and estimate.source() before polling so a queue retry can resume without rerunning the extraction. client.extractions().retrieve returns results(), hasMore(), and nextCursor(); pass nextCursor() back as after to page through large follower lists. 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. 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 Java worker needs every reply to a campaign, support thread, launch post, or incident update as a durable file. reply_extractor requires targetTweetId; estimate first, run the job, poll retrieve, then export the completed job as CSV, JSON, or XLSX. client.extractions().estimateCost maps to POST /extractions/estimate, client.extractions().run maps to POST /extractions, client.extractions().retrieve maps to GET /extractions/{id}, and client.extractions().exportResults maps to GET /extractions/{id}/export.
import com.fasterxml.jackson.databind.ObjectMapper;
import com.x_twitter_scraper.api.client.XTwitterScraperClient;
import com.x_twitter_scraper.api.client.okhttp.XTwitterScraperOkHttpClient;
import com.x_twitter_scraper.api.core.JsonValue;
import com.x_twitter_scraper.api.core.http.HttpResponse;
import com.x_twitter_scraper.api.models.extractions.ExtractionEstimateCostParams;
import com.x_twitter_scraper.api.models.extractions.ExtractionEstimateCostResponse;
import com.x_twitter_scraper.api.models.extractions.ExtractionExportResultsParams;
import com.x_twitter_scraper.api.models.extractions.ExtractionRetrieveParams;
import com.x_twitter_scraper.api.models.extractions.ExtractionRetrieveResponse;
import com.x_twitter_scraper.api.models.extractions.ExtractionRunParams;
import com.x_twitter_scraper.api.models.extractions.ExtractionRunResponse;
import java.io.BufferedWriter;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.nio.file.StandardCopyOption;

XTwitterScraperClient client = XTwitterScraperOkHttpClient.fromEnv();
ObjectMapper objectMapper = new ObjectMapper();
String targetTweetId = "1893704267862470862";

ExtractionEstimateCostResponse estimate = client.extractions().estimateCost(
    ExtractionEstimateCostParams.builder()
        .toolType(ExtractionEstimateCostParams.ToolType.REPLY_EXTRACTOR)
        .targetTweetId(targetTweetId)
        .build()
);

if (!estimate.allowed()) {
    throw new IllegalStateException(
        "Reply export requires " + estimate.creditsRequired() + " credits."
    );
}

ExtractionRunResponse job = client.extractions().run(
    ExtractionRunParams.builder()
        .toolType(ExtractionRunParams.ToolType.REPLY_EXTRACTOR)
        .targetTweetId(targetTweetId)
        .build()
);

boolean completed = false;
for (int attempt = 0; attempt < 120; attempt++) {
    ExtractionRetrieveResponse statusPage = client.extractions().retrieve(job.id());
    JsonValue statusValue = statusPage.job()._additionalProperties().get("status");
    String status = statusValue == null ? "unknown" : statusValue.asString().orElse("unknown");

    if ("completed".equals(status)) {
        completed = true;
        break;
    }

    if ("failed".equals(status)) {
        throw new IllegalStateException("Reply export failed.");
    }

    Thread.sleep(10_000L);
}

if (!completed) {
    throw new IllegalStateException("Reply export did not complete before timeout.");
}

String after = null;
try (BufferedWriter writer = Files.newBufferedWriter(
    Paths.get("xquik-replies.jsonl"),
    StandardCharsets.UTF_8
)) {
    do {
        ExtractionRetrieveParams.Builder pageParams = ExtractionRetrieveParams.builder()
            .id(job.id())
            .limit(1000L);

        if (after != null && !after.isEmpty()) {
            pageParams.after(after);
        }

        ExtractionRetrieveResponse page = client.extractions().retrieve(pageParams.build());

        for (ExtractionRetrieveResponse.Result row : page.results()) {
            writer.write(objectMapper.writeValueAsString(row._additionalProperties()));
            writer.newLine();
        }

        after = page.hasMore() ? page.nextCursor().orElse(null) : null;
    } while (after != null && !after.isEmpty());
}

try (HttpResponse export = client.extractions().exportResults(
    ExtractionExportResultsParams.builder()
        .id(job.id())
        .format(ExtractionExportResultsParams.Format.CSV)
        .build()
)) {
    Files.copy(
        export.body(),
        Paths.get("xquik-replies.csv"),
        StandardCopyOption.REPLACE_EXISTING
    );
}

try (HttpResponse export = client.extractions().exportResults(
    ExtractionExportResultsParams.builder()
        .id(job.id())
        .format(ExtractionExportResultsParams.Format.JSON)
        .build()
)) {
    Files.copy(
        export.body(),
        Paths.get("xquik-replies.json"),
        StandardCopyOption.REPLACE_EXISTING
    );
}

try (HttpResponse export = client.extractions().exportResults(
    ExtractionExportResultsParams.builder()
        .id(job.id())
        .format(ExtractionExportResultsParams.Format.XLSX)
        .build()
)) {
    Files.copy(
        export.body(),
        Paths.get("xquik-replies.xlsx"),
        StandardCopyOption.REPLACE_EXISTING
    );
}
Persist job.id() before polling so a queue retry can resume with client.extractions().retrieve(job.id()) and repeat client.extractions().exportResults(...) after completion. 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 JVM service 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 .addMedia() or .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 DM .addMediaId() handoff.
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.x_twitter_scraper.api.client.XTwitterScraperClient;
import com.x_twitter_scraper.api.client.okhttp.XTwitterScraperOkHttpClient;
import com.x_twitter_scraper.api.core.http.HttpResponseFor;
import com.x_twitter_scraper.api.models.x.dm.DmSendParams;
import com.x_twitter_scraper.api.models.x.dm.DmSendResponse;
import com.x_twitter_scraper.api.models.x.media.MediaUploadParams;
import com.x_twitter_scraper.api.models.x.media.MediaUploadResponse;
import com.x_twitter_scraper.api.models.x.tweets.TweetCreateParams;
import com.x_twitter_scraper.api.models.x.tweets.TweetCreateResponse;
import java.nio.file.Paths;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

XTwitterScraperClient client = XTwitterScraperOkHttpClient.fromEnv();
ObjectMapper objectMapper = new ObjectMapper();

static Map<String, Object> createTweetHandoff(
    Map<String, Object> payload,
    Map<String, Object> base
) {
    Map<String, Object> handoff = new LinkedHashMap<>();

    if (
        "x_write_unconfirmed".equals(payload.get("error")) &&
        "pending_confirmation".equals(payload.get("status"))
    ) {
        handoff.put("status", "pending_confirmation");
        handoff.put("write_action_id", payload.get("writeActionId"));
        handoff.put("charged", payload.get("charged"));
        handoff.put("charged_credits", payload.get("chargedCredits"));
        handoff.put("retryable", payload.get("retryable"));
        handoff.put("poll", "GET /x/write-actions/{id}");
    } else {
        handoff.put("status", "posted");
        handoff.put("tweet_id", payload.get("tweetId"));
        handoff.put("charged", payload.get("charged"));
        handoff.put("charged_credits", payload.get("chargedCredits"));
        if (payload.containsKey("writeActionId")) {
            handoff.put("write_action_id", payload.get("writeActionId"));
        }
    }

    handoff.putAll(base);
    return handoff;
}

Map<String, Object> tweetPayload;
try (HttpResponseFor<TweetCreateResponse> response = client.x().tweets().withRawResponse().create(
    TweetCreateParams.builder()
        .account("@xquikcom")
        .text("Shipping the weekly X API video.")
        .addMedia("https://static.example.com/reports/x-api-export.mp4")
        .build()
)) {
    tweetPayload = objectMapper.readValue(
        response.body(),
        new TypeReference<Map<String, Object>>() {}
    );
}

Map<String, Object> tweetBase = new LinkedHashMap<>();
tweetBase.put("account", "@xquikcom");
tweetBase.put("media", List.of("https://static.example.com/reports/x-api-export.mp4"));
Map<String, Object> tweetHandoff = createTweetHandoff(tweetPayload, tweetBase);

Map<String, Object> replyPayload;
try (HttpResponseFor<TweetCreateResponse> response = client.x().tweets().withRawResponse().create(
    TweetCreateParams.builder()
        .account("@xquikcom")
        .text("Here is the chart behind the update.")
        .addMedia("https://static.example.com/reports/reply-chart.png")
        .replyToTweetId("1893704267862470862")
        .build()
)) {
    replyPayload = objectMapper.readValue(
        response.body(),
        new TypeReference<Map<String, Object>>() {}
    );
}

Map<String, Object> replyBase = new LinkedHashMap<>();
replyBase.put("account", "@xquikcom");
replyBase.put("reply_to_tweet_id", "1893704267862470862");
replyBase.put("media", List.of("https://static.example.com/reports/reply-chart.png"));
Map<String, Object> replyHandoff = createTweetHandoff(replyPayload, replyBase);

MediaUploadResponse media = client.x().media().upload(
    MediaUploadParams.builder()
        .account("@xquikcom")
        .file(Paths.get("handoff.png"))
        .build()
);

DmSendResponse dm = client.x().dm().send(
    "44196397",
    DmSendParams.builder()
        .account("@xquikcom")
        .text("Here is the requested asset.")
        .addMediaId(media.mediaId())
        .build()
);

Map<String, Object> dmHandoff = new LinkedHashMap<>();
dmHandoff.put("status", "sent");
dmHandoff.put("message_id", dm.messageId());
dmHandoff.put("media_id", media.mediaId());
dmHandoff.put("account", "@xquikcom");
dmHandoff.put("user_id", "44196397");

System.out.println(objectMapper.writeValueAsString(tweetHandoff));
System.out.println(objectMapper.writeValueAsString(replyHandoff));
System.out.println(objectMapper.writeValueAsString(dmHandoff));
The generated Java response model covers confirmed tweetId responses. Use client.x().tweets().withRawResponse().create(...) 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. Store tweetHandoff, replyHandoff, and dmHandoff on the CMS, support ticket, queue job, CRM note, or agent state before scheduling follow-up work. 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 public media URLs through media.

Error Handling

The Java SDK throws unchecked exceptions.

400 Bad Request

Throws BadRequestException.

401 Unauthorized

Throws UnauthorizedException.

403 Permission Denied

Throws PermissionDeniedException.

404 Not Found

Throws NotFoundException.

422 Validation Error

Throws UnprocessableEntityException.

429 Rate Limited

Throws RateLimitException.

5xx Server Error

Throws InternalServerException.
Connection and I/O failures use XTwitterScraperIoException. All SDK exceptions inherit from XTwitterScraperException.

Pagination

Paginated responses expose generated fields such as hasNextPage. Use the endpoint cursor fields documented in the API reference when requesting additional pages.
if (tweets.hasNextPage()) {
    System.err.println("More results are available");
}

Webhooks & References

Last modified on May 20, 2026