Uploading large masters
The master goes straight to storage, not through the API. A small master is one presigned PUT. A large one is sent as parallel parts through the multipart calls. This page covers both, the limits that apply to every upload, the formats accepted, and what the CLI checks before it sends a byte. The SDK picks the right method for you. Read this if you upload with your own HTTP client.
Limits
| Limit | Value | Where it is enforced |
|---|---|---|
| Duration per file | 180 minutes | At create from durationSec, and again after analysis from the true duration |
| Size per file | 100 GiB (107 374 182 400 bytes) | At create from sizeBytes, if you send it |
One presigned PUT | 5 GiB | By storage. A larger master must use multipart |
| Parts in a multipart upload | 1 to 10 000 | multipart/init |
| Part and upload URLs | Valid for 1 hour | Storage |
Send sizeBytes on create. Without it, a master over the size limit is accepted at create and fails after you have uploaded it. With it, the create is refused at once with file-too-large, and a durationSec over 180 minutes is refused with file-too-long. GET /v1/usage returns the limits that apply to your account as limits.perFileMinutes and limits.perFileBytes, so read them before a long upload.
Accepted files
An ADM master as a WAV, RF64 or BW64 file, or a bare ambisonic WAV of first, second or third order. RF64 and BW64 matter for large masters: a plain WAV cannot express a size over 4 GB in its 32-bit header, so large masters are written in the 64-bit form. The SDK and CLI read the file's header locally and accept RIFF, RF64 and BW64.
A single presigned PUT
POST /v1/conversions returns uploadUrl. Send the master to it with a plain PUT:
curl -X PUT "$UPLOAD_URL" \
-H "Content-Type: audio/wav" \
-H "Content-Length: $(stat -f %z master.wav 2>/dev/null || stat -c %s master.wav)" \
--data-binary @master.wav- Send the raw bytes as the body, with an explicit
Content-Length. Do not use chunked transfer encoding. - Send
Content-Type: audio/wav. The URL is signed for that type. - Send no
Authorizationheader. The authority is in the URL itself, and the URL expires after one hour. - A
2xxanswer means the master is in place. Then callPOST /v1/conversions/{id}/analyze.
The URL is for a single request of up to 5 GiB. Above that, use multipart. The SDK switches to multipart above 16 MiB whenever the source can be read at random offsets, because parallel parts are faster.
Multipart upload
Multipart splits the master into parts, uploads them in parallel, and assembles them. The conversion must still be uploading for any of the three calls, otherwise they answer 409 already-started.
1. Init
POST /v1/conversions/{id}/multipart/init with the number of parts:
{ "parts": 12 }parts is a whole number from 1 to 10 000, otherwise invalid-multipart. The answer has the upload id and one presigned URL per part, all issued up front:
{
"uploadId": "mpu_7f3a2c91",
"partUrls": [
{ "partNumber": 1, "url": "https://…" },
{ "partNumber": 2, "url": "https://…" }
]
}2. Upload the parts
PUT each part's bytes to its URL, with no Authorization header, and keep the ETag response header from each. Every part except the last must be at least 5 MiB. The SDK's plan is 8 MiB parts, increased in whole MiB when the master is so large that 8 MiB parts would exceed 10 000, with four parts in flight at a time and eight from 1 GiB upwards. It retries a failed part three times with a doubling pause before it gives up. Send every part well inside the one hour the URLs are valid for.
3. Complete
POST /v1/conversions/{id}/multipart/complete with every part's number and ETag:
{
"uploadId": "mpu_7f3a2c91",
"parts": [
{ "partNumber": 1, "etag": "\"9b2cf535f27731c974343645a3985328\"" },
{ "partNumber": 2, "etag": "\"e4d909c290d0fb1ca068ffaddf22cbd0\"" }
]
}It answers { "ok": true }, after which the master is in place exactly as if a single PUT had landed. Then call analyze. If storage refuses to finalise the upload, the answer is 502 upload-failed: start the upload again from init.
Abort
POST /v1/conversions/{id}/multipart/abort with { "uploadId": "…" } frees the stored parts. It is best effort and never fails with a server error. The SDK aborts for you when a part fails for good.
// Multipart by hand, with fetch. Node 20+; `api` is the base URL, `headers` carries your Authorization.
import { open } from "node:fs/promises";
const PART = 8 * 1024 * 1024;
const file = await open("master.wav");
const size = (await file.stat()).size;
const count = Math.ceil(size / PART);
const init = await (await fetch(`${api}/v1/conversions/${id}/multipart/init`, {
method: "POST", headers: { ...headers, "Content-Type": "application/json" },
body: JSON.stringify({ parts: count }),
})).json();
const parts = [];
for (const { partNumber, url } of init.partUrls) {
const start = (partNumber - 1) * PART;
const length = Math.min(PART, size - start);
const buf = Buffer.alloc(length);
await file.read(buf, 0, length, start);
const res = await fetch(url, { method: "PUT", body: buf }); // no Authorization header
if (!res.ok) throw new Error(`part ${partNumber}: ${res.status}`);
parts.push({ partNumber, etag: res.headers.get("etag") });
}
await file.close();
await fetch(`${api}/v1/conversions/${id}/multipart/complete`, {
method: "POST", headers: { ...headers, "Content-Type": "application/json" },
body: JSON.stringify({ uploadId: init.uploadId, parts }),
});
await fetch(`${api}/v1/conversions/${id}/analyze`, { method: "POST", headers });This sends the parts one at a time to keep the example short. Send several at once for speed, and retry a part that fails. A part that fails for good means calling abort and starting again.
You rarely need to write this: conversions.create({ file }) does all of it, in parallel and with retries, for a path or a Blob. A raw stream has no random access, so it always uses the single PUT and is limited to 5 GiB.
What the CLI checks first
esfera convert checks the file locally and uploads nothing until every check passes. A failure exits with code 2.
- The file can be read.
- Its header is WAV, RF64 or BW64.
- It carries ADM metadata, or is ambisonic with 4, 9 or 16 channels. A plain WAV with neither is refused.
- For an ambisonic file, the chosen preset accepts its order.
bed714_3oaandoa3need third order. - Its duration is within
limits.perFileMinutesand its size withinlimits.perFileBytes, both read fromGET /v1/usage. - Its programme count, so a multi-programme master is converted with a deliberate choice.
Do the same in your own pipeline: it costs one GET /v1/usage and a header read, and it avoids uploading a file the API would reject.
Related
- Auto-start and two-step: what to call after the upload.
- Idempotency: recovering when the create response was lost.
multipart/init,completeandabort.