How to Get XChat DM Media with TwexAPI
Text is only half of many DM records. A support case may depend on a screenshot, a creator review may include a video clip, and a compliance export may need to explain which file came from which conversation.
TwexAPI's Get DM Media (XChat v3) endpoint resolves one XChat attachment into a playable media URL. The input comes from attachments returned by Get DM History (XChat v3): use the message's conversation_id, attachment media_hash_key, and the optional but recommended key_version.
The media endpoint does not search a conversation by itself. First read the conversation history, store the attachment metadata, then call POST /v3/twitter/dm-media for the exact attachment you need.
Endpoint and Request Fields
Use POST /v3/twitter/dm-media to fetch one XChat media attachment.
| Field | Type | Required | Notes |
|---|---|---|---|
conversation_id | string | Yes | Conversation ID from /v3/twitter/dm-history. |
media_hash_key | string | Yes | Attachment media_hash_key from a DM history attachment object. |
cookie | string | Yes | X authentication cookie or auth_token. Keep it server-side. |
key_version | string or null | No | Optional message key version from DM history. Recommended when available. |
type_name | string or null | No | Optional hint such as IMAGE, VIDEO, or GIF. |
filename | string or null | No | Optional filename hint from the attachment object. |
pin | string or null | No | Identity PIN. API default is 1234. |
proxy | string or null | No | Optional HTTP proxy for the session. |
Basic Request
1curl --request POST \
2 --url https://api.twexapi.io/v3/twitter/dm-media \
3 --header "Authorization: Bearer $TWEXAPI_BEARER_TOKEN" \
4 --header "Content-Type: application/json" \
5 --data '{
6 "conversation_id": "1928096185664843776:1989842918455291904",
7 "media_hash_key": "blRIlZCVIO",
8 "key_version": "3",
9 "type_name": "VIDEO",
10 "filename": "clip.mp4",
11 "cookie": "auth_token=...; ct0=...; twid=...",
12 "pin": "1234"
13 }'The response returns a playable URL plus file metadata.
1{
2 "code": 200,
3 "msg": "success",
4 "data": {
5 "url": "https://api.twexapi.io/media/xchat/...",
6 "path": "/media/xchat/...",
7 "content_type": "video/mp4",
8 "filename": "clip.mp4",
9 "byte_length": 421299,
10 "cached": true
11 }
12}Store the returned metadata with the original message and attachment record. If your product needs long-term retention, download the returned url into your own storage under your normal privacy and retention rules.
Attachment Input From DM History
/v3/twitter/dm-history can return attachments on a message. Those fields become the input to /v3/twitter/dm-media.
1{
2 "conversation_id": "1928096185664843776:1989842918455291904",
3 "messages": [
4 {
5 "id": "1234567890123456789",
6 "text": "Here is the video.",
7 "sequence_id": "2076661179334979585",
8 "attachments": [
9 {
10 "attachment_id": "cda5ccdd-af53-48a3-bdd1-5039a73aea25",
11 "conversation_id": "1928096185664843776:1989842918455291904",
12 "filename": "clip.mp4",
13 "filesize_bytes": 421299,
14 "key_version": "3",
15 "media_hash_key": "blRIlZCVIO",
16 "type": "video",
17 "type_name": "VIDEO"
18 }
19 ]
20 }
21 ]
22}Use the page-level conversation_id when the attachment object does not include it.
Python Media Resolver
This script takes an attachment object from a saved DM history export, resolves the playable URL, and returns a normalized row you can store next to the message.
1import os
2from datetime import datetime, timezone
3from typing import Any
4
5import requests
6
7API_URL = "https://api.twexapi.io/v3/twitter/dm-media"
8BEARER_TOKEN = os.environ["TWEXAPI_BEARER_TOKEN"]
9X_COOKIE = os.environ["TWEXAPI_X_COOKIE"]
10XCHAT_PIN = os.environ.get("TWEXAPI_XCHAT_PIN", "1234")
11PROXY = os.environ.get("TWEXAPI_PROXY")
12
13def build_media_payload(
14 conversation_id: str,
15 attachment: dict[str, Any],
16) -> dict[str, Any]:
17 media_hash_key = attachment.get("media_hash_key")
18 if not media_hash_key:
19 raise ValueError("attachment.media_hash_key is required")
20
21 payload: dict[str, Any] = {
22 "conversation_id": attachment.get("conversation_id") or conversation_id,
23 "media_hash_key": media_hash_key,
24 "cookie": X_COOKIE,
25 "pin": XCHAT_PIN,
26 }
27
28 for field in ("key_version", "type_name", "filename"):
29 if attachment.get(field):
30 payload[field] = attachment[field]
31
32 if PROXY:
33 payload["proxy"] = PROXY
34
35 return payload
36
37def fetch_dm_media(
38 conversation_id: str,
39 attachment: dict[str, Any],
40) -> dict[str, Any]:
41 response = requests.post(
42 API_URL,
43 headers={
44 "Authorization": f"Bearer {BEARER_TOKEN}",
45 "Content-Type": "application/json",
46 "Accept": "application/json",
47 },
48 json=build_media_payload(conversation_id, attachment),
49 timeout=60,
50 )
51 response.raise_for_status()
52 data = response.json()
53
54 if data.get("code") != 200:
55 raise RuntimeError(data.get("msg", "TwexAPI returned an error"))
56
57 return data["data"]
58
59def normalize_media_result(
60 *,
61 conversation_id: str,
62 message_id: str,
63 attachment: dict[str, Any],
64 media: dict[str, Any],
65) -> dict[str, Any]:
66 return {
67 "conversation_id": conversation_id,
68 "message_id": message_id,
69 "attachment_id": attachment.get("attachment_id"),
70 "media_hash_key": attachment.get("media_hash_key"),
71 "key_version": attachment.get("key_version"),
72 "type_name": attachment.get("type_name"),
73 "source_filename": attachment.get("filename"),
74 "playable_url": media.get("url"),
75 "api_path": media.get("path"),
76 "content_type": media.get("content_type"),
77 "resolved_filename": media.get("filename"),
78 "byte_length": media.get("byte_length"),
79 "cached": media.get("cached"),
80 "resolved_at": datetime.now(timezone.utc).isoformat(),
81 "raw_attachment": attachment,
82 "raw_media": media,
83 }
84
85if __name__ == "__main__":
86 conversation_id = "1928096185664843776:1989842918455291904"
87 message_id = "1234567890123456789"
88 attachment = {
89 "attachment_id": "cda5ccdd-af53-48a3-bdd1-5039a73aea25",
90 "filename": "clip.mp4",
91 "key_version": "3",
92 "media_hash_key": "blRIlZCVIO",
93 "type_name": "VIDEO",
94 }
95
96 media = fetch_dm_media(conversation_id, attachment)
97 print(
98 normalize_media_result(
99 conversation_id=conversation_id,
100 message_id=message_id,
101 attachment=attachment,
102 media=media,
103 )
104 )Set TWEXAPI_BEARER_TOKEN, TWEXAPI_X_COOKIE, and TWEXAPI_XCHAT_PIN in your trusted server environment, then run:
python resolve_xchat_dm_media.pyDo not call this from the browser. The request needs the X cookie, and that credential belongs on the server.
Media Export Workflow
- Read history: Call
POST /v3/twitter/dm-historyand store messages with theirattachments. - Queue attachments: Create one media job per attachment that has
media_hash_key. - Resolve media: Call
POST /v3/twitter/dm-mediawithconversation_id,media_hash_key, andkey_versionwhen available. - Store metadata: Save
url,path,content_type,filename,byte_length,cached, and the raw response. - Archive carefully: If you download the file, store it in your own bucket with access controls and retention rules.
This keeps the media pipeline explainable: every file can be traced back to the conversation, message, attachment, and API response that produced it.
Fields To Store
| Field | Why it matters |
|---|---|
conversation_id | Links the media to the original XChat conversation. |
message_id | Links the file to the message that contained it. |
attachment_id | Attachment-level deduplication when available. |
media_hash_key | Required input for resolving the media. |
key_version | Recommended input from DM history. |
type_name | Helps route images, videos, and GIFs. |
url | Playable URL returned by the media endpoint. |
path | API media path returned with the URL. |
content_type | MIME type for rendering and storage decisions. |
filename | Suggested filename. |
byte_length | File size in bytes. |
cached | Shows whether the media was already available. |
| Raw attachment and response JSON | Lets you reprocess if your schema changes. |
Do not store the full account cookie with media records. Store a credential reference instead.
Common Mistakes
- Calling
/v3/twitter/dm-mediawithout first saving attachment metadata from/v3/twitter/dm-history. - Passing message
idorsequence_idinstead ofmedia_hash_key. - Dropping
key_versioneven though the attachment provided it. - Logging cookies, playable URLs, or downloaded files in places your team cannot audit.
- Treating the media URL as your only archive instead of storing your own copy when retention is required.
Wrap-up
Use POST /v3/twitter/dm-media after dm-history has found an attachment. Pass conversation_id, media_hash_key, and key_version when available, then store the returned playable URL and media metadata with the original message record.