> ## 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 tweet search to CSV & JSON lines

> Search tweets with the Xquik PHP SDK, follow cursors, and save rows as JSON Lines, CSV, or XLSX for workers, queues, and analysts in your own apps and jobs.

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

## Tweet search workflow to JSON Lines, CSV, or XLSX

Use this workflow when PHP code needs tweet search results in a durable file. That code can be a worker, Laravel command, Symfony console command, or cron job. The file can feed a queue, warehouse loader, or analyst CSV export. It can also feed an XLSX workbook or a CRM enrichment step.




`$client->x->tweets->search()` calls `GET /x/tweets/search`. The generated parameter model is `TweetSearchParams`. The service method exposes the same request controls as named arguments. They are `q`, `limit`, `cursor`, `sinceTime`, `untilTime`, and `queryType`.

```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\PaginatedTweets;
use XTwitterScraper\SearchTweet;
use XTwitterScraper\X\Tweets\TweetSearchParams\QueryType;

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

$query = 'from:username webhook OR SDK';
$cursor = null;
$pageIndex = 0;
$headers = [
  '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',
];
$jsonlHandle = fopen('xquik-tweet-search.jsonl', 'wb');
$csvHandle = fopen('xquik-tweet-search.csv', 'wb');

if (false === $jsonlHandle || false === $csvHandle) {
  throw new RuntimeException('Could not open tweet search handoff files');
}

try {
  fputcsv($csvHandle, $headers);

  do {
    $pageCursor = $cursor;

    /** @var PaginatedTweets $page */
    $page = $client->x->tweets->search(
      q: $query,
      cursor: $cursor,
      queryType: QueryType::LATEST,
    );

    foreach ($page->tweets as $tweet) {
      /** @var SearchTweet $tweet */
      $row = [
        'source' => 'xquik.php.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->nextCursor ? null : $page->nextCursor,
        'has_next_page' => $page->hasNextPage,
      ];
      $csvRow = [];
      foreach ($headers as $header) {
        $csvRow[] = $row[$header];
      }

      fputcsv($csvHandle, $csvRow);
      fwrite(
        $jsonlHandle,
        json_encode($row, JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR) . PHP_EOL
      );
    }

    $cursor = $page->hasNextPage ? $page->nextCursor : null;
    $pageIndex++;
  } while (null !== $cursor && '' !== $cursor);
} finally {
  fclose($jsonlHandle);
  fclose($csvHandle);
}
```

### Request mapping

> [card] q
>
> PHP argument `q` maps to REST `q`. It takes an X (Twitter) search query. Examples are `from:username`, a keyword, hashtag, or boolean operator query.

> [card] limit
>
> PHP argument `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`. That applies when you continue with `$page->nextCursor`.

> [card] cursor
>
> PHP argument `cursor` maps to REST `cursor`. Pass the opaque cursor from `$page->nextCursor` to request the next page.

> [card] sinceTime
>
> PHP argument `sinceTime` maps to REST `sinceTime`. Use the inclusive ISO bound on every page.

> [card] untilTime
>
> PHP argument `untilTime` maps to REST `untilTime`. Use the exclusive ISO bound on every page.

> [card] queryType
>
> PHP argument `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` tells you whether another page exists. Use `$page->nextCursor` as the checkpoint for the next request. For bounded pulls that return fewer tweets than `limit`, pass `$page->nextCursor` back as `cursor`. Send the same query, filters, `queryType`, and `limit` with it.

Each `SearchTweet` includes typed properties such as `$id`, `$text`, `$author`, and `$createdAt`. It also includes `$likeCount`, `$replyCount`, `$retweetCount`, `$quoteCount`, `$bookmarkCount`, `$viewCount`, and `$isNoteTweet` when available. Project `$page->tweets` into JSON Lines and CSV rows. Each row holds `tweet_id`, `author_username`, engagement counts, `page_index`, `page_cursor`, `next_cursor`, and `has_next_page`. Workers can then resume. Or they can load the same records into XLSX, CRM, warehouse, or agent workflows.

Tweet search costs 1 credit per tweet returned. If credits cannot cover a bounded `limit` request, the API can return fewer tweets. If credits cover 0 results, it returns `402 insufficient_credits`. The SDK retries connection errors, timeouts, 408, 409, 429, and 5xx responses by default. The default is 2 retries. For explicit `limit` pulls, resume with the same query, filters, `queryType`, and `limit`. Only `cursor` changes.

## Next steps

* [PHP SDK setup](https://docs.xquik.com/sdks/php.md)
* [Follower export](https://docs.xquik.com/sdks/php-follower-export.md)
* [Media tweets](https://docs.xquik.com/sdks/php-media-tweets.md)


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