> ## 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 direct message API: send DMs, read history & media

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

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

Use this workflow for support, sales, community, or agent systems.
Read DM history and send direct messages from a connected X account.
Xquik reads participant-scoped conversations through `GET /x/dm/{userId}/history`.
It sends text DMs through `POST /x/dm/{userId}`.
It can attach one uploaded media item through `media_ids`.
It deletes a DM through `DELETE /x/dm/{userId}/messages/{messageId}`.

## 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`, 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"
  },
  "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` and `username` 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"
}
```

Trust the DM write response when recording delivery.

## Step 2: read direct message history

Use the sender account as the `account` query parameter.
The connected account must belong to the conversation.
Direct messages remain 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 deduplicate 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, store 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 next 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. Attachment
audits can then 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"
}
```

## Step 5: delete a direct message

Delete a DM from the sender account's side with its `message_id`.
The other person keeps their copy.

```bash theme={null}
curl -X DELETE "https://xquik.com/api/v1/x/dm/987654321/messages/1893726451029384193?account=myxhandle" \
  -H "x-api-key: xq_YOUR_KEY_HERE" \
  -H "Idempotency-Key: dm-delete-1893726451029384193" | jq
```

A `200` means the conversation no longer shows the DM. It costs 10 credits.
`422 x_dm_not_deleted` means X still shows it. You pay nothing.
After the `200`, mark the stored `message_id` row as deleted.

## Twitter DM API questions

### How does the X direct message API work?

The X direct message API sends one-to-one messages from a connected X account.
Use a numeric user ID for each recipient.
Send text through `POST /x/dm/{userId}`.
This Twitter DM API workflow also reads participant-scoped message history.
Store each returned `messageId`.
Keep that ID for matching records and audit logs.
Use a connected Twitter account for each private message.
Reuse the idempotency key only for the identical request.
Poll non-terminal write actions through their status URL.
Use a new key only when `safeToRetry` is `true`.

### How do I send a Twitter DM through an API?

A Twitter API DM send starts with recipient lookup.
Call `GET /x/users/{id}` when you only know the username.
Then call `POST /x/dm/{userId}` with `account` and `text`.
The Twitter direct message API returns `messageId` and `success`.
Store both before any CRM, queue, or agent handoff.

### Can the API receive Twitter DMs in real time?

Xquik reads saved history on demand.
This route does not register real-time DM events.
Poll `GET /x/dm/{userId}/history` when the workflow needs new messages.
Omit `cursor` from the first history request.
Then pass each response's `next_cursor` value as the next request's `cursor`.
A Twitter DM history API client should deduplicate each message `id`.

### Why does sending a DM return 403 or 422?

A `403` can indicate a disconnected or non-participant account.
Reconnect accounts that need reauthorization.
Read private conversations through an account that belongs to them.
A `422` means X rejected the request.
Check recipient permissions, message text, and sender account state.
Do not retry an unchanged rejected request.

### Can this API create group DM conversations?

This route accepts one recipient `userId`.
The Xquik contract does not create group conversations.
Do not pass participant arrays to the one-to-one send route.
Check the OpenAPI contract before sending message conversations.

### How do I send a DM with media?

Upload one image, GIF, or video through `POST /x/media`.
Use the returned `mediaId` in a one-item `media_ids` array.
To send DM with media, include non-empty text too.
Send one media ID.
Empty or multiple media IDs fail.
Keep upload and message IDs together for private attachment audits.

### Is Twitter DM automation suitable for welcome messages?

Use Twitter DM automation only for expected, approved customer conversations.
Never send unsolicited welcome messages to new followers.
Review [X developer guidance](https://docs.x.com/developer-guidelines) before sending DMs.
Keep message bodies inside restricted support or CRM systems.
Store IDs and delivery status in shared operational logs.
Give recipients a clear way to stop automated messages.

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

  <Card title="DM delete" icon="trash-2">
    `DELETE /x/dm/{userId}/messages/{messageId}` costs 10 credits, only when X deletes the DM.
  </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">
    X rejected the send with `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_dm_not_deleted" icon="trash-2">
    X still shows the DM after a delete. You pay nothing. Retry with a new `Idempotency-Key`.
  </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="braces">
    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) · [Delete Direct Message](/api-reference/x-write/delete-dm) · [Get DM History](/api-reference/x/dm-history) · [Get User](/api-reference/x/twitter-profile-lookup) · [Media Upload Workflow](/guides/media-upload-workflow)
</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.