> ## 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 DM API for message history & CRM sync

> Read Twitter DMs between a connected account and one user. Store private message rows, keep sender and recipient IDs, and resume pages with next_cursor.

<Panel>
  <Tabs defaultTabIndex={0} sync={false}>
    <Tab title="200" id="response-x-dm-history-200">
      ```json theme={null}
      {
        "messages": [
          {
            "id": "1234567890123456789",
            "text": "Hey, how are you?",
            "senderId": "9876543210",
            "receiverId": "1234567890"
          }
        ],
        "has_next_page": true,
        "next_cursor": "DAACCgACGRElMJcAAA"
      }
      ```
    </Tab>

    <Tab title="400" id="response-x-dm-history-400">
      ```json theme={null}
      {
        "error": "invalid_user_id"
      }
      ```
    </Tab>

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

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

    <Tab title="403" id="response-x-dm-history-403">
      ```json theme={null}
      {
        "error": "dm_not_permitted",
        "message": "X rejected the DM read. The connected account is not a participant in this conversation, or it needs reauthentication. Reconnect the account on the dashboard and try again."
      }
      ```
    </Tab>

    <Tab title="404" id="response-x-dm-history-404">
      ```json theme={null}
      {
        "error": "account_not_found",
        "message": "X account not found. Connect it first at /dashboard/account?tab=x-accounts."
      }
      ```
    </Tab>

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

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

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

    <Tab title="503" id="response-x-dm-history-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>

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

This route reads the DM history between a connected account and one user.
Pass the connected account through `account`. Store message IDs and
`next_cursor` in private systems. Keep full message text private. Use
[Send DM](/api-reference/x-write/send-dm) when the workflow needs a reply.

Compare fields with [X's Direct Messages lookup guide](https://docs.x.com/x-api/direct-messages/lookup/introduction).
Xquik returns the normalized message fields listed below.

<Note>
  Requires a connected X account passed via the `account` query parameter. Only a participant can read a conversation. Pass the connected account that belongs to it.
</Note>

<Note>
  DM history responses can contain private message text. Store them in a
  private support, CRM, warehouse, or agent memory system.
  Do not write full DM bodies to shared logs or public artifacts.
</Note>

## Which DM workflow?

<CardGroup cols={2}>
  <Card title="Read conversation history" icon="history">
    Use `GET /x/dm/{userId}/history` with `account`, then store
    `messages[].id` and `next_cursor` in a private system.
  </Card>

  <Card title="Send text reply" icon="send">
    Use [`POST /x/dm/{userId}`](/api-reference/x-write/send-dm), pass the same
    connected `account`, and store the returned `messageId`.
  </Card>

  <Card title="Send one media item" icon="image">
    Use [`POST /x/media`](/api-reference/x-write/upload-media) first, then pass
    the returned media ID as the only `media_ids` item on the DM send.
  </Card>

  <Card title="Resolve participant ID" icon="user-search">
    Use [`GET /x/users/{id}`](/api-reference/x/twitter-profile-lookup) when a workflow starts
    from a handle and needs the numeric `userId` for history or send calls.
  </Card>
</CardGroup>

<CodeGroup>
  ```bash cURL theme={null}
  curl -G https://xquik.com/api/v1/x/dm/44196397/history \
    --data-urlencode "account=your_handle" \
    -H "x-api-key: xq_YOUR_KEY_HERE" | jq

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

  ```javascript Node.js theme={null}
  const userId = "44196397";
  const account = "your_handle";

  function historyUrl(userId, account, cursor) {
    const params = new URLSearchParams({ account });
    if (cursor) params.set("cursor", cursor);
    return `https://xquik.com/api/v1/x/dm/${userId}/history?${params}`;
  }

  function toDmHistoryRows(page, { account, userId }) {
    return page.messages.map((message) => ({
      record_type: "dm_history",
      conversation_user_id: userId,
      sender_account: account,
      message_id: message.id,
      sender_id: message.senderId,
      receiver_id: message.receiverId,
      message_text: message.text ?? null,
      created_at: message.createdAt ?? null,
      media_url: message.mediaUrl ?? null,
      page_next_cursor: page.has_next_page ? page.next_cursor : null,
      source_endpoint: `/api/v1/x/dm/${userId}/history`,
    }));
  }

  async function savePrivateDmHistoryRows(rows) {
    // Replace this with a private CRM, warehouse, or agent memory write.
    return rows.length;
  }

  const response = await fetch(historyUrl(userId, account), {
    headers: { "x-api-key": "xq_YOUR_KEY_HERE" },
  });
  const data = await response.json();
  const historyRows = toDmHistoryRows(data, { account, userId });
  await savePrivateDmHistoryRows(historyRows);

  // Paginate
  if (data.has_next_page) {
    const next = await fetch(historyUrl(userId, account, data.next_cursor), {
      headers: { "x-api-key": "xq_YOUR_KEY_HERE" },
    });
    const nextData = await next.json();
    const nextRows = toDmHistoryRows(nextData, { account, userId });
    await savePrivateDmHistoryRows(nextRows);
  }
  ```

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

  def to_dm_history_rows(page, account, user_id):
      return [
          {
              "record_type": "dm_history",
              "conversation_user_id": user_id,
              "sender_account": account,
              "message_id": message["id"],
              "sender_id": message["senderId"],
              "receiver_id": message["receiverId"],
              "message_text": message.get("text"),
              "created_at": message.get("createdAt"),
              "media_url": message.get("mediaUrl"),
              "page_next_cursor": page["next_cursor"] if page["has_next_page"] else None,
              "source_endpoint": f"/api/v1/x/dm/{user_id}/history",
          }
          for message in page["messages"]
      ]

  def save_private_dm_history_rows(rows):
      # Replace this with a private CRM, warehouse, or agent memory write.
      return len(rows)

  response = requests.get(
      "https://xquik.com/api/v1/x/dm/44196397/history",
      params={"account": "your_handle"},
      headers={"x-api-key": "xq_YOUR_KEY_HERE"},
  )
  data = response.json()
  history_rows = to_dm_history_rows(data, "your_handle", "44196397")
  save_private_dm_history_rows(history_rows)

  # Paginate
  while data["has_next_page"]:
      data = requests.get(
          "https://xquik.com/api/v1/x/dm/44196397/history",
          params={"account": "your_handle", "cursor": data["next_cursor"]},
          headers={"x-api-key": "xq_YOUR_KEY_HERE"},
      ).json()
      next_rows = to_dm_history_rows(data, "your_handle", "44196397")
      save_private_dm_history_rows(next_rows)
  ```
</CodeGroup>

These examples turn each page into private `dm_history` rows. Store
`message_id`, `sender_id`, `receiver_id`, `message_text`, and `created_at`.
Also store optional `media_url`, `conversation_user_id`, `sender_account`, and
`page_next_cursor` for each page. Keep `message_text` only in private systems.
Use IDs, timestamps, media URLs, and job status in shared logs.

## Path parameters

<ParamField path="userId" type="string" required>
  The other person in the conversation: user ID, username with or without `@`, or URL-encoded profile URL, such as `x.com/nasa`. See [path IDs](/api-reference/overview#path-ids).
</ParamField>

## Query parameters

<ParamField query="account" type="string" required>
  X handle (without the `@` prefix) of the connected X account used to read the conversation. DM history is participant-scoped. The account must belong to the conversation. Connect an account on the dashboard before calling this endpoint.
</ParamField>

<ParamField query="cursor" type="string">
  Pagination cursor. Use the previous response's `next_cursor` to fetch older messages.
</ParamField>

<ParamField query="maxId" type="string">
  Legacy pagination cursor. Use `cursor` for new integrations. When both are present, `cursor` takes precedence.
</ParamField>

## 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="messages" type="object[]">
  Contains direct messages.
  **Message object fields.**

  <ResponseField name="id" type="string">
    Identifies the message.
  </ResponseField>

  <ResponseField name="text" type="string">
    Contains message text when available.
  </ResponseField>

  <ResponseField name="senderId" type="string">
    Identifies the sender.
  </ResponseField>

  <ResponseField name="receiverId" type="string">
    Identifies the recipient.
  </ResponseField>

  <ResponseField name="createdAt" type="string">
    Reports the ISO 8601 timestamp when available.
  </ResponseField>

  <ResponseField name="mediaUrl" type="string">
    Links attached media when present.
  </ResponseField>
</ResponseField>

<ResponseField name="has_next_page" type="boolean">
  Reports whether older messages are available.
</ResponseField>

<ResponseField name="next_cursor" type="string">
  Provides the next-page cursor. Returns an empty string after the final page.
</ResponseField>

```json theme={null}
{
  "messages": [
    {
      "id": "1893456789012345678",
      "text": "Hey, great tool!",
      "senderId": "44196397",
      "receiverId": "987654321",
      "createdAt": "2026-02-24T10:00:00.000Z"
    }
  ],
  "has_next_page": true,
  "next_cursor": "1893456789012345677"
}
```

### 400 Invalid user ID

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

The user ID is empty or invalid.

### 400 Account required

```json theme={null}
{
  "error": "account_required",
  "message": "Provide ?account=<x_handle> for a connected X account. DM history requires a connected participant account."
}
```

The `account` query parameter was missing or empty. Pass the handle of a connected X account that participates in the conversation.

### 401 Unauthenticated

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

Supply a valid API key or OAuth bearer token.

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

### 403 DM not permitted

```json theme={null}
{
  "error": "dm_not_permitted",
  "message": "X rejected the DM read. The connected account is not a participant in this conversation, or it needs reauthentication. Reconnect the account on the dashboard and try again."
}
```

X rejected the DM read. Use a participating connected account. If it needs
reauthentication, reconnect it before retrying.

### 403 Account restricted

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

The connected X account is suspended, locked, or otherwise restricted. Use a different connected account.

### 403 Account needs reauth

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

Reconnect the connected account from the dashboard.

### 404 Account not found

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

Xquik did not find the requested connected X account. Connect it first or pass another account handle.

### 429 Rate limit exceeded

```json theme={null}
{ "error": "rate_limit_exceeded", "retryAfter": 60 }
```

You exceeded your tier's rate limit. Wait for `Retry-After` before retrying.

### 424 Dependency failed

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

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

### 502 X API unavailable

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

The read service failed. Retry after a short delay.

## Twitter DM API questions

### Can I retrieve DM history?

Yes. Call `GET /x/dm/{userId}/history` with a participating connected account.

### Can I send a reply through this endpoint?

No. Use [Send DM](/api-reference/x-write/send-dm). This endpoint reads history only.

### Does this endpoint create direct message webhooks?

No. It reads participant-scoped history when called. It does not register webhooks.

### How do I authenticate?

Send an API key or OAuth bearer token.

### How do I handle a rate limit?

Wait for `Retry-After`, then retry the same page.

## History sync handoff

Use this endpoint when a CRM, support desk, warehouse job, or agent needs
participant-scoped DM context before sending a reply.

<CardGroup cols={2}>
  <Card title="Deduplicate imported messages" icon="key-round">
    Store `messages[].id` as the external DM ID for CRM notes, support tickets,
    warehouse rows, or agent memory.
  </Card>

  <Card title="Keep participants" icon="users">
    Store `messages[].senderId` and `messages[].receiverId` with the connected
    `account` so each private conversation stays tied to the correct sender.
  </Card>

  <Card title="Resume older pages" icon="history">
    Store `next_cursor` when `has_next_page` is true, then pass it as `cursor`
    on the next sync job.
  </Card>

  <Card title="Keep media context" icon="image">
    Store optional `messages[].mediaUrl` with `messages[].createdAt` when a DM
    includes an image, GIF, or video attachment.
  </Card>
</CardGroup>

<Note>
  **Related.** [Direct Message Workflow](/guides/direct-message-workflow) covers
  lookup, participant-scoped history sync, `messageId` storage, and media
  handoff. [Get User](/api-reference/x/twitter-profile-lookup) resolves the recipient
  `userId`. [Send DM](/api-reference/x-write/send-dm) replies from the
  connected account. Use [Upload Media](/api-reference/x-write/upload-media) when
  a reply needs one uploaded `mediaId`.
</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.