Getting started
Quickstart
Upload a WAV file of one person talking, run a job that enhances the voice, and download the result. Every step has a curl and a Python version, and the Python steps add up to the script at the end.
Before you begin
You need:
- An API key with the
jobs:writeandoutputs:readscopes, as described in Authentication. - A WAV file of one person talking, at least one second long, saved as
host.wavin the current directory.
Run the curl steps in order in one shell, with curl 7.84 or later and jq installed. The Python steps need Python 3.9 or later and the requests package. Start by setting your key:
export NOMONO_API_KEY="nm_sk_..."Upload the file
Uploading a file takes three requests.
Create an upload
Send the file name and its size in bytes, and the response gives you the
upload_id, thepart_sizeand a URL for each part.curl -sS https://platform-api.nomono.co/v1/uploads \ -H "Authorization: Bearer $NOMONO_API_KEY" \ -H "Content-Type: application/json" \ -o upload.json \ -d @- <<EOF{"filename": "host.wav", "size_bytes": $(wc -c < host.wav)}EOFUPLOAD_ID=$(jq -r .upload_id upload.json)For a file of 176,084,204 bytes, the response is:
201 Created{ "upload_id": "upl_01a1446f235c7448abbe096bfd043ae3", "part_size": 67108864, "parts": [ {"part_number": 1, "url": "https://..."}, {"part_number": 2, "url": "https://..."}, {"part_number": 3, "url": "https://..."} ], "expires_at": "2026-10-17T09:02:11.204518Z"}Send the parts
Every part is
part_sizebytes, 64 MiB, except the last.PUTeach part to its URL without the API key, and keep theETagheader from each response.split -b "$(jq .part_size upload.json)" host.wav host.wav.part.PARTS="[]"PART_NUMBER=1for PART_FILE in host.wav.part.*; do URL=$(jq -r ".parts[$PART_NUMBER - 1].url" upload.json) ETAG=$(curl -sS --fail -T "$PART_FILE" -o /dev/null -w "%header{etag}" "$URL") PARTS=$(jq -c --argjson part_number "$PART_NUMBER" --arg etag "$ETAG" \ '. + [{part_number: $part_number, etag: $etag}]' <<< "$PARTS") PART_NUMBER=$((PART_NUMBER + 1))donerm host.wav.part.*Complete the upload
Send each part number with its ETag, and the API checks the file and returns the source:
curl -sS "https://platform-api.nomono.co/v1/uploads/$UPLOAD_ID/complete" \ -H "Authorization: Bearer $NOMONO_API_KEY" \ -H "Content-Type: application/json" \ -o source.json \ -d @- <<EOF{"parts": $PARTS}EOFSOURCE_ID=$(jq -r .source_id source.json)201 Created{ "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"}
The upload and its URLs expire after 24 hours. If you need new URLs before then, see Uploads.
Run a job
Create a session
A session groups the sources of one recording and gives each one a role. Here the only source is a
microphonelabelledHost, and the label is used to name its outputs.curl -sS https://platform-api.nomono.co/v1/sessions \ -H "Authorization: Bearer $NOMONO_API_KEY" \ -H "Content-Type: application/json" \ -o session.json \ -d @- <<EOF{"sources": [{"source_id": "$SOURCE_ID", "role": "microphone", "label": "Host"}]}EOFSESSION_ID=$(jq -r .session_id session.json)Create a job
This job enhances the voice with the
listening.fullpreset and returns the track as WAV. It's billed for the length of the session, in minutes.curl -sS https://platform-api.nomono.co/v1/jobs \ -H "Authorization: Bearer $NOMONO_API_KEY" \ -H "Content-Type: application/json" \ -o job.json \ -d @- <<EOF{ "session_id": "$SESSION_ID", "products": {"enhancement": {"preset": "listening.full"}}, "outputs": [{"type": "tracks", "format": "wav"}]}EOFJOB_ID=$(jq -r .job_id job.json)201 Created{ "job_id": "job_01a14471b2d47f19b6a3c0e5d8f9a1b2", "session_id": "ses_01a14471a03b7c56a1e28d9f4b6c7d8e", "status": "queued", "progress": 0.0, "products": {"enhancement": {"preset": "listening.full"}}, "outputs_requested": [{"type": "tracks", "format": "wav"}], "minutes": 30.57, "created_at": "2026-10-16T09:13:02.071624Z", "started_at": null, "finished_at": null, "error": null, "retry_of": null}If you might retry this request, send it with an
Idempotency-Keyas described in Idempotency.Wait for the job
Poll the job every few seconds until its status is
done,failedorcancelled. Itsprogressgoes from 0 to 1 along the way.STATUS=$(jq -r .status job.json)while [ "$STATUS" != done ] && [ "$STATUS" != failed ] && [ "$STATUS" != cancelled ]; do sleep 5 curl -sS "https://platform-api.nomono.co/v1/jobs/$JOB_ID" \ -H "Authorization: Bearer $NOMONO_API_KEY" \ -o job.json STATUS=$(jq -r .status job.json) echo "$STATUS"doneIf the job fails, its
error.messagetells you what to do. Instead of polling, you can also use webhooks to be notified when a job ends.
Download the tracks
List the outputs
A
tracksoutput gives you three files for each microphone: the WAV, an Opus copy and waveform data. Each file comes with a download URL that's valid for 24 hours.curl -sS "https://platform-api.nomono.co/v1/jobs/$JOB_ID/outputs" \ -H "Authorization: Bearer $NOMONO_API_KEY" \ -o outputs.jsonjq -r '.outputs[] | "\(.type) \(.format) \(.size_bytes)"' outputs.jsonThe curl step prints:
Outputtracks wav 176084204tracks opus 21208733tracks waveform 21402Each output in the list looks like this:
Output object{ "output_id": "out_01a144729c0e7b31a8f45d2c6e9b0a17", "type": "tracks", "label": "Host", "source_id": "src_01a1446f235c7448abbe096c9b9adf6c", "format": "wav", "profile": null, "size_bytes": 176084204, "url": "https://...", "expires_at": "2026-10-17T09:45:20Z"}Download a file
Download a file with a plain
GETof itsurl. Here that saves the enhanced track ashost-enhanced.wav.curl -sS -o host-enhanced.wav \ "$(jq -r '.outputs[] | select(.type == "tracks" and .format == "wav") | .url' outputs.json)"To get fresh URLs, list the outputs again. The files themselves are kept until the session is deleted.
The whole script
Save the script below as quickstart.py and run it with the path of your file. It saves the enhanced track next to your file as host-enhanced.wav.
pip install requestspython quickstart.py host.wavimport osimport sysimport timeimport requestsAPI_URL = os.environ.get("NOMONO_API_URL", "https://platform-api.nomono.co/v1")HEADERS = {"Authorization": f"Bearer {os.environ['NOMONO_API_KEY']}"}def create_upload(path: str) -> dict: response = requests.post( f"{API_URL}/uploads", headers=HEADERS, json={"filename": os.path.basename(path), "size_bytes": os.path.getsize(path)}, ) response.raise_for_status() return response.json()def upload_parts(upload: dict, path: str) -> list[dict]: parts = [] with open(path, "rb") as audio: for part in upload["parts"]: audio.seek((part["part_number"] - 1) * upload["part_size"]) # The URL is signed; the request carries no API key. response = requests.put(part["url"], data=audio.read(upload["part_size"])) response.raise_for_status() parts.append({"part_number": part["part_number"], "etag": response.headers["ETag"]}) return partsdef complete_upload(upload_id: str, parts: list[dict]) -> dict: response = requests.post( f"{API_URL}/uploads/{upload_id}/complete", headers=HEADERS, json={"parts": parts}, ) response.raise_for_status() return response.json()def create_session(source_id: str) -> dict: response = requests.post( f"{API_URL}/sessions", headers=HEADERS, json={"sources": [{"source_id": source_id, "role": "microphone", "label": "Host"}]}, ) response.raise_for_status() return response.json()def create_job(session_id: str) -> dict: response = requests.post( f"{API_URL}/jobs", headers=HEADERS, json={ "session_id": session_id, "products": {"enhancement": {"preset": "listening.full"}}, "outputs": [{"type": "tracks", "format": "wav"}], }, ) response.raise_for_status() return response.json()def wait_for_job(job_id: str) -> dict: while True: response = requests.get(f"{API_URL}/jobs/{job_id}", headers=HEADERS) response.raise_for_status() job = response.json() print(f"{job['status']}, progress {job['progress']:.0%}") if job["status"] in ("done", "failed", "cancelled"): return job time.sleep(5)def list_outputs(job_id: str) -> list[dict]: response = requests.get(f"{API_URL}/jobs/{job_id}/outputs", headers=HEADERS) response.raise_for_status() return response.json()["outputs"]def download(url: str, path: str) -> None: # The URL is signed; the request carries no API key. with requests.get(url, stream=True) as response: response.raise_for_status() with open(path, "wb") as file: for chunk in response.iter_content(chunk_size=1024 * 1024): file.write(chunk)def main(path: str) -> None: upload = create_upload(path) parts = upload_parts(upload, path) source = complete_upload(upload["upload_id"], parts) print(f"Uploaded {source['source_id']}, {source['duration_s']} seconds") session = create_session(source["source_id"]) job = create_job(session["session_id"]) print(f"Created {job['job_id']}") job = wait_for_job(job["job_id"]) if job["status"] != "done": sys.exit(f"The job is {job['status']}: {job['error']}") for output in list_outputs(job["job_id"]): print(output["type"], output["format"], output["size_bytes"]) if output["type"] == "tracks" and output["format"] == "wav": enhanced_path = os.path.splitext(path)[0] + "-enhanced.wav" download(output["url"], enhanced_path) print(f"Saved {enhanced_path}")if __name__ == "__main__": main(sys.argv[1])Next steps
- Sessions explains recordings with several microphones, a mixdown or a bed.
- Jobs describes the presets, products and outputs you can ask for.
- Webhooks shows how to get notified when a job ends.
- The API reference lists every route.