> ## 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.

# Kotlin SDK

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

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

Use the Kotlin SDK for generated JVM models, Kotlin nullable values, builders, sync calls, async calls, typed exceptions, retries, and file upload helpers. It is useful when a JVM service, Ktor worker, Spring job, queue consumer, or cron task 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

<Note>
  Maven Central publication is pending. Build from source until the `com.x_twitter_scraper.api:x-twitter-scraper-kotlin` artifact resolves in Maven Central.
</Note>

```bash theme={null}
git clone https://github.com/Xquik-dev/x-twitter-scraper-kotlin.git
cd x-twitter-scraper-kotlin
./gradlew build
```

For local Maven testing:

```bash theme={null}
./gradlew publishToMavenLocal -PpublishLocal
```

Before restoring Maven Central install snippets, verify:

```bash theme={null}
curl -f https://repo1.maven.org/maven2/com/x_twitter_scraper/api/x-twitter-scraper-kotlin/maven-metadata.xml
```

## Authenticate

```bash theme={null}
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:

```kotlin theme={null}
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

val client: XTwitterScraperClient = XTwitterScraperOkHttpClient.fromEnv()
val objectMapper = ObjectMapper()

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

val page: PaginatedTweets = client.x().tweets().search(params)

for (tweet: SearchTweet in page.tweets()) {
    val row = linkedMapOf<String, Any?>(
        "tweet_id" to tweet.id(),
        "text" to tweet.text(),
        "author_username" to tweet.author()?.username(),
        "created_at" to tweet.createdAt(),
    )

    println(objectMapper.writeValueAsString(row))
}
```

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

Use this workflow when a Kotlin service, scheduled worker, queue consumer, or agent 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()`.

```kotlin theme={null}
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.nio.charset.StandardCharsets
import java.nio.file.Files
import java.nio.file.Paths

val client: XTwitterScraperClient = XTwitterScraperOkHttpClient.fromEnv()
val objectMapper = ObjectMapper()

val query = "from:username webhook OR SDK"
var cursor: String? = null
var pageIndex = 0
val headers = listOf(
    "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",
)

Files.newBufferedWriter(Paths.get("xquik-tweet-search.jsonl"), StandardCharsets.UTF_8).use { jsonlWriter ->
    Files.newBufferedWriter(Paths.get("xquik-tweet-search.csv"), StandardCharsets.UTF_8).use { csvWriter ->
        writeCsvRow(csvWriter, headers)

        do {
            val pageCursor = cursor
            val builder = TweetSearchParams.builder()
                .q(query)
                .queryType(QueryType.LATEST)

            if (!cursor.isNullOrEmpty()) {
                builder.cursor(cursor)
            }

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

            for (tweet: SearchTweet in page.tweets()) {
                val author = tweet.author()
                val row = linkedMapOf<String, Any?>(
                    "source" to "xquik.kotlin.search",
                    "query" to query,
                    "tweet_id" to tweet.id(),
                    "text" to tweet.text(),
                    "author_id" to author?.id(),
                    "author_username" to author?.username(),
                    "author_name" to author?.name(),
                    "created_at" to tweet.createdAt(),
                    "like_count" to (tweet.likeCount() ?: 0L),
                    "reply_count" to (tweet.replyCount() ?: 0L),
                    "retweet_count" to (tweet.retweetCount() ?: 0L),
                    "quote_count" to (tweet.quoteCount() ?: 0L),
                    "view_count" to (tweet.viewCount() ?: 0L),
                    "bookmark_count" to (tweet.bookmarkCount() ?: 0L),
                    "is_note_tweet" to (tweet.isNoteTweet() ?: false),
                    "page_index" to pageIndex,
                    "page_cursor" to pageCursor,
                    "next_cursor" to if (page.hasNextPage()) page.nextCursor() else null,
                    "has_next_page" to page.hasNextPage(),
                )

                jsonlWriter.write(objectMapper.writeValueAsString(row))
                jsonlWriter.newLine()
                writeCsvRow(csvWriter, headers.map { header -> row[header] })
            }

            pageIndex++
            cursor = if (page.hasNextPage()) page.nextCursor() else null
        } while (!cursor.isNullOrEmpty())
    }
}

fun writeCsvRow(writer: BufferedWriter, values: List<Any?>) {
    writer.write(
        values.joinToString(",") { value ->
            val cell = value?.toString() ?: ""
            "\"" + cell.replace("\"", "\"\"") + "\""
        }
    )
    writer.newLine()
}
```

### Request Mapping

<CardGroup cols={2}>
  <Card title=".q()" icon="search">
    Kotlin 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.
  </Card>

  <Card title=".limit()" icon="list">
    Kotlin builder method `.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">
    Kotlin builder method `.cursor()` maps to REST `cursor`. Pass the opaque cursor from `page.nextCursor()` to request the next page.
  </Card>

  <Card title=".sinceTime()" icon="clock">
    Kotlin builder method `.sinceTime()` maps to REST `sinceTime`. Use it as the ISO 8601 lower bound for tweet creation time.
  </Card>

  <Card title=".untilTime()" icon="clock">
    Kotlin builder method `.untilTime()` maps to REST `untilTime`. Use it as the ISO 8601 upper bound for tweet creation time.
  </Card>

  <Card title=".queryType()" icon="sliders-horizontal">
    Kotlin builder method `.queryType()` maps to REST `queryType`. Use `QueryType.LATEST` for chronological search or `QueryType.TOP` for engagement-ranked search.
  </Card>
</CardGroup>

### 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. For bounded pulls that return fewer tweets than the requested `.limit()`, pass `page.nextCursor()` back as `.cursor()` with the same query, filters, `QueryType`, and `.limit()`.

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 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 Kotlin 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`.

<Info>
  `client.extractions().run` returns the queued `202 Accepted` receipt from `POST /extractions`: REST `id`, `toolType`, and `status: "running"` as Kotlin `job.id()`, `job.toolType()`, and `job._status()`. Store `job.id()` immediately, then poll `client.extractions().retrieve` 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>

```kotlin theme={null}
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.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.nio.charset.StandardCharsets
import java.nio.file.Files
import java.nio.file.Paths
import java.nio.file.StandardCopyOption

val client: XTwitterScraperClient = XTwitterScraperOkHttpClient.fromEnv()
val objectMapper = ObjectMapper()
val targetUsername = "username"

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

if (!estimate.allowed()) {
    error("Follower export requires ${estimate.creditsRequired()} credits.")
}

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

var completed = false
var attempts = 0
while (!completed && attempts < 120) {
    val statusPage = client.extractions().retrieve(job.id())
    val status = statusPage.job()._additionalProperties()["status"]?.asString() ?: "unknown"

    if (status == "completed") {
        completed = true
    } else if (status == "failed") {
        error("Follower export failed.")
    } else {
        Thread.sleep(10_000L)
    }

    attempts += 1
}

if (!completed) {
    error("Follower export did not complete before timeout.")
}

var after: String? = null
Files.newBufferedWriter(Paths.get("xquik-followers.jsonl"), StandardCharsets.UTF_8).use { writer ->
    do {
        val pageParams = ExtractionRetrieveParams.builder()
            .id(job.id())
            .limit(1000L)

        if (!after.isNullOrEmpty()) {
            pageParams.after(after)
        }

        val page: ExtractionRetrieveResponse = client.extractions().retrieve(pageParams.build())

        for (row in page.results()) {
            writer.write(objectMapper.writeValueAsString(row._additionalProperties()))
            writer.newLine()
        }

        after = if (page.hasMore()) page.nextCursor() else null
    } while (!after.isNullOrEmpty())
}

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

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

client.extractions().exportResults(
    ExtractionExportResultsParams.builder()
        .id(job.id())
        .format(ExtractionExportResultsParams.Format.XLSX)
        .build()
).use { export ->
    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 Kotlin 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`.

Reuse the follower export loop above. Change only the required target, tool type, and output names:

```kotlin theme={null}
val targetTweetId = "1893704267862470862"

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

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

val pageParams = ExtractionRetrieveParams.builder()
    .id(job.id())
    .limit(1000L)

if (!after.isNullOrEmpty()) {
    pageParams.after(after)
}

val page: ExtractionRetrieveResponse = client.extractions().retrieve(pageParams.build())
```

Persist `job.id()` before polling so a queue retry can resume with `client.extractions().retrieve(job.id())` and repeat `client.extractions().exportResults(...)` after completion. Stream `page.results()` to `Paths.get("xquik-replies.jsonl")`. Keep `page.nextCursor()` as the checkpoint when you stream replies to JSON Lines. Pass it back as `after` on the next `ExtractionRetrieveParams.builder()` call.

Keep the same `estimate.allowed()` credit branch from the follower export workflow before calling `run`.

Export the completed job with the same helper used above:

```kotlin theme={null}
client.extractions().exportResults(
    ExtractionExportResultsParams.builder()
        .id(job.id())
        .format(ExtractionExportResultsParams.Format.CSV)
        .build()
).use { export ->
    Files.copy(export.body(), Paths.get("xquik-replies.csv"), StandardCopyOption.REPLACE_EXISTING)
}
```

Repeat the export with `ExtractionExportResultsParams.Format.JSON` to write `Paths.get("xquik-replies.json")` and `ExtractionExportResultsParams.Format.XLSX` to write `Paths.get("xquik-replies.xlsx")`. 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. Cost: 1 credit per reply extracted or returned.

## Workflow: Post Media Tweets and DM Attachments

Use this workflow when a Kotlin 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.

```kotlin theme={null}
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

val client: XTwitterScraperClient = XTwitterScraperOkHttpClient.fromEnv()
val objectMapper = ObjectMapper()

fun createTweetHandoff(
    payload: Map<String, Any?>,
    base: Map<String, Any?>,
): Map<String, Any?> {
    val handoff = linkedMapOf<String, Any?>()
    val billing = payload["billing"] as Map<*, *>
    val request = payload["request"] as Map<*, *>
    val result = payload["result"] as? Map<*, *>
    val terminal = payload["terminal"] == true

    handoff["status"] = payload["status"]
    handoff["terminal"] = terminal
    handoff["safe_to_retry"] = payload["safeToRetry"]
    handoff["write_action_id"] = payload["id"]
    handoff["request_hash"] = request["hash"]
    handoff["tweet_id"] = result?.get("id") ?: payload["tweetId"]
    handoff["charged"] = billing["charged"]
    handoff["charged_credits"] = billing["chargedCredits"]
    handoff["poll"] = if (terminal) null else payload["statusUrl"]

    handoff.putAll(base)
    return handoff
}

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

val tweetHandoff = createTweetHandoff(
    tweetPayload,
    linkedMapOf(
        "account" to "@username",
        "media" to listOf("https://static.example.com/reports/x-api-export.mp4"),
    ),
)

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

val replyHandoff = createTweetHandoff(
    replyPayload,
    linkedMapOf(
        "account" to "@username",
        "reply_to_tweet_id" to "1893704267862470862",
        "media" to listOf("https://static.example.com/reports/reply-chart.png"),
    ),
)

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

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

val dmHandoff = linkedMapOf(
    "status" to "sent",
    "message_id" to dm.messageId(),
    "media_id" to media.mediaId(),
    "account" to "@username",
    "user_id" to "44196397",
)

println(objectMapper.writeValueAsString(tweetHandoff))
println(objectMapper.writeValueAsString(replyHandoff))
println(objectMapper.writeValueAsString(dmHandoff))
```

Use `client.x().tweets().withRawResponse().create(...)` 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.

Keep DM body text in private systems. Shared logs and handoffs should store `message_id`, optional `media_id`, `account`, `user_id`, and lifecycle status. Leave `replyToMessageId` unset because the REST endpoint rejects DM reply threading.

Use public media URLs with `client.x().tweets().create`. Do not pass uploaded `media.mediaId()` values to tweet creation.

## Error Handling

The Kotlin SDK uses the same generated exception hierarchy as the Java SDK.

<CardGroup cols={2}>
  <Card title="400 Bad Request" icon="triangle-alert">
    Throws `BadRequestException`.
  </Card>

  <Card title="401 Unauthorized" icon="lock">
    Throws `UnauthorizedException`.
  </Card>

  <Card title="403 Permission Denied" icon="shield">
    Throws `PermissionDeniedException`.
  </Card>

  <Card title="404 Not Found" icon="circle-x">
    Throws `NotFoundException`.
  </Card>

  <Card title="422 Validation Error" icon="file-warning">
    Throws `UnprocessableEntityException`.
  </Card>

  <Card title="429 Rate Limited" icon="gauge">
    Throws `RateLimitException`.
  </Card>

  <Card title="5xx Server Error" icon="server">
    Throws `InternalServerException`.
  </Card>
</CardGroup>

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

## Pagination

Paginated responses expose generated fields such as `hasNextPage`. Use endpoint cursor parameters from the API reference for follow-up requests.

```kotlin theme={null}
if (tweets.hasNextPage()) {
    System.err.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)
* [Create Monitor](/api-reference/monitors/create)
* [Webhook Overview](/webhooks/overview)
* [Verify HMAC Signatures](/webhooks/verification)
* [REST API Overview](/api-reference/overview)
* [Source Repository](https://github.com/Xquik-dev/x-twitter-scraper-kotlin)
