Skip to main content
Use this workflow when a product, support, CRM, or AI agent system needs a hosted media URL for tweet posts or an uploaded media ID for direct messages. Xquik accepts local files with multipart/form-data and HTTPS media URLs with application/json. If you already have public image URLs or a public MP4 video URL for a tweet or reply, skip POST /x/media and pass those URLs directly in the media array on POST /x/tweets. Send up to 4 images or exactly 1 MP4 video up to 100 MB. Use POST /x/media when you need Xquik to host a local file, validate a generated media URL, or produce a mediaId for a DM attachment.
Need to post an MP4 tweet? If the MP4 is already a public HTTPS URL, skip upload and call Create Tweet with media: ["https://example.com/video.mp4"]. Use POST /x/media first only when Xquik must host a local file or validate a generated URL, then pass the returned mediaUrl to POST /x/tweets.

When to use this workflow

Upload a local file

Upload a local image, GIF, or MP4 with POST /x/media and multipart/form-data.

Upload generated media

Upload AI-generated media from a URL with POST /x/media and application/json.

Post tweets with public media URLs

Pass the URLs directly in the media array on POST /x/tweets.

Post tweets with media uploaded through Xquik

Pass returned mediaUrl in the media array on POST /x/tweets.

Post tweet replies with uploaded media

Pass mediaUrl plus reply_to_tweet_id on POST /x/tweets.

Send a DM with uploaded media

Pass returned mediaId as the only item in media_ids on POST /x/dm/{userId}.

Data you get

DM attachment ID

mediaId is the uploaded media ID for one-item DM media_ids arrays. Media IDs are valid for 24 hours after upload.

Tweet media URL

mediaUrl is the public media URL for tweet media arrays.

Upload confirmation

success is true after upload completes.

Tweet confirmation

tweetId is returned by POST /x/tweets after the media tweet or reply is confirmed.

End-to-end media handoff

Use one checkpoint object after upload and the downstream tweet, reply, or DM write. Keep the field boundary explicit: tweets and replies use public mediaUrl values in media; DMs use the uploaded mediaId as the only media_ids item.
{
  "workflow": "media_upload_handoff",
  "upload": {
    "endpoint": "/api/v1/x/media",
    "account": "myxhandle",
    "source_type": "url",
    "source": "https://example.com/image.png",
    "media_id": "1893726451023847424",
    "media_url": "https://media.xquik.com/uploads/1893726451023847424.png",
    "media_id_expires_in_hours": 24,
    "success": true
  },
  "tweet_post": {
    "endpoint": "/api/v1/x/tweets",
    "account": "myxhandle",
    "media": ["https://media.xquik.com/uploads/1893726451023847424.png"],
    "tweet_id": "1895432178065391234",
    "success": true,
    "charged_credits": "32"
  },
  "reply_post": {
    "endpoint": "/api/v1/x/tweets",
    "account": "myxhandle",
    "reply_to_tweet_id": "1893704267862470862",
    "media": ["https://media.xquik.com/uploads/1893726451023847424.png"],
    "tweet_id": "1895432178065391235",
    "write_status": "posted"
  },
  "dm_send": {
    "endpoint": "/api/v1/x/dm/44196397",
    "account": "myxhandle",
    "recipient_user_id": "44196397",
    "media_ids": ["1893726451023847424"],
    "message_id": "1893726451029384192",
    "success": true
  },
  "field_boundary": {
    "tweet_media_field": "media",
    "tweet_media_value": "mediaUrl",
    "dm_media_field": "media_ids[0]",
    "dm_media_value": "mediaId",
    "forbidden_tweet_field": "media_ids"
  },
  "audit_row": {
    "record_type": "media_upload_handoff",
    "source_endpoint": "/api/v1/x/media",
    "account": "myxhandle",
    "media_id": "1893726451023847424",
    "media_url": "https://media.xquik.com/uploads/1893726451023847424.png",
    "tweet_id": "1895432178065391234",
    "message_id": "1893726451029384192",
    "handoff_format": "jsonl"
  },
  "handoff_state": "store_media_url_for_tweets_and_media_id_for_dms"
}

Upload checkpoint

Store returned mediaId, mediaUrl, success, source URL or filename, and the upload endpoint.

Tweet URL checkpoint

Store the public mediaUrl in media, plus returned tweetId, chargedCredits, and optional writeActionId.

Reply URL checkpoint

Store reply_to_tweet_id, public mediaUrl, returned tweetId, and pending confirmation state when present.

DM ID checkpoint

Store exactly one uploaded mediaId in media_ids, plus returned messageId and recipient ID.

Field boundary

Reject tweet handoffs that put uploaded media IDs in media or send media_ids to POST /x/tweets.

Audit row

Store upload, tweet, reply, and DM IDs together so downstream systems can reconcile each media path.

Step 1: Upload media by URL

Use JSON URL upload when an AI agent, MCP client, or workflow tool has a generated image or MP4 URL that Xquik should validate and host. For tweet-only workflows with already public image URLs or exactly 1 public MP4 video URL up to 100 MB, call POST /x/tweets directly with media. The URL must use HTTPS, resolve to a public address, return a supported media content type, finish within 30 seconds, and stay under the 15,728,640-byte URL download cap.
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/image.png"
  }' | jq
{
  "mediaId": "1893726451023847424",
  "mediaUrl": "https://media.xquik.com/uploads/1893726451023847424.png",
  "success": true
}

URL upload checklist

HTTPS URL

Non-HTTPS URLs return 422 media_download_failed.

Public host

Private or reserved IP targets are rejected.

Supported content type

AVIF, GIF, JPEG, PNG, WebP, and MP4 are accepted.

Size cap

Larger URL downloads than 15,728,640 bytes return 422 media_download_failed.

30-second response window

Slow origins can time out before upload starts.

Step 2: Post a tweet or reply with mediaUrl

POST /x/tweets accepts public media URLs in media. Send up to 4 images or exactly 1 MP4 video up to 100 MB. Use the mediaUrl returned by POST /x/media. Text-only tweet or reply writes cost 30 credits; attached media adds 2 credits per started MB across all files.
curl -X POST https://xquik.com/api/v1/x/tweets \
  -H "x-api-key: xq_YOUR_KEY_HERE" \
  -H "Content-Type: application/json" \
  -d '{
    "account": "myxhandle",
    "text": "Product update with a new screenshot.",
    "media": ["https://media.xquik.com/uploads/1893726451023847424.png"]
  }' | jq
To post a media reply, send the same media URL array and add reply_to_tweet_id.
curl -X POST https://xquik.com/api/v1/x/tweets \
  -H "x-api-key: xq_YOUR_KEY_HERE" \
  -H "Content-Type: application/json" \
  -d '{
    "account": "myxhandle",
    "text": "Here is the requested screenshot.",
    "reply_to_tweet_id": "1893704267862470862",
    "media": ["https://media.xquik.com/uploads/1893726451023847424.png"]
  }' | jq
{
  "tweetId": "1895432178065391234",
  "success": true
}
Store the returned tweetId, chargedCredits, original mediaUrl, and parent reply_to_tweet_id so queues, CRMs, support tickets, or agents can reconcile the posted reply. If the response is 202 with x_write_unconfirmed, store writeActionId, chargedCredits, and poll Get write action status instead of sending the same tweet again. Do not send media_ids to POST /x/tweets. That endpoint rejects media_ids and expects the media URL array instead.

Step 3: Send a DM with mediaId

POST /x/dm/{userId} accepts one uploaded media ID in media_ids. Use the mediaId returned by POST /x/media.
curl -X POST https://xquik.com/api/v1/x/dm/44196397 \
  -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 exactly one uploaded media item. Send no media_ids field for text-only DMs. Store message_id, media_id, recipient, account, and send status in shared handoff rows. Keep full DM body text in private systems only.

JSON Lines handoff

For queues, CRM syncs, warehouse loads, or agent memory, write one record per upload and downstream write to xquik-media-handoff.jsonl.

Media upload row

{
  "record_type": "media_upload",
  "account": "myxhandle",
  "source": "https://example.com/image.png",
  "media_id": "1893726451023847424",
  "media_url": "https://media.xquik.com/uploads/1893726451023847424.png",
  "media_id_expires_in_hours": 24,
  "tweet_media_field": "media",
  "dm_media_field": "media_ids[0]",
  "handoff_format": "jsonl"
}

Tweet or reply row

{
  "record_type": "tweet_media_post",
  "account": "myxhandle",
  "tweet_id": "1895432178065391234",
  "reply_to_tweet_id": "1893704267862470862",
  "media_url": "https://media.xquik.com/uploads/1893726451023847424.png",
  "write_status": "posted",
  "handoff_format": "jsonl"
}

DM media row

{
  "record_type": "dm_media_send",
  "account": "myxhandle",
  "recipient_user_id": "44196397",
  "message_id": "1893726451029384192",
  "media_id": "1893726451023847424",
  "write_status": "sent",
  "handoff_format": "jsonl"
}
Use media_url for tweet and reply media arrays. Use media_id for the single DM media_ids item.

Step 4: Upload a local file

Use multipart upload when your app has the file bytes. Supported formats are AVIF, GIF, JPEG, PNG, WebP, and MP4.
curl -X POST https://xquik.com/api/v1/x/media \
  -H "x-api-key: xq_YOUR_KEY_HERE" \
  -F "account=myxhandle" \
  -F "file=@/path/to/image.png" | jq
For MP4 files longer than 140 seconds, add is_long_video=true.
curl -X POST https://xquik.com/api/v1/x/media \
  -H "x-api-key: xq_YOUR_KEY_HERE" \
  -F "account=myxhandle" \
  -F "file=@/path/to/video.mp4" \
  -F "is_long_video=true" | jq

Cost and error handling

Upload media costs 10 credits per upload call. Posting a tweet or reply is a separate 30-credit create-tweet call before media surcharges; sending the DM is a separate 10-credit write call.

Invalid input

Status 400. Error invalid_input. Check account, file type, file presence, URL presence, and is_long_video.

Billing or credits

Status 402. Errors no_subscription or insufficient_credits. Subscribe or top up credits before retrying.

Reconnect account

Status 403. Error account_needs_reauth. Reconnect the X account from the dashboard.

Media download failed

Status 422. Error media_download_failed. Use an HTTPS URL that returns a supported media file, or switch to multipart upload.

Rate limit or temporary failure

Status 429 or 503. Retry with exponential backoff and respect Retry-After when present.

Handoff checklist

Tweet workflow

Save mediaUrl and pass it to POST /x/tweets as media.

Reply workflow

Save the parent tweet ID, mediaUrl, returned tweetId, and any writeActionId.

DM workflow

Save mediaId and pass it to POST /x/dm/{userId} as one media_ids item.

JSON Lines

Store upload, tweet/reply, or DM handoff rows in xquik-media-handoff.jsonl with media_id and media_url.

Agent workflow

Use JSON URL upload only when the agent needs Xquik to validate or host a generated URL, or needs a DM mediaId. For tweet-only public URLs, pass them directly to POST /x/tweets.

File workflow

Prefer multipart upload when your app owns the file bytes.
Last modified on June 15, 2026