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

# Send X direct messages and store message IDs

> Look up a user ID, read DM history, send an X direct message, store the returned message ID, attach one uploaded media item, and handle DM errors.

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

Use this workflow when a support, sales, community, or agent system needs to read direct message history and send direct messages from a connected X account. Xquik reads a participant-scoped DM conversation with `GET /x/dm/{userId}/history`, sends text DMs with `POST /x/dm/{userId}`, and can attach one uploaded media item with `media_ids`.

## Choose the DM path

<CardGroup cols={2}>
  <Card title="Text-only send" icon="send">
    Call `POST /x/dm/{userId}` with `account` and non-empty `text`. Store
    `messageId`, `success`, sender account, and recipient ID.
  </Card>

  <Card title="Media send" icon="paperclip">
    Call `POST /x/media` first, then send `media_ids: ["<mediaId>"]` on
    `POST /x/dm/{userId}`. Use exactly 1 item and store `media_id` beside
    `messageId`.
  </Card>

  <Card title="History sync" icon="history">
    Call `GET /x/dm/{userId}/history` with `account` before replying when the
    workflow needs private conversation context. Store `messages`,
    `has_next_page`, and `next_cursor`.
  </Card>

  <Card title="Username input" icon="user-search">
    Call `GET /x/users/{id}` first when the app only has a username. DM sends
    require the numeric recipient ID in the path.
  </Card>
</CardGroup>

## When to use this workflow

<CardGroup cols={2}>
  <Card title="Look up the recipient" icon="user">
    Use `GET /x/users/{id}` to convert a username to the numeric recipient ID required by DM writes.
  </Card>

  <Card title="Read message history" icon="message-square">
    Use `GET /x/dm/{userId}/history` with `account` to sync participant-scoped messages.
  </Card>

  <Card title="Send a text DM" icon="send">
    Use `POST /x/dm/{userId}` with `account` and `text`, then store the returned `messageId`.
  </Card>

  <Card title="Attach one media item" icon="image">
    Upload media first, then pass one `mediaId` in `media_ids`.
  </Card>

  <Card title="Avoid bad retries" icon="refresh-cw">
    Retry only `429` and `503`; fix `400`, `402`, `403`, and `422` first.
  </Card>
</CardGroup>

## Data you get

<CardGroup cols={2}>
  <Card title="Recipient profile" icon="search">
    `GET /x/users/{id}` returns recipient `id`, `username`, `name`, optional `canDm`, and profile fields.
  </Card>

  <Card title="History page" icon="history">
    `GET /x/dm/{userId}/history` returns `messages`, `has_next_page`, and `next_cursor`.
  </Card>

  <Card title="Send result" icon="circle-check">
    `POST /x/dm/{userId}` returns `messageId` and `success` for outbound handoff storage.
  </Card>

  <Card title="Media upload" icon="paperclip">
    `POST /x/media` returns `mediaId`, `mediaUrl`, and `success` before the one-item DM attachment send.
  </Card>
</CardGroup>

## End-to-end direct message handoff

Use one checkpoint object after recipient lookup, history sync, a text send, and
an optional media send. Keep full DM bodies in restricted support, CRM,
warehouse, or agent memory systems; shared run logs should carry IDs, status,
cursors, and media references.

```json theme={null}
{
  "workflow": "direct_message_handoff",
  "recipient_lookup": {
    "id": "987654321",
    "username": "username",
    "canDm": true
  },
  "history_page": {
    "account": "myxhandle",
    "conversation_user_id": "987654321",
    "messages_count": 25,
    "page_cursor": null,
    "next_cursor": "1893726451029384190",
    "has_next_page": true
  },
  "text_send": {
    "endpoint": "/api/v1/x/dm/987654321",
    "account": "myxhandle",
    "recipient_user_id": "987654321",
    "message_id": "1893726451029384192",
    "success": true,
    "send_status": "sent"
  },
  "media_send": {
    "upload_media_id": "1893726451023847424",
    "media_ids": ["1893726451023847424"],
    "media_url": "https://media.example.com/support-image.png",
    "message_id": "1893726451029384193",
    "success": true,
    "send_status": "sent"
  },
  "audit_row": {
    "record_type": "dm_handoff_checkpoint",
    "sender_account": "myxhandle",
    "recipient_user_id": "987654321",
    "message_id": "1893726451029384193",
    "media_id": "1893726451023847424",
    "source_endpoint": "/api/v1/x/dm/987654321",
    "handoff_format": "jsonl",
    "message_text_storage": "restricted_system_only"
  },
  "handoff_state": "store_message_ids_and_private_rows"
}
```

<CardGroup cols={2}>
  <Card title="Recipient checkpoint" icon="user-check">
    Store the numeric `id`, `username`, and optional `canDm` preflight hint from user lookup.
  </Card>

  <Card title="History checkpoint" icon="history">
    Store `messages_count`, `next_cursor`, and `has_next_page` for each synced history page.
  </Card>

  <Card title="Text send checkpoint" icon="send">
    Store the `message_id`, `success`, `send_status`, sender account, and recipient ID after `POST /x/dm/{userId}`.
  </Card>

  <Card title="Media checkpoint" icon="image">
    Store exactly one uploaded `media_id` beside the returned DM `message_id`.
  </Card>

  <Card title="Audit checkpoint" icon="file-check">
    Keep shared audit rows limited to IDs, cursors, status, endpoints, and media references.
  </Card>

  <Card title="Invalid fields" icon="ban">
    Do not pass `reply_to_message_id`, empty `media_ids`, or more than one media ID.
  </Card>
</CardGroup>

## Step 1: Look up the recipient user ID

`POST /x/dm/{userId}` requires the numeric X user ID in the path. If you only have a username, call `GET /x/users/{id}` first.

```bash theme={null}
curl https://xquik.com/api/v1/x/users/username \
  -H "x-api-key: xq_YOUR_KEY_HERE" | jq
```

```json theme={null}
{
  "id": "987654321",
  "username": "username",
  "name": "Xquik",
  "canDm": true
}
```

When `canDm` is returned, use it as a preflight hint. If the field is omitted, rely on the DM write response.

## Step 2: Read direct message history

Use the sender account as the `account` query parameter. The connected account must be a participant in the conversation, because direct messages are private and user-scoped.

```bash theme={null}
curl -G https://xquik.com/api/v1/x/dm/987654321/history \
  --data-urlencode "account=myxhandle" \
  -H "x-api-key: xq_YOUR_KEY_HERE" | jq
```

```json theme={null}
{
  "messages": [
    {
      "id": "1893726451029384191",
      "text": "Can you send the setup link?",
      "senderId": "987654321",
      "receiverId": "123456789",
      "createdAt": "2026-02-24T10:00:00.000Z"
    }
  ],
  "has_next_page": true,
  "next_cursor": "1893726451029384190"
}
```

For older messages, pass the previous `next_cursor` as `cursor`. Store each message `id`, `text`, `senderId`, `receiverId`, `createdAt`, and optional `mediaUrl` in your support ticket, CRM note, or JSON export.

<Note>
  DM history and outbound `message_text` values can contain private customer or community conversations. Store them only in private support, CRM, warehouse, or agent memory systems. Shared logs, public artifacts, and status dashboards should keep `message_id`, `sender_id`, `receiver_id`, `created_at`, `media_url`, and job status instead of full DM bodies.
</Note>

### Sync and retry rules

<CardGroup cols={2}>
  <Card title="Opaque cursor" icon="shuffle">
    Treat `next_cursor` as opaque. Pass it back as `cursor` for the next page.
    Do not decode it or build your own cursor.
  </Card>

  <Card title="Store message IDs" icon="key-round">
    Store every message `id`. Use the ID to dedupe support tickets, CRM notes,
    warehouse rows, or JSON exports.
  </Card>

  <Card title="Participant account" icon="user-check">
    Use a participant account. `GET /x/dm/{userId}/history` returns
    `400 account_required` without `account` and `403 dm_not_permitted` when
    the connected account is not in the conversation.
  </Card>

  <Card title="Modern pagination" icon="history">
    Use `cursor`; keep `maxId` only for older integrations that already depend on it.
  </Card>

  <Card title="Transient retries only" icon="refresh-cw">
    Retry `429` and `503`; do not retry `403 dm_not_permitted` with the same non-participant account.
  </Card>
</CardGroup>

## Step 3: Send a text direct message

Use a connected account as the sender. The `account` value can be the connected account username or account ID.

```bash theme={null}
curl -X POST https://xquik.com/api/v1/x/dm/987654321 \
  -H "x-api-key: xq_YOUR_KEY_HERE" \
  -H "Content-Type: application/json" \
  -d '{
    "account": "myxhandle",
    "text": "Thanks for reaching out. Here is the next step."
  }' | jq
```

```json theme={null}
{
  "messageId": "1893726451029384192",
  "success": true
}
```

### Store the outbound handoff

After a `200` response, persist one outbound record before handing control back to a CRM, ticket, queue, or agent. `POST /x/dm/{userId}` returns `messageId` and `success`; use your own job timestamp if the downstream system needs `sent_at`.

```json theme={null}
{
  "status": "sent",
  "recipient_user_id": "987654321",
  "sender_account": "myxhandle",
  "message_id": "1893726451029384192",
  "message_text": "Thanks for reaching out. Here is the next step."
}
```

Text-only DM sends omit media fields. Add `media_ids` in the request and
`media_id` in the handoff only for the media send in Step 4.

When you also sync history, normalize each `messages[]` item separately with `message_id`, `sender_id`, `receiver_id`, `created_at`, optional `media_url`, and `conversation_user_id`.

### JSON Lines handoff

For queue, warehouse, CRM, or agent memory, write one record per history message or outbound send to `xquik-dm-handoff.jsonl`.

```json theme={null}
{
  "record_type": "dm_history",
  "conversation_user_id": "987654321",
  "sender_account": "myxhandle",
  "message_id": "1893726451029384191",
  "sender_id": "987654321",
  "receiver_id": "123456789",
  "created_at": "2026-02-24T10:00:00.000Z",
  "message_text": "Can you send the setup link?",
  "page_next_cursor": "1893726451029384190"
}
```

```json theme={null}
{
  "record_type": "dm_send",
  "status": "sent",
  "conversation_user_id": "987654321",
  "sender_account": "myxhandle",
  "recipient_user_id": "987654321",
  "message_id": "1893726451029384192",
  "message_text": "Thanks for reaching out. Here is the next step.",
  "handoff_format": "jsonl"
}
```

History rows include `media_url` only when the message has media. Text-only send
rows omit `media_id` and `media_ids`; media send rows use the Step 4 shape.

## Step 4: Send a direct message with media

Upload media first, then send exactly one uploaded media ID in `media_ids`.

```bash theme={null}
curl -X POST https://xquik.com/api/v1/x/media \
  -H "x-api-key: xq_YOUR_KEY_HERE" \
  -H "Content-Type: application/json" \
  -d '{
    "account": "myxhandle",
    "url": "https://example.com/support-image.png"
  }' | jq
```

```bash theme={null}
curl -X POST https://xquik.com/api/v1/x/dm/987654321 \
  -H "x-api-key: xq_YOUR_KEY_HERE" \
  -H "Content-Type: application/json" \
  -d '{
    "account": "myxhandle",
    "text": "Here is the requested image.",
    "media_ids": ["1893726451023847424"]
  }' | jq
```

DMs accept one uploaded media ID. Do not pass multiple IDs, an empty array, or `reply_to_message_id`.

The media DM send returns the same `messageId` and `success` fields as a text
DM. Store the uploaded `media_id` beside that returned message ID so attachment
audits can join the upload, recipient, sender account, and outbound message.

```json theme={null}
{
  "messageId": "1893726451029384193",
  "success": true
}
```

```json theme={null}
{
  "record_type": "dm_media_send",
  "conversation_user_id": "987654321",
  "sender_account": "myxhandle",
  "recipient_user_id": "987654321",
  "message_id": "1893726451029384193",
  "message_text": "Here is the requested image.",
  "media_ids": ["1893726451023847424"],
  "media_id": "1893726451023847424",
  "handoff_format": "jsonl"
}
```

## Costs

<CardGroup cols={2}>
  <Card title="Recipient lookup" icon="search">
    `GET /x/users/{id}` costs 1 credit per call.
  </Card>

  <Card title="DM history" icon="history">
    `GET /x/dm/{userId}/history` costs 1 credit per message returned.
  </Card>

  <Card title="Media upload" icon="paperclip">
    `POST /x/media` costs 10 credits per upload call before a media DM send.
  </Card>

  <Card title="DM send" icon="send">
    `POST /x/dm/{userId}` costs 10 credits per call and returns `messageId`.
  </Card>
</CardGroup>

## Error handling

<CardGroup cols={2}>
  <Card title="400 invalid_input" icon="circle-alert">
    Check `account`, `text`, `userId`, and one-item `media_ids`.
  </Card>

  <Card title="400 account_required" icon="user-check">
    Pass the connected sender handle as `account` when reading DM history.
  </Card>

  <Card title="402 billing state" icon="credit-card">
    Subscribe or top up credits before retrying.
  </Card>

  <Card title="403 account_needs_reauth" icon="refresh-cw">
    Reconnect the sender account from the dashboard.
  </Card>

  <Card title="403 dm_not_permitted" icon="user-check">
    Use a connected account that participates in the conversation, or reconnect the account.
  </Card>

  <Card title="422 x_dm_not_allowed" icon="message-circle">
    `422 x_dm_not_allowed`. The recipient may not accept DMs from this connected account. Do not retry unchanged; use another permitted account or ask the recipient to allow messages.
  </Card>

  <Card title="422 x_rejected" icon="circle-alert">
    Check the recipient, account state, and message content before retrying.
  </Card>

  <Card title="429 or 503" icon="refresh-cw">
    Retry with exponential backoff and respect `Retry-After` when present.
  </Card>
</CardGroup>

## Handoff checklist

<CardGroup cols={2}>
  <Card title="Sender" icon="user-check">
    Store the connected X account username or ID sent in `account`.
  </Card>

  <Card title="Recipient" icon="user">
    Store the numeric recipient ID used in the `POST /x/dm/{userId}` path.
  </Card>

  <Card title="History" icon="history">
    For DM history exports, store `messages`, `has_next_page`, and
    `next_cursor`; pass `cursor` to fetch older messages.
  </Card>

  <Card title="Text" icon="message-square">
    Store the required non-empty `text` value.
  </Card>

  <Card title="Media" icon="image">
    Store the optional one-item `media_ids` array containing a `mediaId` from
    `POST /x/media`.
  </Card>

  <Card title="Response" icon="circle-check">
    Store `messageId`, `recipient_user_id`, `sender_account`, `message_text`,
    and optional `media_id` in private audit records or support systems.
  </Card>

  <Card title="JSON Lines" icon="file-json">
    Store history and send records in `xquik-dm-handoff.jsonl` for queues, warehouse loads, CRM syncs, or agent memory.
  </Card>
</CardGroup>

<Note>
  **Related:** [Send Direct Message](/api-reference/x-write/send-dm) · [Get DM History](/api-reference/x/dm-history) · [Get User](/api-reference/x/get-user) · [Media Upload Workflow](/guides/media-upload-workflow)
</Note>
