> ## Documentation Index
> Fetch the complete documentation index at: https://x-preview-mintlify-cc8632d3.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# Chunked Media Upload

> Upload videos and large media files with the v2 chunked upload endpoints: initialize, append, finalize, and status.

This guide walks you through uploading videos and large media files using the v2 chunked upload endpoints.

For video or large media uploads, you must:

1. **INIT** — `POST /2/media/upload/initialize` — start the session and get a `media_id`
2. **APPEND** — `POST /2/media/upload/{id}/append` — upload each chunk
3. **FINALIZE** — `POST /2/media/upload/{id}/finalize` — complete the upload
4. **STATUS** — `GET /2/media/upload` — wait for processing when `processing_info` is returned

<Note>
  Do not send `command=INIT`, `command=APPEND`, or `command=FINALIZE` to `POST /2/media/upload`. Those command-style parameters were the previous upload protocol. The v2 flow uses the dedicated paths above. `command=STATUS` is still used only on the status GET.
</Note>

Video duration and file size are limited by the authenticated user's Premium / verified status and by `media_category`. See [size and duration limits](/x-api/media/introduction#size-and-duration-limits). A successful upload can still be rejected when you attach the `media_id` to [`POST /2/tweets`](/x-api/posts/create-post).

***

## Step 1: Initialize upload (INIT)

Start the upload session. Send a JSON body — not multipart form fields.

<CodeGroup dropdown>
  ```bash cURL theme={null}
  curl -X POST "https://api.x.com/2/media/upload/initialize" \
    -H "Authorization: Bearer $USER_ACCESS_TOKEN" \
    -H "Content-Type: application/json" \
    -d '{
      "media_type": "video/mp4",
      "total_bytes": 1048576,
      "media_category": "tweet_video"
    }'
  ```

  ```python title="Python SDK" lines wrap icon="python" theme={null}
  from xdk import Client

  client = Client(bearer_token="YOUR_USER_ACCESS_TOKEN")

  response = client.media.initialize_upload(
      media_type="video/mp4",
      total_bytes=1048576,
      media_category="tweet_video",
  )

  media_id = response.data.id
  print(f"Media ID: {media_id}")
  ```

  ```javascript title="JavaScript SDK" lines wrap icon="square-js" theme={null}
  import { Client } from "@xdevplatform/xdk";

  const client = new Client({ accessToken: "YOUR_USER_ACCESS_TOKEN" });

  const response = await client.media.initializeUpload({
    mediaType: "video/mp4",
    totalBytes: 1048576,
    mediaCategory: "tweet_video",
  });

  const mediaId = response.data?.id;
  console.log(`Media ID: ${mediaId}`);
  ```
</CodeGroup>

**Response:**

```json theme={null}
{
  "data": {
    "id": "1880028106020515840",
    "media_key": "13_1880028106020515840",
    "expires_after_secs": 86400
  }
}
```

Use `tweet_video` for a regular Post. Use `amplify_video` for Ads creatives. See [media categories](#media-categories).

***

## Step 2: Upload chunks (APPEND)

Upload each chunk to `POST /2/media/upload/{id}/append`. Keep each segment at or below **5 MB** (the server maximum is 8 MB). Segments are indexed from `0`.

<CodeGroup dropdown>
  ```bash cURL theme={null}
  curl -X POST "https://api.x.com/2/media/upload/1880028106020515840/append" \
    -H "Authorization: Bearer $USER_ACCESS_TOKEN" \
    -F "segment_index=0" \
    -F "media=@/path/to/chunk0.mp4"
  ```

  ```python title="Python SDK" lines wrap icon="python" theme={null}
  from xdk import Client

  client = Client(bearer_token="YOUR_USER_ACCESS_TOKEN")

  chunk_size = 4 * 1024 * 1024  # 4 MB chunks

  with open("video.mp4", "rb") as f:
      segment_index = 0
      while True:
          chunk = f.read(chunk_size)
          if not chunk:
              break

          client.media.append_upload(
              id=media_id,
              segment_index=segment_index,
              media=chunk,
          )
          segment_index += 1
          print(f"Uploaded chunk {segment_index}")
  ```

  ```javascript title="JavaScript SDK" lines wrap icon="square-js" theme={null}
  import { Client } from "@xdevplatform/xdk";
  import fs from "fs";

  const client = new Client({ accessToken: "YOUR_USER_ACCESS_TOKEN" });

  const chunkSize = 4 * 1024 * 1024; // 4 MB chunks
  const fileBuffer = fs.readFileSync("video.mp4");

  let segmentIndex = 0;
  for (let offset = 0; offset < fileBuffer.length; offset += chunkSize) {
    const chunk = fileBuffer.slice(offset, offset + chunkSize);

    await client.media.appendUpload(mediaId, {
      segmentIndex,
      media: chunk,
    });

    console.log(`Uploaded chunk ${segmentIndex + 1}`);
    segmentIndex++;
  }
  ```
</CodeGroup>

<Info>
  **Chunking advantages:**

  * Improved reliability on slow networks
  * Uploads can be paused and resumed
  * Failed chunks can be retried individually
</Info>

***

## Step 3: Finalize upload (FINALIZE)

Complete the upload after all chunks are sent:

<CodeGroup dropdown>
  ```bash cURL theme={null}
  curl -X POST "https://api.x.com/2/media/upload/1880028106020515840/finalize" \
    -H "Authorization: Bearer $USER_ACCESS_TOKEN"
  ```

  ```python title="Python SDK" lines wrap icon="python" theme={null}
  from xdk import Client

  client = Client(bearer_token="YOUR_USER_ACCESS_TOKEN")

  response = client.media.finalize_upload(id=media_id)

  print(f"Processing state: {response.data.processing_info.state}")
  ```

  ```javascript title="JavaScript SDK" lines wrap icon="square-js" theme={null}
  import { Client } from "@xdevplatform/xdk";

  const client = new Client({ accessToken: "YOUR_USER_ACCESS_TOKEN" });

  const response = await client.media.finalizeUpload(mediaId);

  console.log(`Processing state: ${response.data?.processing_info?.state}`);
  ```
</CodeGroup>

**Response:**

```json title="Example response" lines wrap icon="https://mintcdn.com/x-preview-mintlify-cc8632d3/D44A1SzbLKXSJH60/icons/xds/icon-brackets.svg?fit=max&auto=format&n=D44A1SzbLKXSJH60&q=85&s=d358c2a36710696a4d2b372d14f1ea43" theme={null}
{
  "data": {
    "id": "1880028106020515840",
    "media_key": "13_1880028106020515840",
    "size": 1048576,
    "expires_after_secs": 86400,
    "processing_info": {
      "state": "pending",
      "check_after_secs": 1
    }
  }
}
```

<Note>
  If `processing_info` is returned, proceed to Step 4 to wait for processing. If not, the media is ready to use.
</Note>

***

## Step 4: Check status (STATUS)

If `processing_info` was returned, poll until processing completes:

<CodeGroup dropdown>
  ```bash cURL theme={null}
  curl "https://api.x.com/2/media/upload?command=STATUS&media_id=1880028106020515840" \
    -H "Authorization: Bearer $USER_ACCESS_TOKEN"
  ```

  ```python title="Python SDK" lines wrap icon="python" theme={null}
  from xdk import Client
  import time

  client = Client(bearer_token="YOUR_USER_ACCESS_TOKEN")

  while True:
      response = client.media.get_upload_status(
          media_id=media_id,
          command="STATUS",
      )
      state = response.data.processing_info.state

      if state == "succeeded":
          print("Media ready!")
          break
      elif state == "failed":
          print("Processing failed")
          break
      else:
          check_after = response.data.processing_info.check_after_secs
          print(f"Processing... checking again in {check_after}s")
          time.sleep(check_after)
  ```

  ```javascript title="JavaScript SDK" lines wrap icon="square-js" theme={null}
  import { Client } from "@xdevplatform/xdk";

  const client = new Client({ accessToken: "YOUR_USER_ACCESS_TOKEN" });

  while (true) {
    const response = await client.media.getUploadStatus(mediaId, {
      command: "STATUS",
    });
    const state = response.data?.processing_info?.state;

    if (state === "succeeded") {
      console.log("Media ready!");
      break;
    } else if (state === "failed") {
      console.log("Processing failed");
      break;
    } else {
      const checkAfter = response.data?.processing_info?.check_after_secs ?? 1;
      console.log(`Processing... checking again in ${checkAfter}s`);
      await new Promise((r) => setTimeout(r, checkAfter * 1000));
    }
  }
  ```
</CodeGroup>

**Processing states:** `pending` → `in_progress` → `succeeded` or `failed`

***

## Step 5: Create Post with media

Once processing is complete, create a Post with the media. Duration and size are checked again against the posting user's entitlement.

<CodeGroup dropdown>
  ```bash cURL theme={null}
  curl -X POST "https://api.x.com/2/tweets" \
    -H "Authorization: Bearer $USER_ACCESS_TOKEN" \
    -H "Content-Type: application/json" \
    -d '{
      "text": "Check out this video!",
      "media": {
        "media_ids": ["1880028106020515840"]
      }
    }'
  ```

  ```python title="Python SDK" lines wrap icon="python" theme={null}
  from xdk import Client

  client = Client(bearer_token="YOUR_USER_ACCESS_TOKEN")

  response = client.posts.create(
      text="Check out this video!",
      media={"media_ids": [media_id]},
  )

  print(f"Posted: {response.data.id}")
  ```

  ```javascript title="JavaScript SDK" lines wrap icon="square-js" theme={null}
  import { Client } from "@xdevplatform/xdk";

  const client = new Client({ accessToken: "YOUR_USER_ACCESS_TOKEN" });

  const response = await client.posts.create({
    text: "Check out this video!",
    media: { mediaIds: [mediaId] },
  });

  console.log(`Posted: ${response.data?.id}`);
  ```
</CodeGroup>

If the video is longer than the posting user is allowed to attach, the response is **403 Forbidden**:

```json theme={null}
{
  "title": "Forbidden",
  "detail": "This user is not allowed to post a video longer than 20 minutes.",
  "type": "about:blank",
  "status": 403
}
```

`N` is the posting user's duration cap (20 minutes by default, 125 minutes for Premium / verified).

***

## Media categories

| Category        | Use for                          | Typical max (default / Premium)          |
| :-------------- | :------------------------------- | :--------------------------------------- |
| `tweet_image`   | Image on a Post                  | 5 MB                                     |
| `tweet_gif`     | Animated GIF on a Post           | 15 MB                                    |
| `tweet_video`   | Video on a Post                  | 20 min / 8 GB · Premium: 125 min / 16 GB |
| `amplify_video` | Ads / promoted video             | 20 min / 8 GB · Premium: 125 min / 16 GB |
| `dm_image`      | Image in a Direct Message        | 5 MB                                     |
| `dm_gif`        | Animated GIF in a Direct Message | 15 MB                                    |
| `dm_video`      | Video in a Direct Message        | 140 s / 512 MB · Premium: 10 min / 1 GB  |
| `subtitles`     | Subtitle file                    | 1 MB                                     |

If you omit `media_category`, the upload is treated as Post media (`tweet_image`, `tweet_video`, or `tweet_gif`) based on content type.

***

## Next steps

<CardGroup cols={2}>
  <Card title="Best practices" icon="https://mintcdn.com/x-preview-mintlify-cc8632d3/D44A1SzbLKXSJH60/icons/xds/icon-book.svg?fit=max&auto=format&n=D44A1SzbLKXSJH60&q=85&s=69571d477792cb4e74160306c68c3166" href="/x-api/media/quickstart/best-practices" width="24" height="24" data-path="icons/xds/icon-book.svg">
    File constraints, codecs, and duration limits
  </Card>

  <Card title="Create Posts" icon="https://mintcdn.com/x-preview-mintlify-cc8632d3/1wZXpxugn056db0j/icons/xds/icon-chat.svg?fit=max&auto=format&n=1wZXpxugn056db0j&q=85&s=4956eafd5e8a5769d11010d186a64bf1" href="/x-api/posts/manage-tweets/quickstart" width="24" height="24" data-path="icons/xds/icon-chat.svg">
    Post with media
  </Card>

  <Card title="Initialize" icon="https://mintcdn.com/x-preview-mintlify-cc8632d3/1wZXpxugn056db0j/icons/xds/icon-code.svg?fit=max&auto=format&n=1wZXpxugn056db0j&q=85&s=0182d99128ca2db5444cb5115d9234da" href="/x-api/media/initialize-media-upload" width="24" height="24" data-path="icons/xds/icon-code.svg">
    INIT endpoint reference
  </Card>

  <Card title="Create Post" icon="https://mintcdn.com/x-preview-mintlify-cc8632d3/1wZXpxugn056db0j/icons/xds/icon-code.svg?fit=max&auto=format&n=1wZXpxugn056db0j&q=85&s=0182d99128ca2db5444cb5115d9234da" href="/x-api/posts/create-post" width="24" height="24" data-path="icons/xds/icon-code.svg">
    POST /2/tweets reference
  </Card>
</CardGroup>
