Skip to content

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 ​

LimitValueWhere it is enforced
Duration per file180 minutesAt create from durationSec, and again after analysis from the true duration
Size per file100 GiB (107 374 182 400 bytes)At create from sizeBytes, if you send it
One presigned PUT5 GiBBy storage. A larger master must use multipart
Parts in a multipart upload1 to 10 000multipart/init
Part and upload URLsValid for 1 hourStorage

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:

bash
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 Authorization header. The authority is in the URL itself, and the URL expires after one hour.
  • A 2xx answer means the master is in place. Then call POST /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:

json
{ "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:

json
{
  "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:

json
{
  "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.

ts
// 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.

  1. The file can be read.
  2. Its header is WAV, RF64 or BW64.
  3. It carries ADM metadata, or is ambisonic with 4, 9 or 16 channels. A plain WAV with neither is refused.
  4. For an ambisonic file, the chosen preset accepts its order. bed714_3oa and oa3 need third order.
  5. Its duration is within limits.perFileMinutes and its size within limits.perFileBytes, both read from GET /v1/usage.
  6. 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.

developers.esfera.audio · the Esfera Converter API, SDK and CLI