Skip to main content
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

Text-only send

Call POST /x/dm/{userId} with account and non-empty text. Store messageId, success, sender account, and recipient ID.

Media send

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.

History sync

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.

Username input

Call GET /x/users/{id} first when the app only has a username. DM sends require the numeric recipient ID in the path.

When to use this workflow

Look up the recipient

Use GET /x/users/{id} to convert a username to the numeric recipient ID required by DM writes.

Read message history

Use GET /x/dm/{userId}/history with account to sync participant-scoped messages.

Send a text DM

Use POST /x/dm/{userId} with account and text, then store the returned messageId.

Attach one media item

Upload media first, then pass one mediaId in media_ids.

Avoid bad retries

Retry only 429 and 503; fix 400, 402, 403, and 422 first.

Data you get

Recipient profile

GET /x/users/{id} returns recipient id, username, name, optional canDm, and profile fields.

History page

GET /x/dm/{userId}/history returns messages, has_next_page, and next_cursor.

Send result

POST /x/dm/{userId} returns messageId and success for outbound handoff storage.

Media upload

POST /x/media returns mediaId, mediaUrl, and success before the one-item DM attachment send.

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.
{
  "workflow": "direct_message_handoff",
  "recipient_lookup": {
    "id": "987654321",
    "username": "xquikcom",
    "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"
}

Recipient checkpoint

Store the numeric id, username, and optional canDm preflight hint from user lookup.

History checkpoint

Store messages_count, next_cursor, and has_next_page for each synced history page.

Text send checkpoint

Store the message_id, success, send_status, sender account, and recipient ID after POST /x/dm/{userId}.

Media checkpoint

Store exactly one uploaded media_id beside the returned DM message_id.

Audit checkpoint

Keep shared audit rows limited to IDs, cursors, status, endpoints, and media references.

Invalid fields

Do not pass reply_to_message_id, empty media_ids, or more than one media ID.

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.
curl https://xquik.com/api/v1/x/users/xquikcom \
  -H "x-api-key: xq_YOUR_KEY_HERE" | jq
{
  "id": "987654321",
  "username": "xquikcom",
  "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.
curl -G https://xquik.com/api/v1/x/dm/987654321/history \
  --data-urlencode "account=myxhandle" \
  -H "x-api-key: xq_YOUR_KEY_HERE" | jq
{
  "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.
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.

Sync and retry rules

Opaque cursor

Treat next_cursor as opaque. Pass it back as cursor for the next page. Do not decode it or build your own cursor.

Store message IDs

Store every message id. Use the ID to dedupe support tickets, CRM notes, warehouse rows, or JSON exports.

Participant account

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.

Modern pagination

Use cursor; keep maxId only for older integrations that already depend on it.

Transient retries only

Retry 429 and 503; do not retry 403 dm_not_permitted with the same non-participant account.

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.
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
{
  "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.
{
  "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.
{
  "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"
}
{
  "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.
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
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.
{
  "messageId": "1893726451029384193",
  "success": true
}
{
  "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

Recipient lookup

GET /x/users/{id} costs 1 credit per call.

DM history

GET /x/dm/{userId}/history costs 1 credit per message returned.

Media upload

POST /x/media costs 10 credits per upload call before a media DM send.

DM send

POST /x/dm/{userId} costs 10 credits per call and returns messageId.

Error handling

400 invalid_input

Check account, text, userId, and one-item media_ids.

400 account_required

Pass the connected sender handle as account when reading DM history.

402 billing state

Subscribe or top up credits before retrying.

403 account_needs_reauth

Reconnect the sender account from the dashboard.

403 dm_not_permitted

Use a connected account that participates in the conversation, or reconnect the account.

422 x_dm_not_allowed

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.

422 x_rejected

Check the recipient, account state, and message content before retrying.

429 or 503

Retry with exponential backoff and respect Retry-After when present.

Handoff checklist

Sender

Store the connected X account username or ID sent in account.

Recipient

Store the numeric recipient ID used in the POST /x/dm/{userId} path.

History

For DM history exports, store messages, has_next_page, and next_cursor; pass cursor to fetch older messages.

Text

Store the required non-empty text value.

Media

Store the optional one-item media_ids array containing a mediaId from POST /x/media.

Response

Store messageId, recipient_user_id, sender_account, message_text, and optional media_id in private audit records or support systems.

JSON Lines

Store history and send records in xquik-dm-handoff.jsonl for queues, warehouse loads, CRM syncs, or agent memory.
Last modified on May 24, 2026