API documentation

Upload a file once, send it to the file hosts you pick, and get one share link back — from your own code.

The MirrorUploadX API is a versioned REST API. You upload a file in chunks, start a distribution to the hosts you choose, and every copy is gathered on one share page with live status. All requests go to https://api.mirroruploadx.com and every path starts with /api/v1.

This page walks through the common flow. Every endpoint, field and response is in the full API reference (Swagger), and the machine-readable OpenAPI schema works with any client generator.

Authentication

Create a key on the API keys page of your dashboard. The full key (it starts with mux_) is shown only once, so store it right away. Send it in the x-api-key header on every request; Authorization: Bearer mux_… is accepted the same way for clients that only speak bearer tokens.

Keys are scoped: a key only reaches the endpoints its scopes cover, and a call with the wrong scope returns 403 INSUFFICIENT_SCOPE with the needed scope in the message. Account settings, key management and webhooks are not reachable with an API key at all (API_KEY_NOT_ALLOWED).

ScopeReaches
upload:writeUpload sessions (create, chunks, complete, cancel), your upload quota, remote URL uploads, starting distribution
file:readListing files, file details, mirror and job status
file:writeShare settings on a file
file:deleteDeleting a file and its mirrors

Upload a file and distribute it

  1. Open a session with POST /api/v1/upload-sessions. filename and sizeBytes (the exact byte length) are required; mimeType and a UUID idempotencyKey are optional — repeating a request with the same key returns the existing session instead of opening a second one. The reply carries data.sessionId (not id) and data.chunkSize.
  2. Send the bytes with PATCH /api/v1/upload-sessions/{id}/chunks: raw bytes, content type application/offset+octet-stream, and the chunk's start position in the upload-offset header. Chunks go in order — use data.uploadedBytes from each reply as the next offset. Send chunks of chunkSize bytes; every chunk except the last must be at least 5 MiB, and an upload can have at most 10 000 chunks. The final chunk's reply carries data.fileId.
  3. Distribute with POST /api/v1/mirror-tasks/start: fileId and providerSlugs are required (slugs come from GET /api/v1/providers). Optional: minSuccess (default 1 — how many hosts must accept the file for the job to count as completed), sharePassword, expiresInDays or expiresAt. The reply has uploadJobId, taskCount, shareSlug, alreadyStarted and skipped[] — a host that cannot take this file is skipped, not failed, so read skipped[]. One file can go to at most 50 hosts right now.
  4. Watch it finish with GET /api/v1/mirror-tasks/{uploadJobId} every few seconds until data.terminal is true. The reply lists every host in tasks[] (providerSlug, status, mirrorUrl, errorCode) and the share link in data.shareUrl.

The largest file accepted right now is 5.00 GB.

curl (bash, needs jq)
set -euo pipefail
API=https://api.mirroruploadx.com
FILE=clip.mp4
SIZE=$(wc -c < "$FILE" | tr -d ' ')

# 1. Open an upload session
SESSION_JSON=$(curl -sS --fail-with-body -X POST "$API/api/v1/upload-sessions" \
  -H "x-api-key: $MUX_API_KEY" -H "content-type: application/json" \
  -d '{"filename":"clip.mp4","sizeBytes":'"$SIZE"',"mimeType":"video/mp4"}')
SESSION=$(echo "$SESSION_JSON" | jq -r .data.sessionId)
CHUNK=$(echo "$SESSION_JSON" | jq -r .data.chunkSize)

# 2. Send the bytes in order, one chunk per request
OFFSET=0
while [ "$OFFSET" -lt "$SIZE" ]; do
  dd if="$FILE" of=part.bin bs="$CHUNK" skip=$((OFFSET / CHUNK)) count=1 status=none
  REPLY=$(curl -sS --fail-with-body -X PATCH "$API/api/v1/upload-sessions/$SESSION/chunks" \
    -H "x-api-key: $MUX_API_KEY" \
    -H "upload-offset: $OFFSET" \
    -H "content-type: application/offset+octet-stream" \
    --data-binary @part.bin)
  OFFSET=$(echo "$REPLY" | jq -r .data.uploadedBytes)
done
FILE_ID=$(echo "$REPLY" | jq -r .data.fileId)

# 3. Distribute to the hosts you pick
JOB=$(curl -sS --fail-with-body -X POST "$API/api/v1/mirror-tasks/start" \
  -H "x-api-key: $MUX_API_KEY" -H "content-type: application/json" \
  -d '{"fileId":"'"$FILE_ID"'","providerSlugs":["gofile","pixeldrain"],"minSuccess":1}' \
  | jq -r .data.uploadJobId)

# 4. Poll until the job is terminal, then print the share link
until curl -sS --fail-with-body "$API/api/v1/mirror-tasks/$JOB" \
        -H "x-api-key: $MUX_API_KEY" > job.json \
      && [ "$(jq -r .data.terminal job.json)" = "true" ]; do
  sleep 5
done
jq -r '.data.status, .data.shareUrl' job.json
Python (requests)
import os, time, requests

API = "https://api.mirroruploadx.com"
H = {"x-api-key": os.environ["MUX_API_KEY"]}

def call(method, path, headers=None, **kw):
    r = requests.request(method, API + path, headers={**H, **(headers or {})}, timeout=120, **kw)
    body = r.json()
    if not body.get("success"):
        err = body["error"]
        raise RuntimeError(f'{r.status_code} {err["code"]}: {err["message"]}')
    return body["data"]

path = "clip.mp4"
size = os.path.getsize(path)

# 1. Open an upload session
s = call("POST", "/api/v1/upload-sessions",
         json={"filename": os.path.basename(path), "sizeBytes": size, "mimeType": "video/mp4"})
session, chunk = s["sessionId"], s["chunkSize"]

# 2. Send the bytes in order; the reply says where to continue
offset, last = 0, None
with open(path, "rb") as f:
    while offset < size:
        f.seek(offset)
        last = call("PATCH", f"/api/v1/upload-sessions/{session}/chunks",
                    headers={"upload-offset": str(offset),
                             "content-type": "application/offset+octet-stream"},
                    data=f.read(chunk))
        offset = last["uploadedBytes"]
file_id = last["fileId"]

# 3. Distribute
job = call("POST", "/api/v1/mirror-tasks/start",
           json={"fileId": file_id, "providerSlugs": ["gofile", "pixeldrain"], "minSuccess": 1})
for sk in job["skipped"]:
    print("skipped", sk["slug"], sk["reason"], sk["detail"])

# 4. Poll until terminal
job_id = job["uploadJobId"]
while True:
    st = call("GET", f"/api/v1/mirror-tasks/{job_id}")
    if st["terminal"]:
        break
    time.sleep(5)
print(st["status"], st["shareUrl"])
for t in st["tasks"]:
    print(t["providerSlug"], t["status"], t["mirrorUrl"] or t["errorCode"])
Node.js (fetch)
// Node.js 18+ (global fetch), ES module
import { open, stat } from 'node:fs/promises';
import { basename } from 'node:path';

const API = 'https://api.mirroruploadx.com';
const KEY = process.env.MUX_API_KEY;

async function call(method, path, { json, body, headers = {} } = {}) {
  const res = await fetch(API + path, {
    method,
    headers: { 'x-api-key': KEY, ...(json ? { 'content-type': 'application/json' } : {}), ...headers },
    body: json ? JSON.stringify(json) : body,
  });
  const out = await res.json();
  if (!out.success) throw new Error(`${res.status} ${out.error.code}: ${out.error.message}`);
  return out.data;
}

const path = 'clip.mp4';
const { size } = await stat(path);

// 1. Open an upload session
const { sessionId, chunkSize } = await call('POST', '/api/v1/upload-sessions', {
  json: { filename: basename(path), sizeBytes: size, mimeType: 'video/mp4' },
});

// 2. Send the bytes in order; the reply says where to continue
const fh = await open(path);
let offset = 0;
let last;
while (offset < size) {
  const buf = Buffer.alloc(Math.min(chunkSize, size - offset));
  await fh.read(buf, 0, buf.length, offset);
  last = await call('PATCH', `/api/v1/upload-sessions/${sessionId}/chunks`, {
    headers: { 'upload-offset': String(offset), 'content-type': 'application/offset+octet-stream' },
    body: buf,
  });
  offset = last.uploadedBytes;
}
await fh.close();

// 3. Distribute
const job = await call('POST', '/api/v1/mirror-tasks/start', {
  json: { fileId: last.fileId, providerSlugs: ['gofile', 'pixeldrain'], minSuccess: 1 },
});
for (const s of job.skipped) console.log('skipped', s.slug, s.reason, s.detail);

// 4. Poll until terminal
let st;
do {
  await new Promise((r) => setTimeout(r, 5000));
  st = await call('GET', `/api/v1/mirror-tasks/${job.uploadJobId}`);
} while (!st.terminal);
console.log(st.status, st.shareUrl);
for (const t of st.tasks) console.log(t.providerSlug, t.status, t.mirrorUrl ?? t.errorCode);

Skip the second call

Pass distribution when you open the session and the server starts distributing as soon as the last chunk lands. Check data.distribution.started in the final chunk's reply: when it is false the upload still succeeded, but nothing was queued.

curl
curl -sS -X POST "https://api.mirroruploadx.com/api/v1/upload-sessions" \
  -H "x-api-key: $MUX_API_KEY" -H "content-type: application/json" \
  -d '{
    "filename": "clip.mp4",
    "sizeBytes": 10485760,
    "mimeType": "video/mp4",
    "distribution": { "providerSlugs": ["gofile", "pixeldrain"], "minSuccess": 1 }
  }'
# The reply to the final chunk then carries data.distribution:
#   { "started": true, "uploadJobId": "…", "taskCount": 2, "shareSlug": "…", "skipped": [] }
#   or { "started": false, "error": { "code": "…", "message": "…" } }

If the final reply is lost

Call GET /api/v1/upload-sessions/{id}: a fileId means the file exists; finalizable: true means every byte arrived and POST /api/v1/upload-sessions/{id}/complete finishes it. That call is safe to retry and never creates a second file. A mismatched offset gets 409 CHUNK_OFFSET_CONFLICT — send HEAD on the session and resume from its Upload-Offset header. Calling start again for a running job is not an error: you get 200 with alreadyStarted: true.

Upload from a URL

POST /api/v1/remote-uploads takes { "urls": [...] } and fetches each file for you (scope upload:write). Only http and https links are fetched, private network addresses are refused, and only a limited number of redirects is followed. One request carries at most 10 URLs, and each file can be up to 100.0 MB — both are returned by GET /api/v1/platform; split longer lists.

The request waits while the files download. If the connection drops first, the file still lands in your account — find it with GET /api/v1/files. Each fileId is then distributed with POST /api/v1/mirror-tasks/start, as above.

curl
curl -sS -X POST "https://api.mirroruploadx.com/api/v1/remote-uploads" \
  -H "x-api-key: $MUX_API_KEY" -H "content-type: application/json" \
  -d '{"urls":["https://example.com/files/archive.zip"]}'

# One result per URL, in order. A failed URL does not fail the batch:
# {"success":true,"data":{"results":[
#   {"url":"https://example.com/files/archive.zip","ok":true,
#    "fileId":"…","scan":…,"distributable":true}
# ]}}
# or {"url":"…","ok":false,"code":"REMOTE_TOO_LARGE","error":"…"}
#
# Then distribute each fileId with POST /api/v1/mirror-tasks/start.

Webhooks

Add an endpoint on the Webhooks page. The URL must use https://; its signing secret (whsec_…) is shown once, when you create it. An endpoint with no events selected receives all of them.

EventWhen
upload.completedEvery selected host accepted the file
upload.partially_completedAt least minSuccess hosts accepted the file, some did not
upload.failedFewer hosts than minSuccess accepted the file
upload.quarantinedThe malware scan flagged the file (only when scanning is enabled)
provider.upload_succeededOne host accepted the file — sent once per host
provider.upload_failedOne host gave up on the file — sent once per host
webhook.testYou pressed Test on the Webhooks page

Each delivery is one POST with a JSON body and the headers X-MUX-Event and X-MUX-Signature. Any 2xx answer within 10 seconds counts as delivered; redirects are not followed. A failed delivery is not retried — keep your own log, and use GET /api/v1/mirror-tasks/{uploadJobId} to catch up. After 15 failed deliveries in a row the endpoint is paused; turning it back on resets the count.

Body
{
  "event": "provider.upload_succeeded",
  "createdAt": "2026-09-25T12:00:00.000Z",
  "data": {
    "uploadJobId": "…",
    "fileId": "…",
    "provider": "gofile",
    "providerName": "Gofile",
    "status": "succeeded",
    "mirrorUrl": "https://…"
  }
}

upload.* events carry uploadJobId, fileId and status; provider.upload_failed carries errorCode instead of mirrorUrl.

Verifying the signature

The header looks like t=1727265600,v1=5f2b…: t is the signing time in Unix seconds, v1 is the hex HMAC-SHA256 of <t>.<raw body> keyed with your endpoint secret. Hash the raw bytes exactly as received, compare in constant time, and reject timestamps more than 5 minutes old.

Node.js (Express)
import express from 'express';
import { createHmac, timingSafeEqual } from 'node:crypto';

function verify(rawBody, header, secret, toleranceSec = 300) {
  const parts = Object.fromEntries(
    header.split(',').map((kv) => {
      const i = kv.indexOf('=');
      return [kv.slice(0, i).trim(), kv.slice(i + 1).trim()];
    }),
  );
  const t = Number(parts.t);
  if (!Number.isFinite(t) || !parts.v1) return false;
  if (Math.abs(Date.now() / 1000 - t) > toleranceSec) return false; // too old: replay
  const expected = createHmac('sha256', secret).update(`${parts.t}.`).update(rawBody).digest();
  const got = Buffer.from(parts.v1, 'hex');
  return got.length === expected.length && timingSafeEqual(got, expected);
}

const app = express();
// Raw bytes, not parsed JSON: the signature covers the body exactly as sent.
app.post('/hooks/mirroruploadx', express.raw({ type: 'application/json' }), (req, res) => {
  if (!verify(req.body, req.get('x-mux-signature') ?? '', process.env.MUX_WEBHOOK_SECRET)) {
    return res.sendStatus(401);
  }
  const { event, data } = JSON.parse(req.body);
  console.log(event, data.uploadJobId);
  res.sendStatus(204); // any 2xx counts as delivered
});
app.listen(3000);
Python (Flask)
import hashlib, hmac, os, time
from flask import Flask, abort, request

def verify(raw_body: bytes, header: str, secret: str, tolerance: int = 300) -> bool:
    parts = dict(kv.strip().split("=", 1) for kv in header.split(",") if "=" in kv)
    t, v1 = parts.get("t", ""), parts.get("v1", "")
    if not t.isdigit() or not v1 or abs(time.time() - int(t)) > tolerance:
        return False
    expected = hmac.new(secret.encode(), t.encode() + b"." + raw_body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, v1)

app = Flask(__name__)

@app.post("/hooks/mirroruploadx")
def hook():
    # get_data(): the raw bytes, exactly as signed
    if not verify(request.get_data(), request.headers.get("X-MUX-Signature", ""),
                  os.environ["MUX_WEBHOOK_SECRET"]):
        abort(401)
    payload = request.get_json()
    print(payload["event"], payload["data"].get("uploadJobId"))
    return "", 204  # any 2xx counts as delivered

Rate limits

Limits are counted per API key, not per IP address. Every response on a limited endpoint carries X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset (epoch seconds). Going over returns 429 RATE_LIMITED with a Retry-After header — wait that many seconds. Chunk uploads get a wide budget; the narrow limits are on opening sessions and starting distributions.

The exact numbers for your keys, and how much of each you have used right now, are on the API keys page. Which endpoints can return 429 is listed in the API reference.

Errors

Successful replies are { "success": true, "data": … }. Errors use one shape, next to the HTTP status. code is stable — branch on it; message is for people and may change. details is added when there is more context, and requestId helps us find your request if you contact support.

Error
{
  "success": false,
  "error": {
    "code": "INSUFFICIENT_SCOPE",
    "message": "This endpoint needs one of these API key scopes: upload:write.",
    "requestId": "…",
    "retryable": false
  }
}
CodeHTTPMeaning
NO_TOKEN401No API key was sent
INVALID_API_KEY401The key is wrong or revoked
INSUFFICIENT_SCOPE403The key lacks the scope this endpoint needs
API_KEY_NOT_ALLOWED403This endpoint cannot be used with an API key
VALIDATION_ERROR400The body or a parameter is invalid; see details
TOO_MANY_MIRRORS400More hosts than one file may have
QUOTA_EXCEEDED409A daily or concurrent upload limit; see details
CHUNK_OFFSET_CONFLICT409upload-offset does not match the bytes received
CHUNK_TOO_SMALL409A chunk other than the last is under 5 MiB
RATE_LIMITED429Too many requests; wait for Retry-After
MAINTENANCE503New uploads are paused for now; retry after Retry-After

The API reference lists the codes each endpoint can return.