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

# X (Twitter) API Kotlin media tweets & DM attachments

> Upload media with the Xquik Kotlin SDK, post tweets with photos or video, and send DM attachments from a connected X (Twitter) account in your own apps.

> For the complete documentation index, see [llms.txt](https://docs.xquik.com/llms.txt).

This guide builds on the [Kotlin SDK setup](https://docs.xquik.com/sdks/kotlin.md), which covers install and authentication.

## Workflow to post media tweets and DM attachments

Use this workflow to post a media tweet or reply. It can also DM 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. The video can be up to 100 MB. 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={"theme":{"light":"github-light","dark":"github-dark"},"languages":{"custom":["/languages/bash.json","/languages/csharp.json","/languages/go.json","/languages/java.json","/languages/javascript.json","/languages/kotlin.json","/languages/php.json","/languages/python.json","/languages/ruby.json","/languages/typescript.json"]}}
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`. 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.




`client.x().dm().deleteMessage` [deletes a DM](https://docs.xquik.com/api-reference/x-write/delete-dm.md).




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

### REST routes

| Task | REST route | Reference |
| - | - | - |
| Post a media tweet or reply | `POST /x/tweets` | [Create tweet](https://docs.xquik.com/api-reference/x-write/create-tweet.md) |
| Upload a local file | `POST /x/media` | [Upload media](https://docs.xquik.com/api-reference/x-write/upload-media.md) |
| Send a DM attachment | `POST /x/dm/{userId}` | [Send DM](https://docs.xquik.com/api-reference/x-write/send-dm.md) |
| Check a write action | `GET /x/write-actions/{id}` | [Write action status](https://docs.xquik.com/api-reference/x-write/get-write-action-status.md) |
| Delete a DM | `DELETE /x/dm/{userId}/messages/{messageId}` | [Delete DM](https://docs.xquik.com/api-reference/x-write/delete-dm.md) |

## Next steps

* [Kotlin SDK setup](https://docs.xquik.com/sdks/kotlin.md)
* [Tweet search](https://docs.xquik.com/sdks/kotlin-tweet-search.md)
* [Follower export](https://docs.xquik.com/sdks/kotlin-follower-export.md)


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.