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

# Get DM history

> Read participant-scoped X direct message history with a connected account, store private rows, and resume older pages with next_cursor

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

Use this endpoint to sync participant-scoped DM pages before a support, CRM,
warehouse, or agent workflow replies. Pass the connected participant account in
`account`, store message IDs plus `next_cursor`, and keep full message text only
in private systems.

<Note>
  Requires a connected X account passed via the `account` query parameter. DM history is participant-scoped, so pass the connected account that belongs to the conversation.
</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/get-user) 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>

The examples normalize each page into private `dm_history` rows. Store
`message_id`, `sender_id`, `receiver_id`, `message_text`, `created_at`,
optional `media_url`, `conversation_user_id`, `sender_account`, and
`page_next_cursor`. Keep `message_text` only in private systems; use the IDs,
timestamp, media URL, and job status in shared logs.

## Path parameters

<ParamField path="userId" type="string" required>
  Target X user ID for the DM conversation (numeric string).
</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, so 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. Pass the `next_cursor` value from the previous response 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. Session cookie authentication is also supported.
</ParamField>

## Response

<Tabs>
  <Tab title="200 OK">
    <ResponseField name="messages" type="object[]">
      Array of direct messages.

      <Expandable title="message object">
        <ResponseField name="id" type="string">Message ID.</ResponseField>
        <ResponseField name="text" type="string">Message text. Omitted if unavailable.</ResponseField>
        <ResponseField name="senderId" type="string">Sender user ID.</ResponseField>
        <ResponseField name="receiverId" type="string">Receiver user ID.</ResponseField>
        <ResponseField name="createdAt" type="string">ISO 8601 timestamp. Omitted if unavailable.</ResponseField>
        <ResponseField name="mediaUrl" type="string">URL of attached media (image, GIF, or video). Omitted when the message has no media attachment.</ResponseField>
      </Expandable>
    </ResponseField>

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

    <ResponseField name="next_cursor" type="string">
      Opaque cursor for the next page. Empty string when no more messages.
    </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"
    }
    ```
  </Tab>

  <Tab title="400 Invalid user ID">
    ```json theme={null}
    { "error": "invalid_user_id" }
    ```

    The user ID is empty or invalid.
  </Tab>

  <Tab title="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.
  </Tab>

  <Tab title="401 Unauthenticated">
    ```json theme={null}
    { "error": "unauthenticated" }
    ```

    Missing or invalid API key.
  </Tab>

  <Tab title="402 Subscription required">
    ```json theme={null}
    { "error": "no_subscription" }
    ```

    No active subscription or insufficient credits. Possible error values: `no_subscription`, `subscription_inactive`, `no_credits`, `insufficient_credits`.
  </Tab>

  <Tab title="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. The connected account is not a participant in this conversation, or it needs reauthentication. Reconnect the account and retry.
  </Tab>

  <Tab title="403 Account restricted">
    ```json theme={null}
    { "error": "account_restricted" }
    ```

    The connected X account is suspended, locked, or otherwise restricted. Use a different connected account.
  </Tab>

  <Tab title="403 Account needs reauth">
    ```json theme={null}
    { "error": "account_needs_reauth" }
    ```

    The connected account needs to be reauthenticated. Reconnect it from the dashboard.
  </Tab>

  <Tab title="404 Account not found">
    ```json theme={null}
    { "error": "account_not_found" }
    ```

    The requested connected X account was not found. Connect it first or pass another account handle.
  </Tab>

  <Tab title="429 Rate Limit Exceeded">
    ```json theme={null}
    { "error": "rate_limit_exceeded", "retryAfter": 60 }
    ```

    Your tier rate limit was exceeded. Wait for the `Retry-After` header before retrying.
  </Tab>

  <Tab title="424 Dependency Failed">
    ```json theme={null}
    { "error": "x_api_unavailable" }
    ```

    The normalized v1 response contract can return 424 when the read service is unavailable.
  </Tab>

  <Tab title="502 X API unavailable">
    ```json theme={null}
    { "error": "x_api_unavailable" }
    ```

    The read service returned an error. Retry after a short delay.
  </Tab>
</Tabs>

## 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="Dedupe 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="Preserve 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) for
  lookup, participant-scoped history sync, `messageId` storage, and media
  handoff; [Get User](/api-reference/x/get-user) to resolve the recipient
  `userId`; [Send DM](/api-reference/x-write/send-dm) to reply from the
  connected account; [Upload Media](/api-reference/x-write/upload-media) when
  a reply needs one uploaded `mediaId`.
</Note>
