Skip to Content
API ReferenceUpload MediaLarge Files (Presigned)

Large File Uploads (Presigned)

POST /api/v1/upload sends the media as part of the request body, and requests above 100 MB are rejected at our edge with 413 Payload Too Large before they ever reach the API. That limit is not a platform limit — TikTok accepts 4 GB, YouTube far more.

This endpoint removes the ceiling. You ask for presigned URLs, upload the bytes directly to our storage (they never transit the API or the edge), then publish by referencing the resulting URL. Uploads are resumable: each part can be retried on its own, so a network blip near the end of a 4 GB file costs you one part, not the whole transfer.

Nothing changes for files under 100 MB. POST /api/v1/upload with a file part keeps working exactly as before — use it, it is one request instead of three.

Endpoint

POST /api/v1/upload/presign

Unlike the other upload endpoints, this one takes application/json, not multipart/form-data.

Authentication

All requests require an API Token in the header: x-api-key: YOUR_API_TOKEN

Team-scoped tokens and the X-Team-ID header behave exactly as on every other /api/v1 route.


The flow, end to end

Phase 1 — Ask for URLs

curl -X POST https://api.multi-upload-tool.com/api/v1/upload/presign \ -H "x-api-key: YOUR_API_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "route": "tiktok_videos", "files": [{ "name": "clip.mp4", "size": 734003200, "type": "video/mp4" }] }'
ParameterTypeRequiredDescription
routestringYesWhich validation preset to use — see the table below.
filesarrayYesOne entry per file: name (string), size (exact byte length, integer > 0), type (MIME type).

size must be the exact byte length of the file. It is cryptographically bound into the signed URLs: sending more bytes than declared fails with SignatureDoesNotMatch from storage, not a friendly error.

Phase 2 — Send the bytes to storage

Multipart routes (every platform video route) return a multipart object per file:

  1. For each entry in parts, PUT the corresponding byte range to parts[i].uploadUrl, with Content-Length set to parts[i].size exactly. The range is [(partNumber - 1) * partSize, + size]. Parts may be sent in parallel.
  2. Keep each response’s ETag header, stripped of its surrounding quotes.
  3. POST to completeUrl with Content-Type: application/xml and the parts sorted ascending:
<CompleteMultipartUpload> <Part><ETag>abc123</ETag><PartNumber>1</PartNumber></Part> <Part><ETag>def456</ETag><PartNumber>2</PartNumber></Part> </CompleteMultipartUpload>
  1. If you give up, DELETE the abortUrl. This matters: an upload you neither complete nor abort leaves billable parts behind. Always abort on failure.

Single-PUT routes (video, photos, thumbnails, documents) return uploadUrl and uploadHeaders instead. PUT the whole file with exactly those headers — both content-type and content-length are signed, so altering or omitting either fails the signature.

curl -X PUT "$UPLOAD_URL" \ -H "Content-Type: video/mp4" \ --data-binary @clip.mp4

Uploading a video from a script or an AI agent? Use the video route. It signs a single PUT for the whole file — no parts to slice, no ETags to collect, no completion call. The trade-off is that a failed transfer restarts from zero, so above a few hundred megabytes on an unreliable link the multipart routes remain the better tool.

Our API is not involved in this phase at all.

Phase 3 — Publish

Pass the URL back to the normal upload endpoint as video, photo or file:

curl -X POST https://api.multi-upload-tool.com/api/v1/upload \ -H "x-api-key: YOUR_API_TOKEN" \ -F "accountId=123" \ -F "video=https://multi-upload-tools.s3.eu-central-003.backblazeb2.com/public-api%2F42%2F..." \ -F "title=My video" \ -F "privacy_level=PUBLIC"

Every file in the response carries two URLs. Which one to use:

URLUse it forWhy
bucketUrlVideo, and anything largeRead straight from storage. The bytes never pass through our API again.
accessUrlPhotos, or when you need a publicly fetchable URLServed by our media proxy — convenient and public, but every byte transits our origin.

Pass either through unchanged. Do not re-host, re-encode, or shorten it.

bucketUrl points at private storage, so fetching it yourself returns 401 — that is expected, not a broken URL. We sign it server-side when you publish. If you need a URL you can open in a browser or hand to a third party, use accessUrl.


Routes

route selects a validation preset. It does not bind the media to a platform: any presigned object may afterwards be published to any account. The target platform’s own limits still apply when you publish.

RouteMultipartMax filesMax sizeAccepted types
videoNo15 GBvideo/*, application/octet-stream
tiktok_videosYes14 GBvideo/mp4, video/webm
youtube_videosYes110 GiBvideo/*, application/octet-stream
pinterest_videosYes12 GBvideo/mp4, video/quicktime, video/x-m4v
threads_videosYes11 GBvideo/mp4, video/quicktime
linkedin_videosYes1500 MBvideo/mp4
photosNo3520 MBimage/jpeg, image/png, image/webp
instagram_photosNo108 MBimage/jpeg, image/png, image/webp
threads_photosNo208 MBimage/jpeg, image/png
bluesky_photosNo42,000,000 bytesimage/jpeg, image/png, image/webp, image/gif
thumbnailsNo120 MBimage/jpeg, image/png, image/webp, image/gif
documentsNo1100 MBPDF, PPT, PPTX, DOC, DOCX

A hard ceiling of 10 GiB per file applies to every route for API callers. video stops at 5 GB because that is the largest object S3 accepts in a single PUT — beyond it, a multipart route is the only option.

Expiry

URLValid for
Multipart part URLs and completeUrl / abortUrl6 hours
Single-PUT uploadUrl1 hour

There is no refresh endpoint. Upload parts in parallel rather than strictly in series — a multi-gigabyte serial transfer over a modest uplink can outlive the window, and an expired multipart leaves parts that must be aborted.

Quota and rate limits

Each successful response includes a usage block: bytes presigned over a rolling 30-day window, your plan’s allowance, and what remains. "unlimited": true means no cap applies.

The quota counts bytes presigned, not bytes currently stored — media is deleted as soon as a post completes, so stored volume is not a meaningful measure. A presign you never upload still counts for the rest of the window.

Rate limit: 120 presign requests per 15 minutes, per user (not per IP — a whole worker fleet behind one address is not throttled as one client).

Errors

Errors use the standard envelope: { "success": false, "error": "...", "code": "..." }.

StatuscodeMeaning
400INVALID_JSONBody is not valid JSON.
400INVALID_REQUESTSchema violation, or a size that is missing, zero or negative.
400TOO_MANY_FILESMore files than the route allows. Response carries limits.
400FILE_TOO_LARGEA declared size exceeds the route’s maximum. Response carries limits.
400INVALID_FILE_TYPEtype is not accepted by the route. Response carries limits.
403STORAGE_QUOTA_EXCEEDEDRolling 30-day allowance exhausted. Response carries usage.
404UNKNOWN_ROUTEUnknown route. Response carries validRoutes.
413BODY_TOO_LARGEThe JSON body itself exceeded 256 KB — you are sending the file instead of describing it.
429Rate limit reached. Response carries retryAfter in seconds.
503Transient backend unavailability. Retry.