Concepts
Uploads
Upload an audio file in parts to presigned URLs, then complete the upload to get a source you can use in a session.
How an upload works
An upload takes three steps:
Create the upload
Call
POST /v1/uploadswith the file's name and exact size, and you get back a presigned URL for each part.Send the parts
PUTeach part to its URL and keep theETagheader from each response.Complete the upload
Call
POST /v1/uploads/{upload_id}/completewith every part's number and ETag, and the response is your new source.
This uploads a 176 MB WAV file in three parts. In the curl version, PART_1_URL to PART_3_URL stand for the part URLs from the first response.
curl -X POST "https://platform-api.nomono.co/v1/uploads" \ -H "Authorization: Bearer $NOMONO_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "filename": "host.wav", "size_bytes": 176084204 }'split -b 67108864 host.wav part-curl -sS --fail -T part-aa -D - -o /dev/null "$PART_1_URL" | grep -i '^etag:'curl -sS --fail -T part-ab -D - -o /dev/null "$PART_2_URL" | grep -i '^etag:'curl -sS --fail -T part-ac -D - -o /dev/null "$PART_3_URL" | grep -i '^etag:'curl -X POST "https://platform-api.nomono.co/v1/uploads/upl_01a1446f235c7448abbe096bfd043ae3/complete" \ -H "Authorization: Bearer $NOMONO_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "parts": [ {"part_number": 1, "etag": "5d41402abc4b2a76b9719d911017c592"}, {"part_number": 2, "etag": "7d793037a0760186574b0282f2f435e7"}, {"part_number": 3, "etag": "e4d909c290d0fb1ca068ffaddf22cbd0"} ] }'Once you have the source, use its source_id to create a session.
Creating an upload
Send the file's name as filename and its exact size in bytes as size_bytes. The size can be anything from 1 byte to 4 GiB (4,294,967,296 bytes), and any other value returns 400 validation_error. We detect the format from the file's contents.
{ "upload_id": "upl_01a1446f235c7448abbe096bfd043ae3", "part_size": 67108864, "parts": [ { "part_number": 1, "url": "https://....s3.eu-west-1.amazonaws.com/...?uploadId=...&partNumber=1&X-Amz-Expires=86400&X-Amz-Signature=..." }, {"part_number": 2, "url": "https://...&partNumber=2&..."}, {"part_number": 3, "url": "https://...&partNumber=3&..."} ], "expires_at": "2026-10-17T09:02:11.204518Z"}| Field | What it holds |
|---|---|
upload_id | The upload's ID, starting upl_. |
part_size | Always 67108864 (64 MiB). |
parts | A part_number and a presigned url for every part. |
expires_at | When the upload and its part URLs expire, 24 hours after creation. |
Sending the parts
Every part is 64 MiB (67,108,864 bytes) except the last, which holds the rest of the file, so part n starts at byte (n − 1) × 67,108,864.
Send each part without your API key and with a Content-Length header that gives its exact size, which curl -T and requests set for you when you pass them the part's bytes. A part of any other length, or one sent with Transfer-Encoding: chunked, returns 403.
You can send the parts in any order and in parallel, and if you send one again, complete the upload with its new ETag. ETags come wrapped in double quotes, which you can keep or strip.
If you lose the part URLs, POST /v1/uploads/{upload_id}/parts?from=N&count=M returns new ones for parts N to N + M − 1, with count from 1 to 100:
curl -X POST "https://platform-api.nomono.co/v1/uploads/upl_01a1446f235c7448abbe096bfd043ae3/parts?from=2&count=100" \ -H "Authorization: Bearer $NOMONO_API_KEY"{ "upload_id": "upl_01a1446f235c7448abbe096bfd043ae3", "parts": [ {"part_number": 2, "url": "https://..."}, {"part_number": 3, "url": "https://..."} ], "expires_at": "2026-10-17T09:02:11.204518Z"}Reissuing doesn't extend the upload, so the new URLs still expire at its expires_at.
A missing or out-of-range from or count returns 400 validation_error, and a from past the upload's last part gets the message "from must be between 1 and 3, the upload's last part". The route can also return:
| Status and code | Message | Hint |
|---|---|---|
409 conflict | Upload is already complete | Use source {source_id}, which the upload created |
404 not_found | Upload has expired | Start a new upload |
Completing an upload
Complete the upload with every part's part_number and latest etag, and you get the source back:
{ "source_id": "src_01a1446f235c7448abbe096c9b9adf6c", "upload_id": "upl_01a1446f235c7448abbe096bfd043ae3", "filename": "host.wav", "format": "wav", "channels": 1, "sample_rate": 48000, "duration_s": 1834.21, "size_bytes": 176084204, "device": null, "status": "ready", "created_at": "2026-10-16T09:02:11.204518Z", "expires_at": "2026-10-23T09:04:37.880912Z"}If the upload is already complete, you get 201 with the same source again, and a file that isn't one of the accepted files gets 422 source_rejected. The other errors are:
| Status and code | Message | Hint |
|---|---|---|
404 not_found | Upload has expired | Start a new upload |
409 upload_incomplete | The uploaded parts add up to 134217728 of 176084204 bytes | Upload the missing parts, then complete the upload again |
409 upload_incomplete | The parts in the request are not the parts that were uploaded | List every part with the ETag its upload returned |
409 upload_incomplete | A part was uploaded again while the upload was being completed | Complete the upload again with each part's latest ETag |
{ "error": { "code": "upload_incomplete", "message": "The uploaded parts add up to 134217728 of 176084204 bytes", "hint": "Upload the missing parts, then complete the upload again" }}After a 409 upload_incomplete the upload stays open, and you complete it again once the parts are fixed.
Accepted files
- WAV (RIFF, RF64 or BW64), integer PCM up to 32 bits or float, including
WAVE_FORMAT_EXTENSIBLEand files a recorder did not finalize. - FLAC with the total sample count in its STREAMINFO block.
- Any sample rate and any number of channels.
- At least one second of audio.
- The same size as
size_bytes.
We check channel counts later, when you create the session.
Warning
A 4-channel sennheiser-ambeo bed must be a WAV file without a channel mask. A 4-channel FLAC file, or a WAV with the quad mask (0x33), uploads without an error, but every binaural or ADM job on it then fails with "Processing failed." See Beds and ambisonics.
A file we can't accept returns 422 source_rejected with the hint "Upload a WAV (PCM or float) or FLAC file of at least one second", and the upload is deleted, so fix the file and start a new upload. Two messages have a hint of their own. "FLAC header does not give the length of the audio" comes with "Encode the file again so that its header gives its length, or upload it as WAV", and "WAV file has too much metadata before its audio" with "Export the file again with less metadata before the audio".
{ "error": { "code": "source_rejected", "message": "Not a WAV or FLAC file", "hint": "Upload a WAV (PCM or float) or FLAC file of at least one second" }}| Message |
|---|
| Not a WAV or FLAC file |
| FLAC header does not start with its stream information |
| FLAC header gives a sample rate of 0 |
| FLAC header does not give the length of the audio |
| Audio is shorter than one second |
| WAV file has too much metadata before its audio |
| File is shorter than its size |
| WAV data chunk comes before the fmt chunk |
| A WAV chunk runs past the end of the file |
| WAV has no data chunk |
| WAV fmt chunk is shorter than 16 bytes |
| WAV extensible fmt chunk is shorter than 40 bytes |
| WAV encoding is not PCM or float |
| WAV header gives 0 channels |
| WAV header gives a sample rate of 0 |
| WAV header gives 0 bits per sample |
| WAV PCM is wider than 32 bits |
| WAV ds64 chunk is shorter than 16 bytes |
| File is 1234 bytes, not the 5678 declared |
| File ends before its header |
A FLAC file that starts with an ID3 tag is rejected as "Not a WAV or FLAC file", so remove the tag before you upload it again.
Abandoning an upload
To abandon an upload you haven't completed, call DELETE /v1/uploads/{upload_id}. We delete the parts you've sent and return 204.
curl -X DELETE "https://platform-api.nomono.co/v1/uploads/upl_01a1446f8e917b23a4c06d1e5f7a8b92" \ -H "Authorization: Bearer $NOMONO_API_KEY"Once an upload is complete, deleting it returns 409 conflict, "Upload is complete", and you delete its source with DELETE /v1/sources/{source_id} instead. That works as long as the source isn't in a session, because a source in a session is deleted with the session.
Expiry
An upload has to be completed within 24 hours, by its expires_at, and after that you'll need to start a new one.
A source that isn't in a session expires at its expires_at, 7 days after its upload was completed, and we delete it soon after. Once you add it to a session, it's kept for as long as the session is kept.