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" }]
}'| Parameter | Type | Required | Description |
|---|---|---|---|
route | string | Yes | Which validation preset to use — see the table below. |
files | array | Yes | One 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:
- For each entry in
parts,PUTthe corresponding byte range toparts[i].uploadUrl, withContent-Lengthset toparts[i].sizeexactly. The range is[(partNumber - 1) * partSize, + size]. Parts may be sent in parallel. - Keep each response’s
ETagheader, stripped of its surrounding quotes. POSTtocompleteUrlwithContent-Type: application/xmland the parts sorted ascending:
<CompleteMultipartUpload>
<Part><ETag>abc123</ETag><PartNumber>1</PartNumber></Part>
<Part><ETag>def456</ETag><PartNumber>2</PartNumber></Part>
</CompleteMultipartUpload>- If you give up,
DELETEtheabortUrl. 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.mp4Uploading 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:
| URL | Use it for | Why |
|---|---|---|
bucketUrl | Video, and anything large | Read straight from storage. The bytes never pass through our API again. |
accessUrl | Photos, or when you need a publicly fetchable URL | Served 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.
| Route | Multipart | Max files | Max size | Accepted types |
|---|---|---|---|---|
video | No | 1 | 5 GB | video/*, application/octet-stream |
tiktok_videos | Yes | 1 | 4 GB | video/mp4, video/webm |
youtube_videos | Yes | 1 | 10 GiB | video/*, application/octet-stream |
pinterest_videos | Yes | 1 | 2 GB | video/mp4, video/quicktime, video/x-m4v |
threads_videos | Yes | 1 | 1 GB | video/mp4, video/quicktime |
linkedin_videos | Yes | 1 | 500 MB | video/mp4 |
photos | No | 35 | 20 MB | image/jpeg, image/png, image/webp |
instagram_photos | No | 10 | 8 MB | image/jpeg, image/png, image/webp |
threads_photos | No | 20 | 8 MB | image/jpeg, image/png |
bluesky_photos | No | 4 | 2,000,000 bytes | image/jpeg, image/png, image/webp, image/gif |
thumbnails | No | 1 | 20 MB | image/jpeg, image/png, image/webp, image/gif |
documents | No | 1 | 100 MB | PDF, 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
| URL | Valid for |
|---|---|
Multipart part URLs and completeUrl / abortUrl | 6 hours |
Single-PUT uploadUrl | 1 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": "..." }.
| Status | code | Meaning |
|---|---|---|
| 400 | INVALID_JSON | Body is not valid JSON. |
| 400 | INVALID_REQUEST | Schema violation, or a size that is missing, zero or negative. |
| 400 | TOO_MANY_FILES | More files than the route allows. Response carries limits. |
| 400 | FILE_TOO_LARGE | A declared size exceeds the route’s maximum. Response carries limits. |
| 400 | INVALID_FILE_TYPE | type is not accepted by the route. Response carries limits. |
| 403 | STORAGE_QUOTA_EXCEEDED | Rolling 30-day allowance exhausted. Response carries usage. |
| 404 | UNKNOWN_ROUTE | Unknown route. Response carries validRoutes. |
| 413 | BODY_TOO_LARGE | The JSON body itself exceeded 256 KB — you are sending the file instead of describing it. |
| 429 | — | Rate limit reached. Response carries retryAfter in seconds. |
| 503 | — | Transient backend unavailability. Retry. |