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
| Area | Operations |
|---|---|
| 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:
- Request a temporary upload session.
- PUT the exact file bytes to the returned upload URL.
- 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
| Scope | Allows |
|---|---|
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.