API v1
Uploads, Imports & Jobs
Learn how to upload files, import remote URLs, finalize assets, detect duplicates, and poll asynchronous Media2URL API jobs.
This page covers the ways content enters Media2URL: direct uploads from your application and asynchronous imports from reachable remote URLs.
Uploads
External uploads use a three-part flow so application servers do not have to proxy file bytes through the API:
POST /uploads/presignvalidates metadata and reserves capacity.PUTsends the exact bytes to the temporary upload URL.POST /uploads/finalizevalidates and promotes the upload into an asset.
Upload request
uploads:write is required. The required request fields are filename, content_type, and size. checksum_sha256 and privacy are optional.
curl --request POST \
--url https://api.media2url.com/v1/uploads/presign \
--header "Authorization: Bearer ${MEDIA2URL_API_KEY}" \
--header "Content-Type: application/json" \
--data '{
"filename": "release-screenshot.png",
"content_type": "image/png",
"size": 245817,
"checksum_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
"privacy": "unlisted"
}'
Supported privacy values are public, unlisted, and private. Availability of private links depends on the active account plan and feature settings.
The response includes:
| Field | Meaning |
|---|---|
upload_id | Single-use identifier passed to finalization. |
upload_url | Short-lived URL for the direct byte upload. |
expires_at | Time after which the session cannot be used. |
required_headers | Headers that must be sent with the direct PUT request. |
Upload the exact bytes
Use the returned URL and headers. Do not change the content type or upload a different file after presigning.
curl --request PUT \
--url "${UPLOAD_URL_FROM_PRESIGN_RESPONSE}" \
--header "Content-Type: image/png" \
--upload-file ./release-screenshot.png
When required_headers contains x-amz-checksum-sha256, send the supplied base64-encoded digest exactly as returned. The API compares the declared byte count and checksum with the object it receives.
Upload finalization
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: release-screenshot-20260920-001" \
--data '{"upload_id":"upl_example_123"}'
Finalization checks the temporary object, validates the declared metadata, applies content safety checks, promotes the object, and commits the reserved quota. A successful response returns an asset resource.
Idempotent finalization
Use a stable Idempotency-Key for one logical finalization attempt. Repeating the same request with the same key and body can return the original result. Reusing the key with a different body is a conflict and must be corrected by creating a new logical request key.
If the session expired, was already consumed, or failed validation, request a new presigned session rather than retrying finalization indefinitely.
Check for duplicates
Use assets:read to search the authenticated account's scope by SHA-256 checksum and optional byte size:
curl --request POST \
--url https://api.media2url.com/v1/uploads/check-duplicate \
--header "Authorization: Bearer ${MEDIA2URL_API_KEY}" \
--header "Content-Type: application/json" \
--data '{
"checksum_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
"size": 245817
}'
The response contains duplicate: true and the matching asset when one is found. A duplicate result does not create a second asset.
Common upload failures
| Status | Typical cause | Safe next action |
|---|---|---|
400 | Missing or malformed metadata. | Fix the request before retrying. |
403 | Missing scope, quota, plan, or privacy entitlement. | Check the key scope and account usage/plan. |
409 | Idempotency key or asset state conflict. | Reuse the original body or generate a new logical request key. |
410 | Upload session expired or was consumed. | Request a new presigned session. |
422 | File bytes or declared metadata failed validation. | Recalculate metadata and upload the intended bytes. |
429 | Short-window rate limit exceeded. | Wait for Retry-After and retry with backoff. |
Remote Imports
Remote imports let the API fetch a reachable source URL and create a Media2URL asset asynchronously. Use imports:write to start an import and imports:read to read its job.
Start an import
curl --request POST \
--url https://api.media2url.com/v1/imports \
--header "Authorization: Bearer ${MEDIA2URL_API_KEY}" \
--header "Content-Type: application/json" \
--header "Idempotency-Key: import-example-20260920-001" \
--data '{
"url": "https://example.com/assets/release-screenshot.png",
"filename": "release-screenshot.png",
"privacy": "public"
}'
The response contains a job_id and initial status. The source URL must be reachable by the service and must satisfy the current validation, safety, size, and account rules. Never use an import request to send a private credential-bearing URL to a third party.
Import requests consume API capacity and can fail because of source availability, content validation, quota, policy, or rate limits. Use the returned error code to decide whether a new source URL or a later retry is appropriate.
Jobs
Poll a job
curl --request GET \
--url https://api.media2url.com/v1/jobs/job_example_123 \
--header "Authorization: Bearer ${MEDIA2URL_API_KEY}"
The documented job states are:
| Status | Meaning |
|---|---|
queued | The import is waiting to be processed. |
processing | The source is being fetched and validated. |
completed | asset contains the resulting resource. |
failed | error contains a public error code and detail. |
Poll with bounded exponential backoff. Stop when the job is completed or failed; do not continue polling indefinitely.
Import examples
The following server-side JavaScript example polls with a bounded delay and stops when the job reaches a terminal state:
const apiKey = process.env.MEDIA2URL_API_KEY;
const jobId = "job_example_123";
const baseUrl = "https://api.media2url.com/v1";
for (let attempt = 0; attempt < 8; attempt += 1) {
const response = await fetch(`${baseUrl}/jobs/${jobId}`, {
headers: { Authorization: `Bearer ${apiKey}` },
});
if (!response.ok) throw new Error(await response.text());
const job = await response.json();
if (job.status === "completed") {
console.log(job.asset);
break;
}
if (job.status === "failed") throw new Error(job.error?.detail || "Import failed");
await new Promise((resolve) => setTimeout(resolve, Math.min(30_000, 1_000 * 2 ** attempt)));
}
Related Operations
Once an upload or import returns an asset, continue with Assets, versions, and folders. Use Errors, retries, and limits when a request needs retry or quota guidance.