Media2URL

API v1

Media2URL API v1

Read the Media2URL API v1 guide for authentication, API keys, scopes, presigned uploads, assets, and direct or share URLs.

Overview

The Media2URL API lets an eligible account integrate hosted media into an application without automating the website interface.

The API exposes usage, upload, asset, folder, remote import, and asynchronous job resources. Use the response from each operation as the source of truth for IDs, delivery URLs, status, and account limits.

Base URL

All public API requests use:

https://api.media2url.com/v1

For example:

curl --request GET \
  --url https://api.media2url.com/v1/usage \
  --header "Authorization: Bearer ${MEDIA2URL_API_KEY}"

Available operations

AreaOperations
Usage
Read the current API, request, storage, and bandwidth usage.
Uploads
Request a presigned URL, upload bytes directly, finalize an asset, and check duplicates.
Assets
List, retrieve, delete, replace, and inspect version history.
Folders
List and create folders for organizing assets.
Imports
Start a remote URL import and poll its asynchronous job.

Quick Start

This guide creates one asset through the complete external API flow:

  1. Request a temporary upload session.
  2. PUT the exact file bytes to the returned upload URL.
  3. Finalize the session to promote the uploaded bytes into an asset.

1. Set the API key on the server

Create an external API key in the developer dashboard and store it in your server environment. Do not put it in browser JavaScript, a mobile app bundle, a public repository, or a documentation example.

export MEDIA2URL_API_KEY="m2u_live_REPLACE_WITH_YOUR_KEY"

The examples below use a synthetic file named hello.png. Replace the size, content type, checksum, and file path with values calculated from your actual file.

2. Request an upload session

curl --request POST \
  --url https://api.media2url.com/v1/uploads/presign \
  --header "Authorization: Bearer ${MEDIA2URL_API_KEY}" \
  --header "Content-Type: application/json" \
  --data '{
    "filename": "hello.png",
    "content_type": "image/png",
    "size": 245817,
    "checksum_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
    "privacy": "public"
  }'

The response contains an upload_id, a short-lived upload_url, an expires_at timestamp, and the exact required_headers for the byte upload:

{
  "upload_id": "upl_example_123",
  "upload_url": "https://upload.example.invalid/temporary-upload",
  "expires_at": "2026-09-20T12:00:00.000Z",
  "required_headers": {
    "Content-Type": "image/png"
  }
}

Do not log or publish the returned upload URL. It grants temporary access to one upload operation.

3. Upload the bytes directly

Use the same content type and checksum headers returned in required_headers. The URL below is a variable populated from the previous response, not a URL to copy into source control.

UPLOAD_URL="https://upload.example.invalid/temporary-upload"

curl --request PUT \
  --url "${UPLOAD_URL}" \
  --header "Content-Type: image/png" \
  --upload-file ./hello.png

When the presign response includes x-amz-checksum-sha256, send the base64-encoded SHA-256 digest with that exact header. The declared size must equal the bytes actually uploaded.

4. Finalize the asset

Use a unique idempotency key for the logical upload. If the request times out after the server may have completed it, repeat the same request with the same key and unchanged body.

curl --request POST \
  --url https://api.media2url.com/v1/uploads/finalize \
  --header "Authorization: Bearer ${MEDIA2URL_API_KEY}" \
  --header "Content-Type: application/json" \
  --header "Idempotency-Key: upload-hello-png-20260920-001" \
  --data '{"upload_id":"upl_example_123"}'

A successful response returns the asset resource, including id, direct_url, share_url, and the available embed values.

JavaScript example

This server-side fetch example keeps the API key in an environment variable and checks each response before continuing:

import { readFile } from "node:fs/promises";

const apiKey = process.env.MEDIA2URL_API_KEY;
const baseUrl = "https://api.media2url.com/v1";

if (!apiKey) throw new Error("MEDIA2URL_API_KEY is required");

const presign = await fetch(`${baseUrl}/uploads/presign`, {
  method: "POST",
  headers: {
    Authorization: `Bearer ${apiKey}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    filename: "hello.png",
    content_type: "image/png",
    size: 245817,
    checksum_sha256: "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
    privacy: "public",
  }),
});

if (!presign.ok) throw new Error(await presign.text());
const session = await presign.json();

// Read the exact bytes from your server-side file or upload stream.
const bytes = await readFile("./hello.png");
const upload = await fetch(session.upload_url, {
  method: "PUT",
  headers: session.required_headers,
  body: bytes,
});
if (!upload.ok) throw new Error(`Direct upload failed: ${upload.status}`);

const finalize = await fetch(`${baseUrl}/uploads/finalize`, {
  method: "POST",
  headers: {
    Authorization: `Bearer ${apiKey}`,
    "Content-Type": "application/json",
    "Idempotency-Key": "upload-hello-png-20260920-001",
  },
  body: JSON.stringify({ upload_id: session.upload_id }),
});
if (!finalize.ok) throw new Error(await finalize.text());
console.log(await finalize.json());

Python example

import os
from pathlib import Path
import requests

api_key = os.environ["MEDIA2URL_API_KEY"]
base_url = "https://api.media2url.com/v1"
file_path = Path("hello.png")
file_bytes = file_path.read_bytes()

presign = requests.post(
    f"{base_url}/uploads/presign",
    headers={"Authorization": f"Bearer {api_key}"},
    json={
        "filename": file_path.name,
        "content_type": "image/png",
        "size": len(file_bytes),
        "checksum_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
        "privacy": "public",
    },
    timeout=30,
)
presign.raise_for_status()
session = presign.json()

upload = requests.put(
    session["upload_url"],
    headers=session["required_headers"],
    data=file_bytes,
    timeout=60,
)
upload.raise_for_status()

finalize = requests.post(
    f"{base_url}/uploads/finalize",
    headers={
        "Authorization": f"Bearer {api_key}",
        "Idempotency-Key": "upload-hello-png-20260920-001",
    },
    json={"upload_id": session["upload_id"]},
    timeout=30,
)
finalize.raise_for_status()
print(finalize.json())

Authentication

The public API uses external bearer API keys. Create and revoke these keys from the developer dashboard. A key is shown only when it is created, so store it in a server-side secret manager immediately.

Authorization header

Send the key on every API request:

Authorization: Bearer m2u_live_REPLACE_WITH_YOUR_KEY

The production key format begins with m2u_live_. Missing, malformed, revoked, expired, or unknown keys return a problem response with HTTP 401 Unauthorized.

export MEDIA2URL_API_KEY="m2u_live_REPLACE_WITH_YOUR_KEY"

curl --request GET \
  --url https://api.media2url.com/v1/usage \
  --header "Authorization: Bearer ${MEDIA2URL_API_KEY}"

Store keys safely

  • Keep the key on your server, worker, or CI secret store.
  • Do not expose it in browser bundles, mobile applications, public repositories, issue reports, screenshots, or logs.
  • Give each application a separate key so it can be revoked without interrupting other integrations.
  • Grant only the scopes the application needs.
  • Set an expiry when a key is temporary; external keys support an expiry from 1 to 365 days or no expiry.
  • Revoke and replace a key immediately if it may have been exposed.

Published scopes

ScopeAllows
usage:read
Read account API, request, storage, and bandwidth usage.
uploads:write
Request upload sessions and finalize new uploads.
assets:read
Check duplicates, list assets, read one asset, and read versions.
assets:delete
Delete an asset.
assets:replace
Request and finalize replacement uploads.
folders:read
List folders.
folders:write
Create folders.
imports:read
Read remote import job status.
imports:write
Start a remote URL import.

The default key scopes are intentionally limited to usage:read, uploads:write, and assets:read. Add broader permissions only when the application requires them.

Scope failures

An authenticated key without the required scope receives 403 Forbidden with code insufficient_scope. The error identifies the missing published scope. Do not solve this by giving every application every scope; update the key's permissions or create a purpose-specific key.

Account access

API access also depends on the active account plan, workspace access, feature availability, and abuse/security status. A valid key can still receive 403 or 503 when the account or API feature is not available. The dashboard and the returned problem response are the authoritative sources for the current account state.

Core API Workflow

For a normal upload, request a session, send the exact bytes, finalize the session, and use the returned asset ID and URLs for later operations. Remote imports follow a separate asynchronous job flow. Asset and folder operations always use the IDs returned by the API rather than values inferred from filenames or URLs.

Next Steps

Machine-readable contract

Download the OpenAPI 3.1 specification to generate a client, inspect schemas, or validate requests. The examples in these guides use the same field names and response shapes.

Public API boundary

The API is separate from Media2URL's dashboard and integration credentials. A key created for a plugin or editor integration cannot be used as an external REST API key. Use an external API key from the developer dashboard and grant only the scopes your application needs.