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

# Twitter timeline API for home feed tweets & media

> Get a connected X account's home feed. Paginate tweets with authors, replies, reposts, likes, views, media, and cursors. Costs 1 credit per tweet returned.

<Panel>
  <Tabs defaultTabIndex={0} sync={false}>
    <Tab title="200" id="response-x-timeline-200">
      ```json theme={null}
      {
        "tweets": [],
        "has_next_page": false,
        "next_cursor": ""
      }
      ```
    </Tab>

    <Tab title="401" id="response-x-timeline-401">
      ```json theme={null}
      {
        "error": "unauthenticated",
        "message": "Authentication required. Provide a valid API key or bearer token."
      }
      ```
    </Tab>

    <Tab title="402" id="response-x-timeline-402">
      ```json theme={null}
      {
        "error": "insufficient_credits",
        "message": "Insufficient credits. Top up or subscribe to continue."
      }
      ```
    </Tab>

    <Tab title="410" id="response-x-timeline-410">
      ```json theme={null}
      {
        "error": "cursor_account_unavailable",
        "message": "The X account for this cursor is unavailable. Start again without the cursor."
      }
      ```
    </Tab>

    <Tab title="424" id="response-x-timeline-424">
      ```json theme={null}
      {
        "error": "x_api_unavailable",
        "message": "X data source temporarily unavailable. Try again later."
      }
      ```
    </Tab>

    <Tab title="429" id="response-x-timeline-429">
      ```json theme={null}
      {
        "error": "rate_limit_exceeded",
        "message": "Too many requests. Try again later.",
        "retryAfter": 60
      }
      ```
    </Tab>

    <Tab title="502" id="response-x-timeline-502">
      ```json theme={null}
      {
        "error": "x_api_unavailable",
        "message": "X data source temporarily unavailable. Try again later."
      }
      ```
    </Tab>

    <Tab title="503" id="response-x-timeline-503">
      ```json theme={null}
      {
        "error": "x_api_unavailable",
        "message": "Xquik is busy right now. Retry shortly."
      }
      ```
    </Tab>
  </Tabs>
</Panel>

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

<Note>
  Requested result counts are upper bounds for paid authenticated calls. When remaining credits cannot cover the full page or ID list, Xquik returns fewer results. If zero paid results are affordable, it returns `402 insufficient_credits`.
</Note>

<Callout icon="coins" color="#5c3327">
  **1 credit per result returned** · [All plans](https://xquik.com/#pricing) from \$0.00012/credit
</Callout>

The route reads the home timeline of 1 of your connected X accounts. Without
one, it returns `424 account_required`. Use [Connect X account](/api-reference/x-accounts/connect)
to add one. When your connected X accounts are busy, it returns `503`. Retry
after the `Retry-After` delay.

Each `next_cursor` reads the next page from the same X account. If that account
can no longer read, the route returns `410 cursor_account_unavailable`. Start
again without the cursor. An account that follows no one returns no tweets and
`has_next_page: false`.

Results exclude X ads.

<CodeGroup>
  ```bash cURL theme={null}
  curl https://xquik.com/api/v1/x/timeline \
    -H "x-api-key: xq_YOUR_KEY_HERE" | jq

  # Page 2
  curl -G https://xquik.com/api/v1/x/timeline \
    --data-urlencode "cursor=abc123" \
    -H "x-api-key: xq_YOUR_KEY_HERE" | jq
  ```

  ```javascript Node.js theme={null}
  const baseUrl = "https://xquik.com/api/v1/x/timeline";
  const seenTweetIds = new Set();
  let pageCursor = "";

  for (let pageIndex = 0; pageIndex < 3; pageIndex += 1) {
    const params = new URLSearchParams();
    if (pageCursor !== "") params.set("cursor", pageCursor);
    if (seenTweetIds.size > 0) {
      params.set("seenTweetIds", Array.from(seenTweetIds).join(","));
    }

    const query = params.toString();
    const response = await fetch(query === "" ? baseUrl : `${baseUrl}?${query}`, {
      headers: { "x-api-key": "xq_YOUR_KEY_HERE" },
    });
    const page = await response.json();
    if (!response.ok) throw new Error(JSON.stringify(page));

    const timelineRows = page.tweets.map((tweet) => {
      seenTweetIds.add(tweet.id);
      return {
        timeline_source: "home",
        tweet_id: tweet.id,
        tweet_url: tweet.url ?? null,
        text: tweet.text,
        author_id: tweet.author?.id ?? null,
        author_username: tweet.author?.username ?? null,
        author_name: tweet.author?.name ?? null,
        author_followers: tweet.author?.followers ?? null,
        author_verified: tweet.author?.verified ?? null,
        author_profile_picture: tweet.author?.profilePicture ?? null,
        created_at: tweet.createdAt ?? null,
        is_reply: tweet.isReply ?? false,
        in_reply_to_id: tweet.inReplyToId ?? null,
        like_count: tweet.likeCount ?? null,
        reply_count: tweet.replyCount ?? null,
        retweet_count: tweet.retweetCount ?? null,
        quote_count: tweet.quoteCount ?? null,
        view_count: tweet.viewCount ?? null,
        media_urls: (tweet.media ?? []).map((item) => item.mediaUrl).filter(Boolean),
        page_index: pageIndex,
        page_cursor: pageCursor,
        next_cursor: page.next_cursor,
        has_next_page: page.has_next_page,
      };
    });
    for (const row of timelineRows) process.stdout.write(`${JSON.stringify(row)}\n`);

    if (!page.has_next_page || page.next_cursor === "") break;
    pageCursor = page.next_cursor;
  }
  ```

  ```python Python theme={null}
  import json
  import requests

  seen_tweet_ids = set()
  page_cursor = ""

  for page_index in range(3):
      params = {}
      if page_cursor:
          params["cursor"] = page_cursor
      if seen_tweet_ids:
          params["seenTweetIds"] = ",".join(sorted(seen_tweet_ids))

      response = requests.get(
          "https://xquik.com/api/v1/x/timeline",
          params=params,
          headers={"x-api-key": "xq_YOUR_KEY_HERE"},
      )
      page = response.json()
      if not response.ok:
          raise RuntimeError(page)

      for tweet in page["tweets"]:
          seen_tweet_ids.add(tweet["id"])
          timeline_row = {
              "timeline_source": "home",
              "tweet_id": tweet["id"],
              "tweet_url": tweet.get("url"),
              "text": tweet["text"],
              "author_id": (tweet.get("author") or {}).get("id"),
              "author_username": (tweet.get("author") or {}).get("username"),
              "author_name": (tweet.get("author") or {}).get("name"),
              "author_followers": (tweet.get("author") or {}).get("followers"),
              "author_verified": (tweet.get("author") or {}).get("verified"),
              "author_profile_picture": (tweet.get("author") or {}).get("profilePicture"),
              "created_at": tweet.get("createdAt"),
              "is_reply": tweet.get("isReply", False),
              "in_reply_to_id": tweet.get("inReplyToId"),
              "like_count": tweet.get("likeCount"),
              "reply_count": tweet.get("replyCount"),
              "retweet_count": tweet.get("retweetCount"),
              "quote_count": tweet.get("quoteCount"),
              "view_count": tweet.get("viewCount"),
              "media_urls": [
                  item["mediaUrl"] for item in tweet.get("media", []) if item.get("mediaUrl")
              ],
              "page_index": page_index,
              "page_cursor": page_cursor,
              "next_cursor": page["next_cursor"],
              "has_next_page": page["has_next_page"],
          }
          print(json.dumps(timeline_row, separators=(",", ":")))

      if not page["has_next_page"] or not page["next_cursor"]:
          break
      page_cursor = page["next_cursor"]
  ```
</CodeGroup>

## Home timeline handoff

Use `GET /x/timeline` for the authenticated account's home feed.
The examples write JSON Lines rows.
They include author ID, username, display name, and tweet text.
Rows include follower count, verified state, profile image URL, `seenTweetIds`, and cursor fields.
Store processed tweet IDs.
Then pass them as `seenTweetIds` with the last saved `next_cursor`.

Use the home timeline for inboxes, CRM routing, and approved monitoring. Store the connected
account ID and collection time with each page.

Keep tweet IDs, authors, text, engagement counts, replies, media, and
cursors. X controls ranking and source availability. The response
is not a complete public archive. X explains the feed model in its
[home and user timeline documentation](https://docs.x.com/x-api/posts/timelines/introduction).

<CardGroup cols={2}>
  <Card title="Home feed rows" icon="house">
    Store one row per `tweets[]` item with `timeline_source: "home"` for the
    connected account.
  </Card>

  <Card title="Seen tweet deduplication" icon="list-checks">
    Add processed tweet IDs to `seenTweetIds` before requesting the next page.
  </Card>

  <Card title="Cursor checkpoint" icon="arrow-right">
    Store `has_next_page` and `next_cursor`. Pass `next_cursor` back as `cursor`
    only when `has_next_page` is true.
  </Card>

  <Card title="Account-scoped sync" icon="lock-keyhole">
    Keep home timeline rows in account-scoped inbox, CRM, monitor seed, or agent
    memory systems.
  </Card>
</CardGroup>

| Home timeline column | Response source | Feed use |
| - | - | - |
| `tweet_id` | `tweets[].id` | Deduplicate rows and build tweet URLs. |
| `author_id` | `tweets[].author.id` | Keep stable author identity when usernames change. |
| `page_cursor` | Request `cursor` | Trace the page that produced each row. |
| `next_cursor` | Response `next_cursor` | Resume after storing the current page. |

## Twitter API timeline pagination

Process one home-feed page at a time.
Use each returned tweet ID to deduplicate timeline tweets. Save each page before
advancing its cursor. Keep the prior cursor until validating its replacement.

Send the last `next_cursor` as `cursor` after storing the full page. Pass
processed tweet IDs through `seenTweetIds` to reduce repeat rows. Stop when
`has_next_page` is false or `next_cursor` is empty.

Respect the `Retry-After` header after a 429 or 503 response.
After a 424 `account_required` response, connect an X account first.
After a 410 `cursor_account_unavailable` response, start again without the cursor.
After other 424 or 502 responses, retry the stored cursor.
Never advance a checkpoint after a failed destination write.

## Route home timeline tweets

| Routing rule | Concrete match | Destination |
| - | - | - |
| Support reply | Approved account or keyword | Support review queue |
| Campaign mention | Campaign phrase in `text` | Campaign verification queue |
| Media review | Non-empty `media` | Image or video review |
| High engagement | Approved likes, replies, reposts, or views | Priority research queue |
| No rule match | No approved condition matches | Store without generating a notification |

Keep each rule name beside its tweet ID. Keep the original tweet text.
Reprocess a tweet only after its routing rule changes.

## Twitter timeline API questions

### How do you authenticate timeline requests?

Send an Xquik API key through the `x-api-key` header. Keep keys server-side.
Never expose a key in a browser, mobile bundle, or public repository.

### Can you filter home timeline tweets by hashtag?

No. `GET /x/timeline` returns the connected account's ranked home feed. Use
[tweet search](/api-reference/x/search-tweets) for keyword, author, date, or media
filters.

### Can you display timeline tweets in an app?

Yes. Render `tweets[]` with the returned text, author, media, and tweet URL.
Store tweet IDs for deduplication. Refresh from the last confirmed cursor.

### How should timeline API errors be retried?

Fix authentication after a 401 response. Add credits after a 402 response.
Connect an X account after `424 account_required`. Start again without the
cursor after `410 cursor_account_unavailable`. Respect `Retry-After` after 429
or 503. Resume from the stored cursor after other 424 or 502 responses.

## Query parameters

<ParamField query="cursor" type="string">
  Pagination cursor. Pass the `next_cursor` value from the previous response to fetch the next page. The next page comes from the same connected X account.
</ParamField>

<ParamField query="seenTweetIds" type="string">
  Comma-separated tweet IDs to exclude from results. Ignore empty entries. Use this to skip tweets the user has already seen.
</ParamField>

## Which timeline endpoint?

<CardGroup cols={2}>
  <Card title="Home timeline" icon="house">
    Use `GET /x/timeline` for the connected account's home feed.
  </Card>

  <Card title="Profile timeline" icon="user-round">
    Use [`GET /x/users/{id}/tweets`](/api-reference/x/user-tweets) for one
    public profile's timeline.
  </Card>

  <Card title="Mentions timeline" icon="at-sign">
    Use [`GET /x/users/{id}/mentions`](/api-reference/x/user-mentions) for
    public mentions of one account.
  </Card>

  <Card title="Saved tweets" icon="bookmark">
    Use [`GET /x/bookmarks`](/api-reference/x/bookmarks) for tweets the
    connected account saved.
  </Card>

  <Card title="Notifications" icon="bell">
    Use [`GET /x/notifications`](/api-reference/x/notifications) for compact
    inbox activity rows.
  </Card>

  <Card title="Monitor events" icon="radio">
    Use [`List events`](/api-reference/events/list) after account or keyword
    monitors have captured replayable webhook events.
  </Card>
</CardGroup>

## Headers

<ParamField header="x-api-key" type="string" required>
  Your API key. You can also authenticate with an OAuth bearer token.
</ParamField>

## Response

### 200 OK

<ResponseField name="tweets" type="object[]">
  Array of timeline tweets.
  **Tweet object fields.**

  <ResponseField name="id" type="string">Tweet ID.</ResponseField>
  <ResponseField name="text" type="string">Contains the tweet text.</ResponseField>
  <ResponseField name="type" type="string">Returns the tweet type when available.</ResponseField>
  <ResponseField name="createdAt" type="string">Returns an ISO 8601 timestamp when available.</ResponseField>
  <ResponseField name="isNoteTweet" type="boolean">Marks long-form Note Tweets when available.</ResponseField>
  <ResponseField name="isPinned" type="boolean">Whether the author pinned this post to their profile. Omitted if unavailable.</ResponseField>
  <ResponseField name="likeCount" type="number">Counts likes when available.</ResponseField>
  <ResponseField name="retweetCount" type="number">Counts reposts when available.</ResponseField>
  <ResponseField name="replyCount" type="number">Counts replies when available.</ResponseField>
  <ResponseField name="quoteCount" type="number">Counts quote tweets when available.</ResponseField>
  <ResponseField name="viewCount" type="number">Counts views when available.</ResponseField>
  <ResponseField name="bookmarkCount" type="number">Counts bookmarks when available.</ResponseField>
  <ResponseField name="url" type="string">Links to the tweet on X when available.</ResponseField>
  <ResponseField name="lang" type="string">Identifies the tweet language when available.</ResponseField>
  <ResponseField name="isReply" type="boolean">Marks replies when available.</ResponseField>
  <ResponseField name="inReplyToId" type="string">Identifies the parent tweet for a reply.</ResponseField>
  <ResponseField name="inReplyToUserId" type="string">Identifies the replied-to user.</ResponseField>
  <ResponseField name="inReplyToUsername" type="string">Identifies the replied-to username.</ResponseField>
  <ResponseField name="conversationId" type="string">Identifies the conversation when available.</ResponseField>
  <ResponseField name="source" type="string">Identifies the posting client when available.</ResponseField>
  <ResponseField name="displayTextRange" type="number[]">Provides rendered text offsets when available.</ResponseField>
  <ResponseField name="isLimitedReply" type="boolean">Shows whether X limits replies.</ResponseField>
  <ResponseField name="isQuoteStatus" type="boolean">Marks quote tweets when available.</ResponseField>
  <ResponseField name="isRetweet" type="boolean">Whether this row is a retweet. `text` carries the original post in full.</ResponseField>
  <ResponseField name="entities" type="object">Returns parsed entities when available.</ResponseField>
  <ResponseField name="contentDisclosure" type="object">Describes paid partnerships and AI-generated media. Includes `advertising.isPaidPromotion` and `aiGenerated.hasAiGeneratedMedia` when X returns them.</ResponseField>

  <ResponseField name="author" type="object">
    Returns the tweet author when available.
    **Author object fields.**
    <ResponseField name="id" type="string">Identifies the author.</ResponseField>
    <ResponseField name="username" type="string">Returns the current X username.</ResponseField>
    <ResponseField name="name" type="string">Returns the display name.</ResponseField>
    <ResponseField name="followers" type="number">Counts the author's followers.</ResponseField>
    <ResponseField name="verified" type="boolean">Shows whether X marks the author as verified.</ResponseField>
    <ResponseField name="profilePicture" type="string">Returns the profile image URL when available.</ResponseField>
  </ResponseField>

  <ResponseField name="media" type="object[]">
    Lists attached images, GIFs, or videos when available.
    **Media item fields.**
    <ResponseField name="mediaUrl" type="string">Returns the direct media URL.</ResponseField>
    <ResponseField name="videoVariants" type="object[]">Lists video renditions with bitrates, content types, and URLs.</ResponseField>
    <ResponseField name="type" type="string">Identifies the media type.</ResponseField>
    <ResponseField name="url" type="string">Returns the shortened URL from the tweet text.</ResponseField>
  </ResponseField>

  <ResponseField name="quoted_tweet" type="object">Embeds the quoted tweet when present.</ResponseField>
  <ResponseField name="retweeted_tweet" type="object">Embeds the original repost when present.</ResponseField>
</ResponseField>

<ResponseField name="has_next_page" type="boolean">
  Shows whether more tweets remain.
</ResponseField>

<ResponseField name="next_cursor" type="string">
  Provides the next page cursor. Empty after the final page.
</ResponseField>

```json theme={null}
{
  "tweets": [
    {
      "id": "1893456789012345678",
      "text": "Timeline tweet content",
      "createdAt": "2026-02-24T10:00:00.000Z",
      "likeCount": 200,
      "retweetCount": 50,
      "replyCount": 15,
      "url": "https://x.com/user/status/1893456789012345678",
      "author": {
        "id": "44196397",
        "username": "elonmusk",
        "name": "Elon Musk",
        "followers": 150000000,
        "verified": true,
        "profilePicture": "https://pbs.twimg.com/profile_images/example.jpg"
      }
    }
  ],
  "has_next_page": true,
  "next_cursor": "DAADDAABCgABF..."
}
```

### 401 Unauthenticated

```json theme={null}
{ "error": "unauthenticated" }
```

Missing or invalid API key.

### 402 Insufficient credits

```json theme={null}
{ "error": "insufficient_credits" }
```

Metered access requires enough available credits. Possible error values include `no_subscription`, `subscription_inactive`, `no_credits`, and `insufficient_credits`.

### 410 Cursor account unavailable

```json theme={null}
{
  "error": "cursor_account_unavailable",
  "message": "The X account for this cursor is unavailable. Start again without the cursor."
}
```

The X account that returned the cursor can no longer read. Request the first
page again without `cursor`.

### 429 Rate limit exceeded

```json theme={null}
{
  "error": "rate_limit_exceeded",
  "message": "Too many requests. Try again later.",
  "retryAfter": 60
}
```

The API key, user, or plan tier is sending requests too fast. Respect the `Retry-After` header before retrying.

### 502 X API unavailable

```json theme={null}
{ "error": "x_api_unavailable" }
```

The read service failed. Retry after a short delay.

### 424 Dependency failed

```json theme={null}
{ "error": "x_api_unavailable" }
```

The opt-in normalized v1 contract returns `424` when the read service fails.
Send `xquik-api-contract: 2026-04-29` to opt in. Default v1 returns `502`.

Without a connected X account, the route returns `424 account_required`. This
status needs no contract header.

```json theme={null}
{
  "error": "account_required",
  "message": "This read needs your own connected X account. Connect or restore it at dashboard.xquik.com/account, then retry."
}
```

<Note>
  **Related.** [Notifications](/api-reference/x/notifications) · [Bookmarks](/api-reference/x/bookmarks)
</Note>

<div className="related-api-links">
  <Accordion title="Related timeline, bookmark & notification APIs" icon="link">
    * Account feeds: [Home timeline](/api-reference/x/timeline) · [Notifications](/api-reference/x/notifications) · [Mentions](/api-reference/x/user-mentions)
    * Saved and private: [Bookmarks](/api-reference/x/bookmarks) · [Bookmark folders](/api-reference/x/bookmark-folders) · [DM history](/api-reference/x/dm-history)
  </Accordion>
</div>


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