> ## Documentation Index
> Fetch the complete documentation index at: https://docs.vmeg.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Material upload

> Presigned and multipart CDN upload

Materials are uploaded **directly to CDN/object storage** via presigned URLs, then registered with the VMEG API. Bytes do not pass through the API gateway.

Every upload-related **POST** in this flow requires [`X-Idempotency-Key`](/guides/idempotency).

<Warning>
  Early steps (gen-upload-url or multipart initiate) may already return a `materialId`. That ID is only for continuing the upload. Do **not** pass it to create-async or other product APIs until the full upload finishes — single-file **complete**, multipart **complete**, or an instant-upload / dedup response that already includes a registered `material`.
</Warning>

## Choose an upload mode

| Mode                            | When to use                  |
| ------------------------------- | ---------------------------- |
| **Single-file (presigned URL)** | Smaller files, one PUT       |
| **Multipart**                   | Large files, resumable parts |

Request and response fields: [Generate presigned upload URL](/api-reference/assets/materials/gen-upload-url), [Complete single-file upload](/api-reference/assets/materials/complete-upload), and other **Assets - Materials → upload** operations in [API reference](/api-reference/introduction).

## Single-file upload

<Steps>
  <Step title="Get presigned URL">
    `POST /openapi/v1/assets/material/upload/gen-upload-url` with `fileHash` (MD5) and `fileName`.
  </Step>

  <Step title="PUT file to CDN">
    If the response includes `data.material`, the file was deduplicated — skip PUT. Otherwise upload bytes to `data.uploadUrl`.
  </Step>

  <Step title="Complete upload">
    `POST /openapi/v1/assets/material/upload/complete` with `s3Uri`, `materialId`, `fileHash`, and `fileName` from the gen-upload-url step.
  </Step>
</Steps>

### Example

<CodeGroup>
  ```javascript Node.js theme={null}
  import crypto from "node:crypto";
  import fs from "node:fs";

  const API_BASE = "https://api.vmeg.ai";
  const API_KEY = process.env.VMEG_API_KEY;
  const FILE_PATH = "./demo.mp4";

  function md5File(path) {
    const hash = crypto.createHash("md5");
    hash.update(fs.readFileSync(path));
    return hash.digest("hex");
  }

  function idemKey(step) {
    return `upload-${step}-${crypto.randomUUID()}`;
  }

  async function apiPost(path, body) {
    const res = await fetch(`${API_BASE}${path}`, {
      method: "POST",
      headers: {
        Authorization: `Bearer ${API_KEY}`,
        "Content-Type": "application/json",
        "X-Idempotency-Key": idemKey(path),
      },
      body: JSON.stringify(body),
    });
    const json = await res.json();
    if (json.code !== 200) throw new Error(json.message || "API error");
    return json.data;
  }

  async function uploadSingleFile() {
    const fileName = FILE_PATH.split("/").pop();
    const fileHash = md5File(FILE_PATH);
    const fileBytes = fs.readFileSync(FILE_PATH);

    const gen = await apiPost("/openapi/v1/assets/material/upload/gen-upload-url", {
      fileHash,
      fileName,
    });

    if (gen.material?.materialId) {
      console.log("Instant upload (dedup):", gen.material.materialId);
      return gen.material.materialId;
    }

    const putRes = await fetch(gen.uploadUrl, { method: "PUT", body: fileBytes });
    if (!putRes.ok) throw new Error(`CDN PUT failed: ${putRes.status}`);

    const material = await apiPost("/openapi/v1/assets/material/upload/complete", {
      s3Uri: gen.s3Uri,
      materialId: gen.materialId,
      fileHash,
      fileName,
    });

    console.log("materialId:", material.materialId);
    return material.materialId;
  }

  uploadSingleFile().catch(console.error);
  ```

  ```python Python theme={null}
  import hashlib
  import os
  import uuid

  import requests

  API_BASE = "https://api.vmeg.ai"
  API_KEY = os.environ["VMEG_API_KEY"]
  FILE_PATH = "./demo.mp4"


  def md5_file(path: str) -> str:
      h = hashlib.md5()
      with open(path, "rb") as f:
          for chunk in iter(lambda: f.read(1024 * 1024), b""):
              h.update(chunk)
      return h.hexdigest()


  def idem_key(step: str) -> str:
      return f"upload-{step}-{uuid.uuid4()}"


  def api_post(path: str, body: dict) -> dict:
      res = requests.post(
          f"{API_BASE}{path}",
          headers={
              "Authorization": f"Bearer {API_KEY}",
              "Content-Type": "application/json",
              "X-Idempotency-Key": idem_key(path),
          },
          json=body,
          timeout=120,
      )
      res.raise_for_status()
      payload = res.json()
      if payload.get("code") != 200:
          raise RuntimeError(payload.get("message") or "API error")
      return payload["data"]


  def upload_single_file() -> str:
      file_name = os.path.basename(FILE_PATH)
      file_hash = md5_file(FILE_PATH)

      gen = api_post(
          "/openapi/v1/assets/material/upload/gen-upload-url",
          {"fileHash": file_hash, "fileName": file_name},
      )

      if gen.get("material", {}).get("materialId"):
          material_id = gen["material"]["materialId"]
          print("Instant upload (dedup):", material_id)
          return material_id

      with open(FILE_PATH, "rb") as f:
          put = requests.put(gen["uploadUrl"], data=f, timeout=600)
      put.raise_for_status()

      material = api_post(
          "/openapi/v1/assets/material/upload/complete",
          {
              "s3Uri": gen["s3Uri"],
              "materialId": gen["materialId"],
              "fileHash": file_hash,
              "fileName": file_name,
          },
      )
      material_id = material["materialId"]
      print("materialId:", material_id)
      return material_id


  if __name__ == "__main__":
      upload_single_file()
  ```

  ```java Java theme={null}
  import java.io.IOException;
  import java.net.URI;
  import java.net.http.HttpClient;
  import java.net.http.HttpRequest;
  import java.net.http.HttpResponse;
  import java.nio.file.Files;
  import java.nio.file.Path;
  import java.security.MessageDigest;
  import java.time.Duration;
  import java.util.Map;
  import java.util.UUID;

  import com.fasterxml.jackson.databind.JsonNode;
  import com.fasterxml.jackson.databind.ObjectMapper;

  public final class SingleMaterialUpload {
    private static final String API_BASE = "https://api.vmeg.ai";
    private static final String API_KEY = System.getenv("VMEG_API_KEY");
    private static final Path FILE_PATH = Path.of("demo.mp4");

    private static final HttpClient HTTP = HttpClient.newBuilder()
        .connectTimeout(Duration.ofSeconds(30))
        .build();
    private static final ObjectMapper JSON = new ObjectMapper();

    public static void main(String[] args) throws Exception {
      String materialId = uploadSingleFile();
      System.out.println("materialId: " + materialId);
    }

    static String uploadSingleFile() throws Exception {
      String fileName = FILE_PATH.getFileName().toString();
      String fileHash = md5File(FILE_PATH);
      byte[] fileBytes = Files.readAllBytes(FILE_PATH);

      JsonNode gen = apiPost(
          "/openapi/v1/assets/material/upload/gen-upload-url",
          Map.of("fileHash", fileHash, "fileName", fileName));

      if (gen.hasNonNull("material") && gen.get("material").hasNonNull("materialId")) {
        return gen.get("material").get("materialId").asText();
      }

      HttpRequest put = HttpRequest.newBuilder()
          .uri(URI.create(gen.get("uploadUrl").asText()))
          .PUT(HttpRequest.BodyPublishers.ofByteArray(fileBytes))
          .timeout(Duration.ofMinutes(10))
          .build();
      HttpResponse<Void> putRes = HTTP.send(put, HttpResponse.BodyHandlers.discarding());
      if (putRes.statusCode() < 200 || putRes.statusCode() >= 300) {
        throw new IOException("CDN PUT failed: " + putRes.statusCode());
      }

      JsonNode material = apiPost(
          "/openapi/v1/assets/material/upload/complete",
          Map.of(
              "s3Uri", gen.get("s3Uri").asText(),
              "materialId", gen.get("materialId").asText(),
              "fileHash", fileHash,
              "fileName", fileName));

      return material.get("materialId").asText();
    }

    static JsonNode apiPost(String path, Map<String, String> body) throws Exception {
      String bodyJson = JSON.writeValueAsString(body);
      HttpRequest req = HttpRequest.newBuilder()
          .uri(URI.create(API_BASE + path))
          .timeout(Duration.ofSeconds(120))
          .header("Authorization", "Bearer " + API_KEY)
          .header("Content-Type", "application/json")
          .header("X-Idempotency-Key", "upload-" + path + "-" + UUID.randomUUID())
          .POST(HttpRequest.BodyPublishers.ofString(bodyJson))
          .build();
      HttpResponse<String> res = HTTP.send(req, HttpResponse.BodyHandlers.ofString());
      JsonNode root = JSON.readTree(res.body());
      if (root.path("code").asInt() != 200) {
        throw new IOException(root.path("message").asText("API error"));
      }
      return root.get("data");
    }

    static String md5File(Path path) throws Exception {
      MessageDigest md5 = MessageDigest.getInstance("MD5");
      md5.update(Files.readAllBytes(path));
      StringBuilder sb = new StringBuilder();
      for (byte b : md5.digest()) {
        sb.append(String.format("%02x", b));
      }
      return sb.toString();
    }
  }
  ```
</CodeGroup>

<Note>
  The Java example uses Jackson (`ObjectMapper`) for JSON. Add `com.fasterxml.jackson.core:jackson-databind` to your project, or parse responses with your preferred JSON library.
</Note>

## Multipart upload

For large files:

1. `POST .../multipart/initiate`
2. `POST .../multipart/presign-parts` — presigned URL per part
3. PUT each part to CDN and record each part `ETag`
4. `POST .../multipart/complete` — finalize and get `materialId`
5. On failure: `POST .../multipart/abort`

Use `recommendedPartSize` from the initiate response (S3 minimum part size is typically 5 MB except the last part).

### Example

<CodeGroup>
  ```javascript Node.js theme={null}
  import crypto from "node:crypto";
  import fs from "node:fs";

  const API_BASE = "https://api.vmeg.ai";
  const API_KEY = process.env.VMEG_API_KEY;
  const FILE_PATH = "./large.mp4";

  function md5File(path) {
    const hash = crypto.createHash("md5");
    hash.update(fs.readFileSync(path));
    return hash.digest("hex");
  }

  function idemKey(step) {
    return `multipart-${step}-${crypto.randomUUID()}`;
  }

  async function apiPost(path, body) {
    const res = await fetch(`${API_BASE}${path}`, {
      method: "POST",
      headers: {
        Authorization: `Bearer ${API_KEY}`,
        "Content-Type": "application/json",
        "X-Idempotency-Key": idemKey(path),
      },
      body: JSON.stringify(body),
    });
    const json = await res.json();
    if (json.code !== 200) throw new Error(json.message || "API error");
    return json.data;
  }

  function readPart(buffer, offset, size) {
    return buffer.subarray(offset, Math.min(offset + size, buffer.length));
  }

  async function uploadMultipart() {
    const fileName = FILE_PATH.split("/").pop();
    const fileHash = md5File(FILE_PATH);
    const fileBytes = fs.readFileSync(FILE_PATH);

    const init = await apiPost("/openapi/v1/assets/material/upload/multipart/initiate", {
      fileHash,
      fileName,
    });

    const partSize = init.recommendedPartSize || 8 * 1024 * 1024;
    const partNumbers = [];
    for (let offset = 0, n = 1; offset < fileBytes.length; offset += partSize, n++) {
      partNumbers.push(n);
    }

    const presign = await apiPost(
      "/openapi/v1/assets/material/upload/multipart/presign-parts",
      {
        materialId: init.materialId,
        uploadId: init.uploadId,
        fileHash,
        fileName,
        partNumbers,
      }
    );

    const parts = [];
    for (const { partNumber, url } of presign.urls) {
      const start = (partNumber - 1) * partSize;
      const chunk = readPart(fileBytes, start, partSize);
      const putRes = await fetch(url, { method: "PUT", body: chunk });
      if (!putRes.ok) throw new Error(`Part ${partNumber} PUT failed: ${putRes.status}`);
      const eTag = (putRes.headers.get("etag") || "").replace(/"/g, "");
      parts.push({ partNumber, eTag });
    }

    const material = await apiPost(
      "/openapi/v1/assets/material/upload/multipart/complete",
      {
        materialId: init.materialId,
        uploadId: init.uploadId,
        fileHash,
        fileName,
        parts,
      }
    );

    console.log("materialId:", material.materialId);
    return material.materialId;
  }

  uploadMultipart().catch(console.error);
  ```

  ```python Python theme={null}
  import hashlib
  import math
  import os
  import uuid

  import requests

  API_BASE = "https://api.vmeg.ai"
  API_KEY = os.environ["VMEG_API_KEY"]
  FILE_PATH = "./large.mp4"


  def md5_file(path: str) -> str:
      h = hashlib.md5()
      with open(path, "rb") as f:
          for chunk in iter(lambda: f.read(1024 * 1024), b""):
              h.update(chunk)
      return h.hexdigest()


  def idem_key(step: str) -> str:
      return f"multipart-{step}-{uuid.uuid4()}"


  def api_post(path: str, body: dict) -> dict:
      res = requests.post(
          f"{API_BASE}{path}",
          headers={
              "Authorization": f"Bearer {API_KEY}",
              "Content-Type": "application/json",
              "X-Idempotency-Key": idem_key(path),
          },
          json=body,
          timeout=120,
      )
      res.raise_for_status()
      payload = res.json()
      if payload.get("code") != 200:
          raise RuntimeError(payload.get("message") or "API error")
      return payload["data"]


  def upload_multipart() -> str:
      file_name = os.path.basename(FILE_PATH)
      file_hash = md5_file(FILE_PATH)
      file_size = os.path.getsize(FILE_PATH)

      init = api_post(
          "/openapi/v1/assets/material/upload/multipart/initiate",
          {"fileHash": file_hash, "fileName": file_name},
      )

      part_size = init.get("recommendedPartSize") or 8 * 1024 * 1024
      part_count = max(1, math.ceil(file_size / part_size))
      part_numbers = list(range(1, part_count + 1))

      presign = api_post(
          "/openapi/v1/assets/material/upload/multipart/presign-parts",
          {
              "materialId": init["materialId"],
              "uploadId": init["uploadId"],
              "fileHash": file_hash,
              "fileName": file_name,
              "partNumbers": part_numbers,
          },
      )

      parts = []
      with open(FILE_PATH, "rb") as f:
          for item in presign["urls"]:
              part_number = item["partNumber"]
              f.seek((part_number - 1) * part_size)
              chunk = f.read(part_size)
              put = requests.put(item["url"], data=chunk, timeout=600)
              put.raise_for_status()
              e_tag = put.headers.get("ETag", "").strip('"')
              parts.append({"partNumber": part_number, "eTag": e_tag})

      material = api_post(
          "/openapi/v1/assets/material/upload/multipart/complete",
          {
              "materialId": init["materialId"],
              "uploadId": init["uploadId"],
              "fileHash": file_hash,
              "fileName": file_name,
              "parts": parts,
          },
      )
      material_id = material["materialId"]
      print("materialId:", material_id)
      return material_id


  if __name__ == "__main__":
      upload_multipart()
  ```

  ```java Java theme={null}
  import java.io.IOException;
  import java.io.RandomAccessFile;
  import java.net.URI;
  import java.net.http.HttpClient;
  import java.net.http.HttpRequest;
  import java.net.http.HttpResponse;
  import java.nio.file.Path;
  import java.security.MessageDigest;
  import java.time.Duration;
  import java.util.ArrayList;
  import java.util.List;
  import java.util.Map;
  import java.util.UUID;

  import com.fasterxml.jackson.databind.JsonNode;
  import com.fasterxml.jackson.databind.ObjectMapper;
  import com.fasterxml.jackson.databind.node.ArrayNode;
  import com.fasterxml.jackson.databind.node.ObjectNode;

  public final class MultipartMaterialUpload {
    private static final String API_BASE = "https://api.vmeg.ai";
    private static final String API_KEY = System.getenv("VMEG_API_KEY");
    private static final Path FILE_PATH = Path.of("large.mp4");

    private static final HttpClient HTTP = HttpClient.newBuilder()
        .connectTimeout(Duration.ofSeconds(30))
        .build();
    private static final ObjectMapper JSON = new ObjectMapper();

    public static void main(String[] args) throws Exception {
      System.out.println("materialId: " + uploadMultipart());
    }

    static String uploadMultipart() throws Exception {
      String fileName = FILE_PATH.getFileName().toString();
      String fileHash = md5File(FILE_PATH);
      long fileSize = java.nio.file.Files.size(FILE_PATH);

      JsonNode init = apiPost(
          "/openapi/v1/assets/material/upload/multipart/initiate",
          Map.of("fileHash", fileHash, "fileName", fileName));

      long partSize = init.has("recommendedPartSize")
          ? init.get("recommendedPartSize").asLong()
          : 8L * 1024 * 1024;
      int partCount = (int) Math.max(1, (fileSize + partSize - 1) / partSize);

      List<Integer> partNumbers = new ArrayList<>();
      for (int i = 1; i <= partCount; i++) {
        partNumbers.add(i);
      }

      ObjectNode presignBody = JSON.createObjectNode();
      presignBody.put("materialId", init.get("materialId").asText());
      presignBody.put("uploadId", init.get("uploadId").asText());
      presignBody.put("fileHash", fileHash);
      presignBody.put("fileName", fileName);
      ArrayNode nums = presignBody.putArray("partNumbers");
      partNumbers.forEach(nums::add);

      JsonNode presign = apiPost(
          "/openapi/v1/assets/material/upload/multipart/presign-parts", presignBody);

      ArrayNode completedParts = JSON.createArrayNode();
      try (RandomAccessFile raf = new RandomAccessFile(FILE_PATH.toFile(), "r")) {
        for (JsonNode item : presign.get("urls")) {
          int partNumber = item.get("partNumber").asInt();
          long offset = (long) (partNumber - 1) * partSize;
          int len = (int) Math.min(partSize, fileSize - offset);
          byte[] chunk = new byte[len];
          raf.seek(offset);
          raf.readFully(chunk);

          HttpRequest put = HttpRequest.newBuilder()
              .uri(URI.create(item.get("url").asText()))
              .PUT(HttpRequest.BodyPublishers.ofByteArray(chunk))
              .timeout(Duration.ofMinutes(10))
              .build();
          HttpResponse<String> putRes = HTTP.send(put, HttpResponse.BodyHandlers.ofString());
          if (putRes.statusCode() < 200 || putRes.statusCode() >= 300) {
            throw new IOException("Part PUT failed: " + putRes.statusCode());
          }
          String eTag = putRes.headers().firstValue("etag")
              .orElse("").replace("\"", "");

          ObjectNode part = JSON.createObjectNode();
          part.put("partNumber", partNumber);
          part.put("eTag", eTag);
          completedParts.add(part);
        }
      }

      ObjectNode completeBody = JSON.createObjectNode();
      completeBody.put("materialId", init.get("materialId").asText());
      completeBody.put("uploadId", init.get("uploadId").asText());
      completeBody.put("fileHash", fileHash);
      completeBody.put("fileName", fileName);
      completeBody.set("parts", completedParts);

      JsonNode material = apiPost(
          "/openapi/v1/assets/material/upload/multipart/complete", completeBody);

      return material.get("materialId").asText();
    }

    static JsonNode apiPost(String path, Object body) throws Exception {
      String bodyJson = body instanceof String ? (String) body : JSON.writeValueAsString(body);
      HttpRequest req = HttpRequest.newBuilder()
          .uri(URI.create(API_BASE + path))
          .timeout(Duration.ofSeconds(120))
          .header("Authorization", "Bearer " + API_KEY)
          .header("Content-Type", "application/json")
          .header("X-Idempotency-Key", "multipart-" + path + "-" + UUID.randomUUID())
          .POST(HttpRequest.BodyPublishers.ofString(bodyJson))
          .build();
      HttpResponse<String> res = HTTP.send(req, HttpResponse.BodyHandlers.ofString());
      JsonNode root = JSON.readTree(res.body());
      if (root.path("code").asInt() != 200) {
        throw new IOException(root.path("message").asText("API error"));
      }
      return root.get("data");
    }

    static JsonNode apiPost(String path, Map<String, String> body) throws Exception {
      return apiPost(path, (Object) JSON.valueToTree(body));
    }

    static String md5File(Path path) throws Exception {
      MessageDigest md5 = MessageDigest.getInstance("MD5");
      md5.update(java.nio.file.Files.readAllBytes(path));
      StringBuilder sb = new StringBuilder();
      for (byte b : md5.digest()) {
        sb.append(String.format("%02x", b));
      }
      return sb.toString();
    }
  }
  ```
</CodeGroup>

## After upload

Only after the full upload flow succeeds, use the registered `materialId` in `source.materialId` for [media translation](/guides/products/media-translation). Manage assets via [Materials](/guides/assets/materials).
