> ## 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 PHP media tweets & DM attachments

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

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

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

## Workflow to post media tweets and DM attachments

Use this workflow when a PHP app needs to publish a media tweet. Use it too when the app replies with media. It also covers a direct message with an uploaded local file. `POST /x/tweets` accepts public media URLs through `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 its `$media->mediaID` only for the one-item `mediaIDs` array on `$client->x->dm->send()`.




Use `$client->x->tweets->raw->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.

```php 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"]}}
<?php

use XTwitterScraper\Client;
use XTwitterScraper\Core\FileParam;

/**
 * @param array<string,mixed> $payload
 *
 * @return array<string,mixed>
 */
function createTweetHandoff(Client $client, array $payload): array
{
  $response = $client->x->tweets->raw->create(params: $payload);
  $action = json_decode(
    (string) $response->getBody(),
    true,
    flags: JSON_THROW_ON_ERROR,
  );
  $result = $action['result'] ?? [];

  return [
    'status' => $action['status'],
    'terminal' => $action['terminal'],
    'safe_to_retry' => $action['safeToRetry'],
    'write_action_id' => $action['id'],
    'request_hash' => $action['request']['hash'],
    'tweet_id' => $result['id'] ?? $action['tweetId'] ?? null,
    'charged' => $action['billing']['charged'],
    'charged_credits' => $action['billing']['chargedCredits'],
    'poll' => $action['terminal'] ? null : $action['statusUrl'],
  ];
}

$client = new Client(
  apiKey: getenv('X_TWITTER_SCRAPER_API_KEY') ?: ''
);

$tweetHandoff = createTweetHandoff($client, [
  'account' => '@username',
  'text' => 'Shipping the weekly X API video.',
  'media' => ['https://static.example.com/reports/x-api-export.mp4'],
]);

$replyHandoff = createTweetHandoff($client, [
  'account' => '@username',
  'text' => 'Here is the chart behind the update.',
  'media' => ['https://static.example.com/reports/reply-chart.png'],
  'replyToTweetID' => $tweetHandoff['tweet_id'] ?? '1893704267862470862',
]);

$localFile = fopen('handoff.png', 'rb');

if (false === $localFile) {
  throw new RuntimeException('Could not open handoff.png');
}

try {
  $media = $client->x->media->upload(
    account: '@username',
    file: FileParam::fromResource($localFile),
  );
} finally {
  fclose($localFile);
}

$dm = $client->x->dm->send(
  '44196397',
  account: '@username',
  text: 'Here is the requested asset.',
  mediaIDs: [$media->mediaID],
);

$dmHandoff = [
  'message_id' => $dm->messageID,
  'media_id' => $media->mediaID,
  'user_id' => '44196397',
  'account' => '@username',
  'status' => 'sent',
];

fwrite(
  STDOUT,
  json_encode(
    [
      'tweet_handoff' => $tweetHandoff,
      'reply_handoff' => $replyHandoff,
      'dm_handoff' => $dmHandoff,
    ],
    JSON_THROW_ON_ERROR
  ) . PHP_EOL
);
```

Store `write_action_id` and `charged_credits`. Before retrying a pending tweet or reply, poll [Get Write Action Status](https://docs.xquik.com/api-reference/x-write/get-write-action-status.md). Store `tweet_id` on the CMS, queue job, or agent state after confirmed writes. Store `$dm->messageID`, `$media->mediaID`, `account`, `user_id`, and send status. Put them on the support ticket or CRM note. Keep DM body text in private systems. Shared logs, public artifacts, queue status, and agent handoffs should omit full DM bodies. They should store `message_id`, optional `media_id`, `account`, `user_id`, and send status. Leave generated `replyToMessageID` unset even if SDK params expose it. The REST endpoint rejects DM reply threading. Text-only tweet and reply writes cost 30 credits. Tweet media adds 2 credits per started MB across attached files. Uploading media costs 10 credits. 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.




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

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

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


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