X-Idempotency-Key.
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.Choose an upload mode
| Mode | When to use |
|---|---|
| Single-file (presigned URL) | Smaller files, one PUT |
| Multipart | Large files, resumable parts |
Single-file upload
1
Get presigned URL
POST /openapi/v1/assets/material/upload/gen-upload-url with fileHash (MD5) and fileName.2
PUT file to CDN
If the response includes
data.material, the file was deduplicated — skip PUT. Otherwise upload bytes to data.uploadUrl.3
Complete upload
POST /openapi/v1/assets/material/upload/complete with s3Uri, materialId, fileHash, and fileName from the gen-upload-url step.Example
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);
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()
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();
}
}
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.Multipart upload
For large files:POST .../multipart/initiatePOST .../multipart/presign-parts— presigned URL per part- PUT each part to CDN and record each part
ETag POST .../multipart/complete— finalize and getmaterialId- On failure:
POST .../multipart/abort
recommendedPartSize from the initiate response (S3 minimum part size is typically 5 MB except the last part).
Example
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);
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()
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();
}
}
After upload
Only after the full upload flow succeeds, use the registeredmaterialId in source.materialId for media translation. Manage assets via Materials.
