/api
Live · self-serve keysAudio analysis as an API.
Most teams script ffmpeg for loudness checks, or ship audio to a cloud they don’t control. AudioLab puts loudness, voice quality, and signal analysis behind one key, and it can run right in the browser, so the audio never has to leave the user’s device.
Start here
Do
Your first call
From nothing to a real response in under two minutes. No account required to see the shape.
- 01 Drop a file Use the playground below. It runs the engine in your browser, no key, no upload. Open the playground
- 02 Copy the request Grab the cURL, JavaScript, or Python snippet, already shaped to the response you just saw. Grab the snippet
- 03 Add your key Add your Bearer key and POST to audiolab.tools/v1/* (the same JSON you just saw). Self-serve keys with a free tier, live now. Get a key
curl https://audiolab.tools/v1/mixlab/analyze \
-H "Authorization: Bearer $AUDIOLAB_KEY" \
-F file=@track.wav Without a key you get a trial of 20 calls a month, 3 a minute. A free key gives you 1,000 calls and 2 GB a month, no card. Reading the answer: what each field means and how far to trust it.
Endpoints
Each takes a multipart audio file, or a JSON body with a public url, and returns structured JSON. Full schemas live in the
OpenAPI spec.
- POST
/v1/mixlab/analyzeMixLab · loudness + dynamics
Loudness (BS.1770-4), true-peak, loudness range, crest factor, stereo image, and a tonal-balance spectrum.
Know if a track hits −14 LUFS, clips, or has a boomy low-end, before it ships.
- POST
/v1/voicelab/qaVoiceLab · speech quality
Speech quality: speech/silence ratio, pacing, signal-to-noise, energy in the pauses, sibilance, and clipping risk.
Catch a noisy or clipped voice take automatically.
- POST
/v1/signallabSignalLab · index + tags
A metadata index: content type, tags, clipping/silence regions, brightness and dynamics buckets, file metadata.
Tag and triage a whole library: what is it, is it clean, where is the silence.
- POST
/v1/signallab/sampleOne sound from a sample library, not a recording: material and structure, plus the character vector for a single hit or tempo and loop QC for a phrase. Whatever could not be measured carries its reason.
- POST
/v1/analyze/profileMeasure against a named profile and get back only the lenses that question needs, keyed by stable lens id. Eleven profiles: basic (free), voice, master, provenance, dataset, environment, broadcast, loop, room (reverberation from a clap), chain (lost packets) and pitch (where a voice sits and how clean it is). Add series:true for the curves and per-block lanes.
- POST
/v1/mixlab/check-targetPass or fail against a delivery target (Spotify, Apple Music, YouTube, podcast, EBU and others, or your own LUFS and true peak), with the delta per measure and, on a fail, an ffmpeg loudnorm command.
- POST
/v1/mixlab/timeseriesShort-term loudness about ten times a second, with its time-base, plus downsampled waveform peaks: for loudness charts and level meters.
- POST
/v1/mixlab/spectrumFFT bins and seven band energies with the spectral descriptors (centroid, rolloff, flatness): for spectrum displays and tonal-balance diagnosis.
- POST
/v1/voicelab/segmentsVoiced speech regions with start and end times and the level of each: when speech happens, not who is speaking. For auto-trim and chapters.
- POST
/v1/batchRun any analysis route over up to 20 items in one call: URLs and/or uploaded objectPaths (local files). Each item is metered as one call; per-item ok/error in the response.
- GET
/v1/usageWhere your key stands this month: plan, calls and data used against the limits the API enforces, and your requests per minute. The same numbers as /account. Needs a key. Reading it is free and does not count as a call.
This is the API, running client-side. Drop a file, switch endpoints, and copy a request that returns the exact JSON you see. Your file is not uploaded; it stays in the browser.
This playground runs the identical engine client-side, so the response below is exactly what the hosted API returns. Your file never leaves the browser. The hosted API is live with self-serve keys at audiolab.tools/v1/*.
curl https://audiolab.tools/v1/mixlab/analyze \
-H "Authorization: Bearer $AUDIOLAB_KEY" \
-F file=@track.wavLoudness, dynamics, spectrum
Every endpoint, from the request to the answer field by field, with the status and the margin of each number, the errors it can give, and a box to try it with your own key. Generated from the OpenAPI contract, so it cannot drift from what the API sends. To try without uploading anything, use the playground above: it runs the engine in your browser.
POST /v1/mixlab/analyze Analyze loudness, dynamics, stereo, and spectrum 37 fields
Submit an audio file and get back the same JSON the in-browser MixLab analyzer renders. Every analysis returns inline; there is no async path. Files too long to decode are refused with 413 rather than queued.
Request
Send a public url (up to 150 MB), a small file as the multipart field file or as the raw body with an audio content type
(about 4.5 MB), or an objectPath after a signed upload. A file you send is uploaded to the API: see /privacy for what is kept.
| JSON field | Type | Notes |
|---|---|---|
| url | string | Public http(s) URL to the audio file (alternative to multipart upload). |
| objectPath | string | Path returned by /api/upload-url, for audio that has no public URL. Readable only by the key that uploaded it, and kept for about an hour so a follow-up question about the same file does not need a re-upload. |
| series | boolean | Send the curves as well: shortTermLufs, waveform and spectrum. Off by default since 22 September 2026, because they are ~98% of the payload and no answer needs them. Raw or multipart callers use ?series=true. |
| round | boolean | Round the numbers to what the measurement is worth: dB and LUFS to 2 decimals, seconds to 3, shares to 3, Hz to 1. Same keys, same types, only fewer decimals. Off by default: without it you get the full precision, exactly as before. Supported on mixlab/analyze, voicelab/qa, signallab and analyze/profile. |
| measureStatus | boolean | Add one extra key, measureStatus, with the status (validated, proxy, beta, withdrawn, planned), the basis, how it works (dsp, model, mixed) and, where known, the margin of every measure in the answer. Off by default: without it the answer has no extra key. Supported on mixlab/analyze, voicelab/qa, signallab and analyze/profile. |
Example
curl https://audiolab.tools/v1/mixlab/analyze \
-H "Authorization: Bearer $AUDIOLAB_KEY" \
-H "Content-Type: application/json" \
-d '{"url": "https://audiolab.tools/reference/is-original.mp3"}'import os
import requests
r = requests.post(
"https://audiolab.tools/v1/mixlab/analyze",
headers={"Authorization": f"Bearer {os.environ['AUDIOLAB_KEY']}"},
json={"url": "https://audiolab.tools/reference/is-original.mp3"},
timeout=300,
)
if not r.ok:
# error.code says what happened: https://audiolab.tools/docs/errors
raise RuntimeError(r.json()["error"]["code"])
print(r.json())const res = await fetch("https://audiolab.tools/v1/mixlab/analyze", {
method: "POST",
headers: { Authorization: `Bearer ${process.env.AUDIOLAB_KEY}`, "Content-Type": "application/json" },
body: JSON.stringify({ url: "https://audiolab.tools/reference/is-original.mp3" }),
});
const body = await res.json();
// On an error, body.error.code says what happened: https://audiolab.tools/docs/errors
if (!res.ok) throw new Error(body.error.code);
console.log(body); Response
MixLab analysis (mirrors the engine AnalysisResult type). The word next to a number says how far to trust it
(the five statuses); a margin says how far it can be off.
Add "measureStatus": true to the request to get the same status and margin inside the answer.
| Field | Type | Status | Notes |
|---|---|---|---|
| measureStatus | MeasureStatus | ||
| durationSec* | number | Validated | |
| sampleRate* | integer | Validated | |
| channels* | integer | Validated | |
| integratedLufs* | number | Validated | Integrated loudness (LUFS), BS.1770-4. · ±0.1 LU |
| reportUrl | string | Shareable numbers-only report page for this analysis (https://audiolab.tools/r/<id>). Present on every metered analysis response. | |
| shortTermLufsMax | number | Validated | |
| loudnessRange | number | Validated | LRA (LU). · ±1 LU |
| truePeakDb* | number | Validated | True peak (dBTP). · +0.2 / -0.4 dB |
| peakDb | number | Validated | |
| rmsDb | number | Validated | |
| crestFactorDb | number | Validated | |
| stereoWidth | number | Validated | |
| stereoCorrelation | number | Validated | |
| stereoBalanceDb | number | Validated | RMS left minus right, dB. Positive is left louder. 0 on mono. |
| stereoLayout | string | Validated | What kind of stereo file this is. dual-mono is two identical channels; one-sided is a dead channel. · mono | dual-mono | one-sided | stereo | silent |
| bassCorrelation | number | Validated | Correlation below 120 Hz after a 4th-order low-pass. Null on mono or without energy there. Below 0 the low end cancels in mono. |
| momentaryLufsMax | number | Validated | Loudest 400 ms window (EBU Tech 3341 M), LUFS. Ungated. |
| plrDb | number | Validated | Peak to loudness ratio: true peak minus integrated, LU. |
| psrDb | number | Validated | Peak to short-term ratio: true peak minus the loudest 3 s window, LU. |
| platformNormalisation | object[] | Validated | Per platform: the gain it applies, the true peak after it, and whether that clears the ceiling. Read the peak AFTER the gain. |
| monoCompatible | boolean | Proxy | |
| spectralCentroidHz | number | Validated | |
| spectralRolloffHz | number | Validated | |
| spectralFlatness | number | Validated | |
| bandEnergy | Band | Validated | |
| harshness.score | number | ||
| harshness.label | string | Low | Medium | High | |
| harshness.band | string | ||
| muddiness.score | number | ||
| muddiness.label | string | Low | Medium | High | |
| spectrum.freqs | number[] | ||
| spectrum.mags | number[] | ||
| waveform | number[] | Validated | Downsampled waveform peaks. |
| shortTermLufs | number[] | Validated | Short-term loudness over time, at most 2000 points. Longer takes keep the PEAK per bucket, so a loud moment survives; seriesIntervalSec says how much time one point covers. Use /v1/mixlab/timeseries to choose the resolution yourself. |
| seriesIntervalSec | number | Seconds covered by one point of shortTermLufs. | |
| fileName | string |
Errors
- 400 Malformed request, a URL we could not use, or a parameter that is not valid. url_not_allowed, url_fetch_failed, invalid_json, missing_input, invalid_parameter, batch_too_large, upload_not_found
- 401 Invalid or revoked API key. /v1/batch and /v1/usage always need a key; the analysis routes also run without one, as a small keyless trial. invalid_api_key
- 402 A limit of your plan was reached: the calls or the data of the month, or a preset that needs a paid plan. The body carries usage where it applies. quota_exceeded, data_quota_exceeded, profile_requires_paid_plan, voice_minutes_exhausted
- 413 File too large for the route, or a recording too long to decode. file_too_large, track_too_long
- 422 File could not be decoded as audio. unsupported_format, room_not_measurable
- 429 Too many requests this minute. See Retry-After. rate_limited
- 500 Something went wrong on our side. The details are logged and never sent to you. storage_unavailable, analysis_error
Every error carries a stable code and a docs_url: the cause, the fix and an example for each.
Try it with your key
This sends your URL or file to the EU-hosted API, so the audio leaves your device and counts as a call. The API keeps no audio: a direct request is decoded and discarded inside the request, and only numbers come back. Details on /privacy. To measure without uploading, use the playground above.
POST /v1/voicelab/qa Analyze speech quality 55 fields
Request
Send a public url (up to 150 MB), a small file as the multipart field file or as the raw body with an audio content type
(about 4.5 MB), or an objectPath after a signed upload. A file you send is uploaded to the API: see /privacy for what is kept.
| JSON field | Type | Notes |
|---|---|---|
| url | string | Public http(s) URL to the audio file (alternative to multipart upload). |
| objectPath | string | Path returned by /api/upload-url, for audio that has no public URL. Readable only by the key that uploaded it, and kept for about an hour so a follow-up question about the same file does not need a re-upload. |
| series | boolean | Send the curves as well: rmsEnvelope, voicedMask, segments and segmentalSnrPerSegmentDb. Off by default since 22 September 2026, because they are ~98% of the payload and no answer needs them. Raw or multipart callers use ?series=true. |
| round | boolean | Round the numbers to what the measurement is worth: dB and LUFS to 2 decimals, seconds to 3, shares to 3, Hz to 1. Same keys, same types, only fewer decimals. Off by default: without it you get the full precision, exactly as before. Supported on mixlab/analyze, voicelab/qa, signallab and analyze/profile. |
| measureStatus | boolean | Add one extra key, measureStatus, with the status (validated, proxy, beta, withdrawn, planned), the basis, how it works (dsp, model, mixed) and, where known, the margin of every measure in the answer. Off by default: without it the answer has no extra key. Supported on mixlab/analyze, voicelab/qa, signallab and analyze/profile. |
| speechDetector | string | Which speech detector finds the speech in the recording; it decides speechRatio, speechSec, silenceSec, longSilences, segments and the measures that read the pauses. v1 is the first detector and is the default until 15 January 2027; k2 lets a phrase run on into its soft word starts and endings (by at most 100 ms per side) and was better on new speakers in the tests (frame F1 from 0.88 to 0.94 on clean audio and noise at 20 and 10 dB; see the status of voice.speechRatio at https://audiolab.tools/lenses). The same keys and types in both, only the values differ: with k2 the speech ratio reads about 5 to 14 points higher, everything that reads the speech frames or the pauses moves with it, signalToNoiseDb is corrected below 16 dB and reads about 4 to 5 dB lower at 10 dB noise and below (closer to the true ratio), and a pause measure is null when no pause frame is left; brightness, sibilanceRisk, noiseFloorDb and peakDb do not move (details: https://audiolab.tools/docs/start/changes-and-deprecation). From 15 January 2027 the default becomes k2 and v1 stays selectable until at least 15 April 2027. Supported on voicelab/qa, voicelab/segments and analyze/profile. · v1 | k2 |
Example
curl https://audiolab.tools/v1/voicelab/qa \
-H "Authorization: Bearer $AUDIOLAB_KEY" \
-H "Content-Type: application/json" \
-d '{"url": "https://audiolab.tools/samples/voice/speech-female-plain.mp3"}'import os
import requests
r = requests.post(
"https://audiolab.tools/v1/voicelab/qa",
headers={"Authorization": f"Bearer {os.environ['AUDIOLAB_KEY']}"},
json={"url": "https://audiolab.tools/samples/voice/speech-female-plain.mp3"},
timeout=300,
)
if not r.ok:
# error.code says what happened: https://audiolab.tools/docs/errors
raise RuntimeError(r.json()["error"]["code"])
print(r.json())const res = await fetch("https://audiolab.tools/v1/voicelab/qa", {
method: "POST",
headers: { Authorization: `Bearer ${process.env.AUDIOLAB_KEY}`, "Content-Type": "application/json" },
body: JSON.stringify({ url: "https://audiolab.tools/samples/voice/speech-female-plain.mp3" }),
});
const body = await res.json();
// On an error, body.error.code says what happened: https://audiolab.tools/docs/errors
if (!res.ok) throw new Error(body.error.code);
console.log(body); Response
VoiceLab speech-quality analysis (mirrors the engine VoiceAnalysis type). The word next to a number says how far to trust it
(the five statuses); a margin says how far it can be off.
Add "measureStatus": true to the request to get the same status and margin inside the answer.
| Field | Type | Status | Notes |
|---|---|---|---|
| measureStatus | MeasureStatus | ||
| durationSec* | number | Validated | |
| sampleRate* | integer | Validated | |
| speechSec | number | Proxy | |
| silenceSec | number | Proxy | |
| speechRatio* | number | Proxy | |
| segments | VoiceSegment[] | Proxy | |
| estimatedSyllableRate | number | Proxy | Speech bursts per second, counted from deep dips in the envelope. Not a syllable count. |
| speakingRateLabel | string | Proxy | Slow | Normal | Fast | Variable |
| longSilences | number | Proxy | |
| peakDb | number | Validated | |
| averageSpeechRms | number | ||
| averageSpeechRmsDb | number | Validated | |
| noiseFloorDb | number | Validated | |
| signalToNoiseDb | number | Validated | ±6 dB |
| clipping.count | integer | ||
| clipping.severity | string | None | Low | Medium | High | |
| brightness | number | Proxy | Mean spectral centre of every non-silent frame (pauses with noise included) over 2500 Hz, capped at 1. |
| activeKind | string | Proxy | What the active material looks like, from the frame labels shared with the SignalLab timeline: voice, music or noise. Voice QA assumes speech input, so read every voice figure as a figure about the material rather than about a voice as soon as this is not "voice". Null when nothing rises above the silence gate. · voice | music | noise | mixed |
| activeVoiceShare | number | Proxy | Share of active frames labelled voice, 0 to 1. Music with vocals genuinely sits in the middle. Null when nothing is active. |
| roomEchoScore | number | Withdrawn | Energy left in the pauses after speech: the 60 to 420 ms tail after each phrase against the phrase itself. Noise OR reverb, and this number does not separate the two, so do not read it as a room verdict. Tested on 27 Sep 2026 against 24 measured room responses: it tracked the measured RT60 at Spearman 0.06 and a 1.71 s room scored below a 0.18 s booth, while it does rise with background noise. The field name is kept for compatibility with saved history and existing integrations. Null when no speech was found to read it from. |
| sibilanceRisk | string | Proxy | Low | Medium | High |
| segmentalSnrDb | number | Validated | SNR measured per phrase and averaged, dB. |
| segmentalSnrSpreadDb | number | Validated | Spread of the per-phrase SNR, dB. |
| segmentalSnrPerSegmentDb | number[] | Validated | |
| gapModulationRatio | number | Proxy | 2 to 8 Hz share of the gap-envelope energy: speech rhythm where the target is silent. |
| gapModulationHighRatio | number | Proxy | Share of the movement in the pauses between 8 and 16 Hz, from pauses of at least 1.4 s. Flat noise already reads about 0.5. |
| gapPeriodicity | number | Proxy | Autocorrelation peak of the gap envelope at syllable lags. |
| gapLevelDb | number | Proxy | Mean level of the gaps, dBFS. |
| humLevelDb | number | Validated | Mains hum in the pauses, dB above the 30 to 600 Hz floor. Null when no 0.34 s window fits inside a pause. |
| humMainsHz | integer | Validated | 50, 60, or null when the hum stays under 6 dB or was not measured. · 50 | 60 |
| startsInSpeech | boolean | Validated | The take opens mid-sound. |
| endsInSpeech | boolean | Validated | The take is cut off at the end. |
| edgeStartDb | number | Validated | Envelope level at the first frame, dBFS. |
| edgeEndDb | number | Validated | Envelope level at the last frame, dBFS. |
| sceneBackground | string | Proxy | What fills the speech pauses, read after the first 0.4 s of each: quiet, stationary (a machine), program (a second voice or music), unsteady (moves without cadence or pitch). Null under one second of pause. · quiet | stationary | program | unsteady |
| sceneProgramScore | number | Proxy | 0 to 1: how strongly the pauses carry a program, from level movement times speech cadence or a moving pitch. Above 0.5 the label reads program. |
| sceneStationaryScore | number | Proxy | 0 to 1: how still the pause floor stands in level and in spectral shape. Above 0.5 the label reads stationary. |
| sceneGapLowBandShare | number | Proxy | Share of the pause power below 500 Hz. |
| sceneGapSpeechBandShare | number | Proxy | Share of the pause power between 300 and 3400 Hz. |
| sceneGapStabilityDb | number | Proxy | Standard deviation, dB, of the quieter half of the pause frames (20 ms each). |
| sceneGapHarmonicShare | number | Proxy | Share of 40 ms pause frames with a clear, moving pitch between 70 and 400 Hz: a second voice or music. A fixed pitch is reported as sceneGapToneHz instead. |
| sceneGapToneHz | number | Proxy | One steady tone filling the pauses (a fan, a transformer), Hz, or null. |
| sceneGapSpectralFlux | number | Proxy | 0 to 1: how much the spectral shape of the pause floor changes between 0.4 s windows. Under 0.06 a machine, above 0.2 music or speech. |
| speechToBackgroundDb | number | Proxy | Mean speech level minus the mean pause floor level, dB. |
| sceneIntelligibility | string | Proxy | A proxy, not an STI: the weaker of the worst-tenth per-phrase SNR and speech over background (3 dB reads 0, 18 dB reads 1), reduced for clipping. Null under two measurable phrases. · clear | borderline | buried |
| sceneIntelligibilityScore | number | Proxy | The intelligibility proxy as a number, 0 to 1. |
| worstPhraseSnrDb | number | Validated | Tenth percentile of the per-phrase SNR, dB: what the weakest phrases measure against the noise beside them. |
| rmsEnvelope | number[] | Validated | Level envelope, at most 2000 points. Longer takes are reduced by keeping the PEAK per bucket, so a short clip or a hot moment survives; seriesIntervalSec says how much time one point covers. |
| voicedMask | boolean[] | Proxy | Speech present per envelope point, same length and interval as rmsEnvelope. A bucket reads true when any of its points did. |
| seriesIntervalSec | number | Seconds covered by one point of rmsEnvelope and voicedMask, so the series still maps onto the timeline after reduction. | |
| totalSegments | integer | Speech segments the engine found, before any cap. | |
| returnedSegments | integer | Segments in this response. Lower than totalSegments when truncated. | |
| truncated | boolean | True when more than 500 segments were found and only the longest 500 are returned, with segmentalSnrPerSegmentDb kept in step. | |
| fileName | string |
Errors
- 400 Malformed request, a URL we could not use, or a parameter that is not valid. url_not_allowed, url_fetch_failed, invalid_json, missing_input, invalid_parameter, batch_too_large, upload_not_found
- 401 Invalid or revoked API key. /v1/batch and /v1/usage always need a key; the analysis routes also run without one, as a small keyless trial. invalid_api_key
- 402 A limit of your plan was reached: the calls or the data of the month, or a preset that needs a paid plan. The body carries usage where it applies. quota_exceeded, data_quota_exceeded, profile_requires_paid_plan, voice_minutes_exhausted
- 413 File too large for the route, or a recording too long to decode. file_too_large, track_too_long
- 422 File could not be decoded as audio. unsupported_format, room_not_measurable
- 429 Too many requests this minute. See Retry-After. rate_limited
- 500 Something went wrong on our side. The details are logged and never sent to you. storage_unavailable, analysis_error
Every error carries a stable code and a docs_url: the cause, the fix and an example for each.
Try it with your key
This sends your URL or file to the EU-hosted API, so the audio leaves your device and counts as a call. The API keeps no audio: a direct request is decoded and discarded inside the request, and only numbers come back. Details on /privacy. To measure without uploading, use the playground above.
POST /v1/analyze/profile Measure against a named profile and get back only the lenses that question needs
One preset answers one question, keyed by stable lens id. Profiles: basic, voice, master, provenance, dataset, environment, broadcast, loop, room, chain, pitch. Only basic is free, the rest return 402. series:true adds curves and can reach ~2 MB: for your own code, not for a chat client.
Request
Send a public url (up to 150 MB), a small file as the multipart field file or as the raw body with an audio content type
(about 4.5 MB), or an objectPath after a signed upload. A file you send is uploaded to the API: see /privacy for what is kept.
| JSON field | Type | Notes |
|---|---|---|
| url | string | Public http(s) URL to the audio file. |
| objectPath | string | Path returned by /api/upload-url, for audio that has no public URL. Readable only by the key that uploaded it, and kept for about an hour so a follow-up question about the same file does not need a re-upload. |
| profile* | string | The preset. room: the reverberation time per octave band from one clap, balloon pop, impulse response or sweep (see roomSource); a recording that holds none of them is a 422 with the code room_not_measurable. chain: the gaps of digital silence inside the sound (lost packets). pitch: the pitch of a voice, how voiced the recording is and the harmonics-to-noise ratio. A measure that has not met its test yet (a beta that is not offered) is left out of the answer; the status of every measure is at https://audiolab.tools/lenses. · basic | voice | master | provenance | dataset | environment | broadcast | loop | room | chain | pitch |
| series | boolean | Include per-block lanes and curves alongside the single values. |
| round | boolean | Round the numbers to what the measurement is worth: dB and LUFS to 2 decimals, seconds to 3, shares to 3, Hz to 1. Same keys, same types, only fewer decimals. Off by default: without it you get the full precision, exactly as before. Supported on mixlab/analyze, voicelab/qa, signallab and analyze/profile. |
| measureStatus | boolean | Add one extra key, measureStatus, with the status (validated, proxy, beta, withdrawn, planned), the basis, how it works (dsp, model, mixed) and, where known, the margin of every measure in the answer. Off by default: without it the answer has no extra key. Supported on mixlab/analyze, voicelab/qa, signallab and analyze/profile. |
| contentModel | string | Which model gives the content type (voice, music, mixed, noise, unknown) and its confidence. v1 is the first model and is the default until 15 January 2027; v2 is the new one, much better on speech and music in the tests (see the status of signal.contentType at https://audiolab.tools/lenses). The same keys and types in both, only the values differ, and with v2 the evidence list contains model:v2. From 15 January 2027 the default becomes v2 and v1 stays selectable until at least 15 April 2027. Supported on signallab and analyze/profile. · v1 | v2 |
| roomSource | string | For the room profile: how the recording was made. clap: one clap (the default; the bands from 500 Hz up); balloon: one balloon pop (the same bands); impulse: the file already is an impulse response (every octave band from 125 Hz); sweep: the file holds the exponential sweep of the engine as the room returned it (every octave band from 125 Hz). Supported on analyze/profile with the room profile. · clap | balloon | impulse | sweep |
| speechDetector | string | Which speech detector finds the speech in the recording; it decides speechRatio, speechSec, silenceSec, longSilences, segments and the measures that read the pauses. v1 is the first detector and is the default until 15 January 2027; k2 lets a phrase run on into its soft word starts and endings (by at most 100 ms per side) and was better on new speakers in the tests (frame F1 from 0.88 to 0.94 on clean audio and noise at 20 and 10 dB; see the status of voice.speechRatio at https://audiolab.tools/lenses). The same keys and types in both, only the values differ: with k2 the speech ratio reads about 5 to 14 points higher, everything that reads the speech frames or the pauses moves with it, signalToNoiseDb is corrected below 16 dB and reads about 4 to 5 dB lower at 10 dB noise and below (closer to the true ratio), and a pause measure is null when no pause frame is left; brightness, sibilanceRisk, noiseFloorDb and peakDb do not move (details: https://audiolab.tools/docs/start/changes-and-deprecation). From 15 January 2027 the default becomes k2 and v1 stays selectable until at least 15 April 2027. Supported on voicelab/qa, voicelab/segments and analyze/profile. · v1 | k2 |
Example
curl https://audiolab.tools/v1/analyze/profile \
-H "Authorization: Bearer $AUDIOLAB_KEY" \
-H "Content-Type: application/json" \
-d '{"url": "https://audiolab.tools/reference/is-original.mp3", "profile": "basic"}'import os
import requests
r = requests.post(
"https://audiolab.tools/v1/analyze/profile",
headers={"Authorization": f"Bearer {os.environ['AUDIOLAB_KEY']}"},
json={"url": "https://audiolab.tools/reference/is-original.mp3", "profile": "basic"},
timeout=300,
)
if not r.ok:
# error.code says what happened: https://audiolab.tools/docs/errors
raise RuntimeError(r.json()["error"]["code"])
print(r.json())const res = await fetch("https://audiolab.tools/v1/analyze/profile", {
method: "POST",
headers: { Authorization: `Bearer ${process.env.AUDIOLAB_KEY}`, "Content-Type": "application/json" },
body: JSON.stringify({ url: "https://audiolab.tools/reference/is-original.mp3", profile: "basic" }),
});
const body = await res.json();
// On an error, body.error.code says what happened: https://audiolab.tools/docs/errors
if (!res.ok) throw new Error(body.error.code);
console.log(body); Response
The answer is keyed by lens id and holds only the lenses the profile needs, each with its value, so there is no fixed field list. Every lens, its status and its margin is in the catalogue; the profile basic is free.
Errors
- 400 Malformed request, a URL we could not use, or a parameter that is not valid. url_not_allowed, url_fetch_failed, invalid_json, missing_input, invalid_parameter, batch_too_large, upload_not_found
- 401 Invalid or revoked API key. /v1/batch and /v1/usage always need a key; the analysis routes also run without one, as a small keyless trial. invalid_api_key
- 402 A limit of your plan was reached: the calls or the data of the month, or a preset that needs a paid plan. The body carries usage where it applies. quota_exceeded, data_quota_exceeded, profile_requires_paid_plan, voice_minutes_exhausted
- 413 File too large for the route, or a recording too long to decode. file_too_large, track_too_long
- 422 File could not be decoded as audio. unsupported_format, room_not_measurable
- 429 Too many requests this minute. See Retry-After. rate_limited
- 500 Something went wrong on our side. The details are logged and never sent to you. storage_unavailable, analysis_error
Every error carries a stable code and a docs_url: the cause, the fix and an example for each.
Try it with your key
This sends your URL or file to the EU-hosted API, so the audio leaves your device and counts as a call. The API keeps no audio: a direct request is decoded and discarded inside the request, and only numbers come back. Details on /privacy. To measure without uploading, use the playground above.
POST /v1/signallab Index an audio file (tags, content type, clipping/silence, metadata) 46 fields
Note: use /v1/signallab, not /v1/signallab/index. The hosting layer strips a trailing /index from any path, so the older form redirects and then 404s for clients that do not follow redirects.
Request
Send a public url (up to 150 MB), a small file as the multipart field file or as the raw body with an audio content type
(about 4.5 MB), or an objectPath after a signed upload. A file you send is uploaded to the API: see /privacy for what is kept.
| JSON field | Type | Notes |
|---|---|---|
| url | string | Public http(s) URL to the audio file (alternative to multipart upload). |
| objectPath | string | Path returned by /api/upload-url, for audio that has no public URL. Readable only by the key that uploaded it, and kept for about an hour so a follow-up question about the same file does not need a re-upload. |
| series | boolean | Send the curves as well: regions, energyEnvelope and centroidEnvelope. Off by default since 22 September 2026, because they are ~98% of the payload and no answer needs them. Raw or multipart callers use ?series=true. |
| round | boolean | Round the numbers to what the measurement is worth: dB and LUFS to 2 decimals, seconds to 3, shares to 3, Hz to 1. Same keys, same types, only fewer decimals. Off by default: without it you get the full precision, exactly as before. Supported on mixlab/analyze, voicelab/qa, signallab and analyze/profile. |
| measureStatus | boolean | Add one extra key, measureStatus, with the status (validated, proxy, beta, withdrawn, planned), the basis, how it works (dsp, model, mixed) and, where known, the margin of every measure in the answer. Off by default: without it the answer has no extra key. Supported on mixlab/analyze, voicelab/qa, signallab and analyze/profile. |
| contentModel | string | Which model gives the content type (voice, music, mixed, noise, unknown) and its confidence. v1 is the first model and is the default until 15 January 2027; v2 is the new one, much better on speech and music in the tests (see the status of signal.contentType at https://audiolab.tools/lenses). The same keys and types in both, only the values differ, and with v2 the evidence list contains model:v2. From 15 January 2027 the default becomes v2 and v1 stays selectable until at least 15 April 2027. Supported on signallab and analyze/profile. · v1 | v2 |
Example
curl https://audiolab.tools/v1/signallab \
-H "Authorization: Bearer $AUDIOLAB_KEY" \
-H "Content-Type: application/json" \
-d '{"url": "https://audiolab.tools/reference/is-original.mp3"}'import os
import requests
r = requests.post(
"https://audiolab.tools/v1/signallab",
headers={"Authorization": f"Bearer {os.environ['AUDIOLAB_KEY']}"},
json={"url": "https://audiolab.tools/reference/is-original.mp3"},
timeout=300,
)
if not r.ok:
# error.code says what happened: https://audiolab.tools/docs/errors
raise RuntimeError(r.json()["error"]["code"])
print(r.json())const res = await fetch("https://audiolab.tools/v1/signallab", {
method: "POST",
headers: { Authorization: `Bearer ${process.env.AUDIOLAB_KEY}`, "Content-Type": "application/json" },
body: JSON.stringify({ url: "https://audiolab.tools/reference/is-original.mp3" }),
});
const body = await res.json();
// On an error, body.error.code says what happened: https://audiolab.tools/docs/errors
if (!res.ok) throw new Error(body.error.code);
console.log(body); Response
SignalLab indexer output (mirrors the engine SignalIndex type). The word next to a number says how far to trust it
(the five statuses); a margin says how far it can be off.
Add "measureStatus": true to the request to get the same status and margin inside the answer.
| Field | Type | Status | Notes |
|---|---|---|---|
| measureStatus | MeasureStatus | ||
| fileName* | string | ||
| fileSizeKb | number | Validated | |
| format | string | Validated | |
| durationSec* | number | Validated | |
| sampleRate* | integer | Validated | |
| channels* | integer | Validated | |
| bitDepthGuess | string | Proxy | A guess from the file type (16/24-bit guess for WAV, lossy for compressed formats). Not read from the samples. |
| peakDb | number | ||
| rmsDb | number | ||
| noiseFloorDb | number | ||
| dcOffset | number | Validated | |
| hasClipping | boolean | Validated | |
| clippingRegions | integer | Validated | |
| silenceRegions | integer | Validated | |
| silenceTotalSec | number | Validated | |
| contentTypeGuess* | string | Proxy | voice | music | mixed | noise | silence | unknown |
| contentConfidence | number | Proxy | |
| contentEvidence | string[] | Proxy | The measurements that back the content type, as tags. |
| beatStability | number | Proxy | Deprecated alias of tempo confidence; one release, then gone. |
| spectralFlatness | number | Validated | Flatness of the opening 10 s, 0 tonal to 1 noise-like. |
| loopSeamRatio | number | Validated | Wrap-around jump divided by the mean sample-to-sample step. Under 3 inaudible, above 10 a click. Null on files too short to judge. |
| edgeSilenceStartMs | number | Validated | Milliseconds under -60 dBFS at the start. |
| edgeSilenceEndMs | number | Validated | Milliseconds under -60 dBFS at the end. |
| inputSha256 | string | Validated | SHA-256 of the submitted bytes, when supplied. |
| bandwidthHz | number | Validated | Median per-block bandwidth over the non-silent blocks, Hz. |
| lossySource | string | Validated | The wall a lossy encoder leaves, or unknown where two explanations collide (a 64 kbps wall and a sharp resample read the same). · lossless | lossy ~64k | lossy ~128k | lossy ~256k | unknown |
| effectiveBitDepth | integer | Validated | Bits actually in use: 16 in a 24-bit container reads 16. Null under a thousand non-silent samples. |
| upsampledFromHz | number | Validated | Likely original sample rate behind a resampler-shaped roll-off, or null. |
| bandwidthSplices | integer | Validated | Bandwidth jumps of more than 3 kHz that hold for at least two seconds. |
| toneProminenceDb | number | Validated | Most prominent tone against the masking noise in its critical band, dB (ISO 1996-2 annex C). Null on speech and under 0.17 s. |
| toneFrequencyHz | number | Validated | |
| toneLabel | string | Validated | Null where no tone was measured; see toneProminenceDb. · none | audible | prominent |
| thirdOctaveDb.centres | number[] | ||
| thirdOctaveDb.db | number[] | ||
| brightnessBucket | string | Proxy | Dark | Balanced | Bright | Very bright |
| dynamicsBucket | string | Proxy | Flat | Compressed | Modern | Dynamic |
| dominantBand | string | Proxy | A rough band label from the zero-crossing rate. Not the band with the most energy. |
| monophonicProbability | number | Validated | Left-right correlation, never below 0. Near 1 means mono in disguise. Not a probability. |
| regions | TimelineRegion[] | Proxy | |
| totalRegions | integer | Regions the engine found, before any cap. | |
| returnedRegions | integer | Regions in this response. Lower than totalRegions when truncated. | |
| truncated | boolean | True when more than 500 regions were found and only the longest 500 are returned. | |
| energyEnvelope | number[] | Validated | |
| centroidEnvelope | number[] | Validated | Zero-crossing rate per frame, a rough brightness trace. Not a spectral centre in Hz. |
| tags | string[] | Proxy |
Errors
- 400 Malformed request, a URL we could not use, or a parameter that is not valid. url_not_allowed, url_fetch_failed, invalid_json, missing_input, invalid_parameter, batch_too_large, upload_not_found
- 401 Invalid or revoked API key. /v1/batch and /v1/usage always need a key; the analysis routes also run without one, as a small keyless trial. invalid_api_key
- 402 A limit of your plan was reached: the calls or the data of the month, or a preset that needs a paid plan. The body carries usage where it applies. quota_exceeded, data_quota_exceeded, profile_requires_paid_plan, voice_minutes_exhausted
- 413 File too large for the route, or a recording too long to decode. file_too_large, track_too_long
- 422 File could not be decoded as audio. unsupported_format, room_not_measurable
- 429 Too many requests this minute. See Retry-After. rate_limited
- 500 Something went wrong on our side. The details are logged and never sent to you. storage_unavailable, analysis_error
Every error carries a stable code and a docs_url: the cause, the fix and an example for each.
Try it with your key
This sends your URL or file to the EU-hosted API, so the audio leaves your device and counts as a call. The API keeps no audio: a direct request is decoded and discarded inside the request, and only numbers come back. Details on /privacy. To measure without uploading, use the playground above.
POST /v1/signallab/sample Describe a single sound from a sample library (material, character, loop) 41 fields
One SOUND, not a recording: a single one-shot from a sample library, for sorting and searching a pack. Use /v1/signallab for a take or an episode. A single hit returns the character vector with loop null; material with several onsets returns tempo and loop QC with character null.
Request
Send a public url (up to 150 MB), a small file as the multipart field file or as the raw body with an audio content type
(about 4.5 MB), or an objectPath after a signed upload. A file you send is uploaded to the API: see /privacy for what is kept.
| JSON field | Type | Notes |
|---|---|---|
| url | string | Public http(s) URL to the audio file (alternative to multipart upload). |
| objectPath | string | Path returned by /api/upload-url, for audio that has no public URL. Readable only by the key that uploaded it, and kept for about an hour so a follow-up question about the same file does not need a re-upload. |
Example
curl https://audiolab.tools/v1/signallab/sample \
-H "Authorization: Bearer $AUDIOLAB_KEY" \
-H "Content-Type: application/json" \
-d '{"url": "https://audiolab.tools/reference/cal-1k-sine.mp3"}'import os
import requests
r = requests.post(
"https://audiolab.tools/v1/signallab/sample",
headers={"Authorization": f"Bearer {os.environ['AUDIOLAB_KEY']}"},
json={"url": "https://audiolab.tools/reference/cal-1k-sine.mp3"},
timeout=300,
)
if not r.ok:
# error.code says what happened: https://audiolab.tools/docs/errors
raise RuntimeError(r.json()["error"]["code"])
print(r.json())const res = await fetch("https://audiolab.tools/v1/signallab/sample", {
method: "POST",
headers: { Authorization: `Bearer ${process.env.AUDIOLAB_KEY}`, "Content-Type": "application/json" },
body: JSON.stringify({ url: "https://audiolab.tools/reference/cal-1k-sine.mp3" }),
});
const body = await res.json();
// On an error, body.error.code says what happened: https://audiolab.tools/docs/errors
if (!res.ok) throw new Error(body.error.code);
console.log(body); Response
One sound from a sample library. character and loop are mutually exclusive: a single hit fills character, material with several onsets fills loop, and the other is null. The word next to a number says how far to trust it
(the five statuses); a margin says how far it can be off.
Add "measureStatus": true to the request to get the same status and margin inside the answer.
| Field | Type | Status | Notes |
|---|---|---|---|
| material.structure | string | silence = no energy; continuous = one sustained event; single-event = one hit; phrase = several onsets. · silence | continuous | single-event | phrase | |
| material.durationClass | string | sample under 5 s, clip under 30 s, track under 15 min, long-form beyond that. · sample | clip | track | long-form | |
| material.evidence | string[] | Plain sentences behind the call, such as "4 onsets" or "96% of windows above -40 dB". | |
| material.onsetCount | integer | ||
| material.attackStrength | number | Peak-to-surroundings ratio of the strongest onset, 0 to 1. | |
| character.durationMs | number | ||
| character.attackMs | number | Time from onset to peak. | |
| character.decayT20Ms | number | Decay from a Schroeder curve, not from a short envelope: at low frequencies an envelope swings more within one period than the decay it is meant to measure. | |
| character.decayFitR2 | number | Fit quality of that decay line, 0 to 1. Low means the number is not worth much. | |
| character.decayRangeDb | number | ||
| character.decayTruncated | boolean | True when the file ends before the sound does. | |
| character.band.sub | number | ||
| character.band.bass | number | ||
| character.band.lowMid | number | ||
| character.band.mid | number | ||
| character.band.highMid | number | ||
| character.band.presence | number | ||
| character.band.air | number | ||
| character.bodyTilt | number | Spectral tilt of the body, dB per octave. | |
| character.bodyResonance | number | How peaked the body is: high means one resonance dominates. | |
| character.clickRatio | number | Share of the energy in the first few milliseconds. | |
| character.f0Hz | number | ||
| character.note | string | Nearest note name for f0Hz, such as F1. | |
| character.cents | integer | Deviation from that note, -50 to +50. | |
| character.pitchDefiniteness | number | MPM clarity: 1.00 on a pure tone, 0.07 on white noise. Below the gate the pitch fields are omitted with a reason. | |
| character.glideSt | number | Pitch change in semitones between two windows whose centres sit exactly 180 ms apart, so this is NOT the glide over the whole sound. A glide of 12 semitones spread over 1.5 s reads about 1.4 here. Sign follows direction: negative falls. | |
| character.crestDb | number | ||
| character.channelCorrelation | number | Null for mono input. | |
| character.omitted | object | Per field that could not be measured, the reason why. | |
| loop.durationSec | number | ||
| loop.bpm | number | ||
| loop.bars | integer | How many bars that tempo makes the file, at 4/4. | |
| loop.tempoStrength | number | Strength of the winning tempo peak. | |
| loop.candidateMargin | number | How far the winner sits above the runner-up. Near 0 means the tempo is a coin flip. | |
| loop.bpmRaw | number | Before snapping to the octave and the grid. | |
| loop.octaveCandidates | object[] | The same tempo at half and double time, since 120 and 60 fit the same file. | |
| loop.seamRatio | number | Level jump at the loop seam relative to the material. 0 is seamless; high means it clicks when it wraps. | |
| loop.edgeSilence.startMs | number | ||
| loop.edgeSilence.endMs | number | ||
| loop.omitted | object | ||
| omitted | object | Top-level reasons, for whole blocks that could not be measured. |
Errors
- 400 Malformed request, a URL we could not use, or a parameter that is not valid. url_not_allowed, url_fetch_failed, invalid_json, missing_input, invalid_parameter, batch_too_large, upload_not_found
- 401 Invalid or revoked API key. /v1/batch and /v1/usage always need a key; the analysis routes also run without one, as a small keyless trial. invalid_api_key
- 402 A limit of your plan was reached: the calls or the data of the month, or a preset that needs a paid plan. The body carries usage where it applies. quota_exceeded, data_quota_exceeded, profile_requires_paid_plan, voice_minutes_exhausted
- 413 File too large for the route, or a recording too long to decode. file_too_large, track_too_long
- 422 File could not be decoded as audio. unsupported_format, room_not_measurable
- 429 Too many requests this minute. See Retry-After. rate_limited
- 500 Something went wrong on our side. The details are logged and never sent to you. storage_unavailable, analysis_error
Every error carries a stable code and a docs_url: the cause, the fix and an example for each.
Try it with your key
This sends your URL or file to the EU-hosted API, so the audio leaves your device and counts as a call. The API keeps no audio: a direct request is decoded and discarded inside the request, and only numbers come back. Details on /privacy. To measure without uploading, use the playground above.
POST /v1/mixlab/check-target Check audio against a loudness target (Spotify, EBU, podcast, etc.) 18 fields
Run MixLab and verify against a delivery target. Returns pass/fail, per-metric deltas, issue messages, and an ffmpeg loudnorm command when it fails. Presets: spotify, apple-music, youtube, tidal, amazon-music, podcast, ebu-broadcast, atsc-broadcast. Use custom with lufs+tp.
Request
| JSON field | Type | Notes |
|---|---|---|
| url | string | Public http(s) URL to the audio file. |
| objectPath | string | Path returned by /api/upload-url, for audio that has no public URL. Readable only by the key that uploaded it, and kept for about an hour so a follow-up question about the same file does not need a re-upload. |
| target* | string | Preset name or "custom". |
| lufs | number | Custom target LUFS (when target="custom"). |
| tp | number | Custom target dBTP (when target="custom"). |
Example
curl https://audiolab.tools/v1/mixlab/check-target \
-H "Authorization: Bearer $AUDIOLAB_KEY" \
-H "Content-Type: application/json" \
-d '{"url": "https://audiolab.tools/reference/is-original.mp3", "target": "spotify"}'import os
import requests
r = requests.post(
"https://audiolab.tools/v1/mixlab/check-target",
headers={"Authorization": f"Bearer {os.environ['AUDIOLAB_KEY']}"},
json={"url": "https://audiolab.tools/reference/is-original.mp3", "target": "spotify"},
timeout=300,
)
if not r.ok:
# error.code says what happened: https://audiolab.tools/docs/errors
raise RuntimeError(r.json()["error"]["code"])
print(r.json())const res = await fetch("https://audiolab.tools/v1/mixlab/check-target", {
method: "POST",
headers: { Authorization: `Bearer ${process.env.AUDIOLAB_KEY}`, "Content-Type": "application/json" },
body: JSON.stringify({ url: "https://audiolab.tools/reference/is-original.mp3", target: "spotify" }),
});
const body = await res.json();
// On an error, body.error.code says what happened: https://audiolab.tools/docs/errors
if (!res.ok) throw new Error(body.error.code);
console.log(body); Response
Pass/fail verdict for a loudness target check, with concrete deltas and an ffmpeg loudnorm fix command. The word next to a number says how far to trust it
(the five statuses); a margin says how far it can be off.
Add "measureStatus": true to the request to get the same status and margin inside the answer.
| Field | Type | Status | Notes |
|---|---|---|---|
| pass* | boolean | True only if both LUFS within ±0.5 LU of target and true-peak ≤ target+0.5 dB. | |
| target.lufs | number | Target integrated loudness (LUFS). | |
| target.tp | number | Target maximum true-peak (dBTP). | |
| target.label | string | Human-readable target name (e.g. "Spotify"). | |
| target.source | string | published: the platform documents the figure. measured: taken from outside measurements, so it can shift. · published | measured | |
| target.sourceUrl | string | Where the platform publishes the figure. Present on published targets. | |
| target.verifiedOn | string | Date the published figure was last checked at sourceUrl. | |
| target.measurementCaveat | string | How the platform's own measurement differs from integrated loudness. Present only where it does (atsc-broadcast). | |
| target.bron | string | Deprecated Dutch alias of source; removed after 2026-10-31. · published | measured | |
| target.bewijs | string | Deprecated Dutch alias of sourceUrl; removed after 2026-10-31. | |
| target.gecontroleerd | string | Deprecated Dutch alias of verifiedOn; removed after 2026-10-31. | |
| target.meetVerschil | string | Deprecated Dutch alias of measurementCaveat; removed after 2026-10-31. | |
| measurements.integratedLufs | number | ||
| measurements.truePeakDb | number | ||
| measurements.lufsDelta | number | measured - target. | |
| measurements.tpDelta | number | measured - target. | |
| issues* | object[] | ||
| ffmpegFix | string | ffmpeg loudnorm one-liner to fix when failing; null when passing. |
Errors
- 400 Malformed request, a URL we could not use, or a parameter that is not valid. url_not_allowed, url_fetch_failed, invalid_json, missing_input, invalid_parameter, batch_too_large, upload_not_found
- 401 Invalid or revoked API key. /v1/batch and /v1/usage always need a key; the analysis routes also run without one, as a small keyless trial. invalid_api_key
- 402 A limit of your plan was reached: the calls or the data of the month, or a preset that needs a paid plan. The body carries usage where it applies. quota_exceeded, data_quota_exceeded, profile_requires_paid_plan, voice_minutes_exhausted
- 413 File too large for the route, or a recording too long to decode. file_too_large, track_too_long
- 429 Too many requests this minute. See Retry-After. rate_limited
- 500 Something went wrong on our side. The details are logged and never sent to you. storage_unavailable, analysis_error
Every error carries a stable code and a docs_url: the cause, the fix and an example for each.
Try it with your key
This sends your URL or file to the EU-hosted API, so the audio leaves your device and counts as a call. The API keeps no audio: a direct request is decoded and discarded inside the request, and only numbers come back. Details on /privacy. To measure without uploading, use the playground above.
POST /v1/mixlab/timeseries Loudness and waveform over time (for graphing) 8 fields
Returns short-term LUFS samples (~10 Hz, EBU R128 3s window) with their time-base, plus downsampled waveform peaks. For loudness charts, level meters, waveform displays.
Request
| JSON field | Type | Notes |
|---|---|---|
| url | string | |
| objectPath | string | Path returned by /api/upload-url, for audio that has no public URL. Readable only by the key that uploaded it, and kept for about an hour so a follow-up question about the same file does not need a re-upload. |
| waveformPoints | integer | Number of waveform peaks to return (downsampled). Default 200. |
Example
curl https://audiolab.tools/v1/mixlab/timeseries \
-H "Authorization: Bearer $AUDIOLAB_KEY" \
-H "Content-Type: application/json" \
-d '{"url": "https://audiolab.tools/reference/is-original.mp3"}'import os
import requests
r = requests.post(
"https://audiolab.tools/v1/mixlab/timeseries",
headers={"Authorization": f"Bearer {os.environ['AUDIOLAB_KEY']}"},
json={"url": "https://audiolab.tools/reference/is-original.mp3"},
timeout=300,
)
if not r.ok:
# error.code says what happened: https://audiolab.tools/docs/errors
raise RuntimeError(r.json()["error"]["code"])
print(r.json())const res = await fetch("https://audiolab.tools/v1/mixlab/timeseries", {
method: "POST",
headers: { Authorization: `Bearer ${process.env.AUDIOLAB_KEY}`, "Content-Type": "application/json" },
body: JSON.stringify({ url: "https://audiolab.tools/reference/is-original.mp3" }),
});
const body = await res.json();
// On an error, body.error.code says what happened: https://audiolab.tools/docs/errors
if (!res.ok) throw new Error(body.error.code);
console.log(body); Response
Loudness and waveform data over time, for graphing or visualization. The word next to a number says how far to trust it
(the five statuses); a margin says how far it can be off.
Add "measureStatus": true to the request to get the same status and margin inside the answer.
| Field | Type | Status | Notes |
|---|---|---|---|
| durationSec* | number | Validated | |
| sampleRate | integer | Validated | |
| loudnessOverTime* | SeriesArray | ||
| waveformOverTime* | SeriesArray | ||
| summary.integratedLufs | number | ||
| summary.shortTermLufsMax | number | ||
| summary.loudnessRange | number | ||
| summary.truePeakDb | number |
Errors
- 400 Malformed request, a URL we could not use, or a parameter that is not valid. url_not_allowed, url_fetch_failed, invalid_json, missing_input, invalid_parameter, batch_too_large, upload_not_found
- 401 Invalid or revoked API key. /v1/batch and /v1/usage always need a key; the analysis routes also run without one, as a small keyless trial. invalid_api_key
- 402 A limit of your plan was reached: the calls or the data of the month, or a preset that needs a paid plan. The body carries usage where it applies. quota_exceeded, data_quota_exceeded, profile_requires_paid_plan, voice_minutes_exhausted
- 413 File too large for the route, or a recording too long to decode. file_too_large, track_too_long
- 429 Too many requests this minute. See Retry-After. rate_limited
- 500 Something went wrong on our side. The details are logged and never sent to you. storage_unavailable, analysis_error
Every error carries a stable code and a docs_url: the cause, the fix and an example for each.
Try it with your key
This sends your URL or file to the EU-hosted API, so the audio leaves your device and counts as a call. The API keeps no audio: a direct request is decoded and discarded inside the request, and only numbers come back. Details on /privacy. To measure without uploading, use the playground above.
POST /v1/mixlab/spectrum FFT spectrum bins + band energies 8 fields
Returns paired frequency/magnitude arrays plus 7-band energies and spectral descriptors (centroid, rolloff, flatness). For spectrum-analyzer UIs and tonal-balance diagnosis beyond the harshness/muddiness labels.
Request
| JSON field | Type | Notes |
|---|---|---|
| url | string | |
| objectPath | string | Path returned by /api/upload-url, for audio that has no public URL. Readable only by the key that uploaded it, and kept for about an hour so a follow-up question about the same file does not need a re-upload. |
Example
curl https://audiolab.tools/v1/mixlab/spectrum \
-H "Authorization: Bearer $AUDIOLAB_KEY" \
-H "Content-Type: application/json" \
-d '{"url": "https://audiolab.tools/reference/is-original.mp3"}'import os
import requests
r = requests.post(
"https://audiolab.tools/v1/mixlab/spectrum",
headers={"Authorization": f"Bearer {os.environ['AUDIOLAB_KEY']}"},
json={"url": "https://audiolab.tools/reference/is-original.mp3"},
timeout=300,
)
if not r.ok:
# error.code says what happened: https://audiolab.tools/docs/errors
raise RuntimeError(r.json()["error"]["code"])
print(r.json())const res = await fetch("https://audiolab.tools/v1/mixlab/spectrum", {
method: "POST",
headers: { Authorization: `Bearer ${process.env.AUDIOLAB_KEY}`, "Content-Type": "application/json" },
body: JSON.stringify({ url: "https://audiolab.tools/reference/is-original.mp3" }),
});
const body = await res.json();
// On an error, body.error.code says what happened: https://audiolab.tools/docs/errors
if (!res.ok) throw new Error(body.error.code);
console.log(body); Response
FFT spectrum bins plus band energies and spectral descriptors. The word next to a number says how far to trust it
(the five statuses); a margin says how far it can be off.
Add "measureStatus": true to the request to get the same status and margin inside the answer.
| Field | Type | Status | Notes |
|---|---|---|---|
| sampleRate* | integer | Validated | |
| spectralCentroidHz | number | Validated | |
| spectralRolloffHz | number | Validated | |
| spectralFlatness | number | Validated | |
| bandEnergy* | Band | Validated | |
| spectrum.freqs | number[] | ||
| spectrum.mags | number[] | ||
| spectrum.count | integer |
Errors
- 400 Malformed request, a URL we could not use, or a parameter that is not valid. url_not_allowed, url_fetch_failed, invalid_json, missing_input, invalid_parameter, batch_too_large, upload_not_found
- 401 Invalid or revoked API key. /v1/batch and /v1/usage always need a key; the analysis routes also run without one, as a small keyless trial. invalid_api_key
- 402 A limit of your plan was reached: the calls or the data of the month, or a preset that needs a paid plan. The body carries usage where it applies. quota_exceeded, data_quota_exceeded, profile_requires_paid_plan, voice_minutes_exhausted
- 413 File too large for the route, or a recording too long to decode. file_too_large, track_too_long
- 429 Too many requests this minute. See Retry-After. rate_limited
- 500 Something went wrong on our side. The details are logged and never sent to you. storage_unavailable, analysis_error
Every error carries a stable code and a docs_url: the cause, the fix and an example for each.
Try it with your key
This sends your URL or file to the EU-hosted API, so the audio leaves your device and counts as a call. The API keeps no audio: a direct request is decoded and discarded inside the request, and only numbers come back. Details on /privacy. To measure without uploading, use the playground above.
POST /v1/voicelab/segments Voiced speech segments with timestamps 6 fields
List voiced speech regions with start/end timestamps and per-segment RMS level. For auto-trim and chapter generation. RMS-based voice-activity detection: when speech happens, not who is speaking, and not where the speaker changes.
Request
| JSON field | Type | Notes |
|---|---|---|
| url | string | |
| objectPath | string | Path returned by /api/upload-url, for audio that has no public URL. Readable only by the key that uploaded it, and kept for about an hour so a follow-up question about the same file does not need a re-upload. |
| speechDetector | string | Which speech detector finds the segments. v1 is the first detector and is the default until 15 January 2027; k2 lets a phrase run on into its soft word starts and endings (by at most 100 ms per side), so neighbouring segments sometimes merge into fewer, longer ones. The same keys and types in both, only the values differ. From 15 January 2027 the default becomes k2 and v1 stays selectable until at least 15 April 2027. Supported on voicelab/qa, voicelab/segments and analyze/profile. · v1 | k2 |
Example
curl https://audiolab.tools/v1/voicelab/segments \
-H "Authorization: Bearer $AUDIOLAB_KEY" \
-H "Content-Type: application/json" \
-d '{"url": "https://audiolab.tools/samples/voice/speech-female-plain.mp3"}'import os
import requests
r = requests.post(
"https://audiolab.tools/v1/voicelab/segments",
headers={"Authorization": f"Bearer {os.environ['AUDIOLAB_KEY']}"},
json={"url": "https://audiolab.tools/samples/voice/speech-female-plain.mp3"},
timeout=300,
)
if not r.ok:
# error.code says what happened: https://audiolab.tools/docs/errors
raise RuntimeError(r.json()["error"]["code"])
print(r.json())const res = await fetch("https://audiolab.tools/v1/voicelab/segments", {
method: "POST",
headers: { Authorization: `Bearer ${process.env.AUDIOLAB_KEY}`, "Content-Type": "application/json" },
body: JSON.stringify({ url: "https://audiolab.tools/samples/voice/speech-female-plain.mp3" }),
});
const body = await res.json();
// On an error, body.error.code says what happened: https://audiolab.tools/docs/errors
if (!res.ok) throw new Error(body.error.code);
console.log(body); Response
Voiced regions with timestamps. For auto-trim and chapter generation. It finds where speech is, not who is speaking or where the speaker changes. The word next to a number says how far to trust it
(the five statuses); a margin says how far it can be off.
Add "measureStatus": true to the request to get the same status and margin inside the answer.
| Field | Type | Status | Notes |
|---|---|---|---|
| durationSec* | number | Validated | |
| speechSec | number | Proxy | |
| silenceSec | number | Proxy | |
| speechRatio* | number | Proxy | 0..1 fraction of audio that is voiced. |
| longSilences | number | Proxy | Count of unusually long silences (e.g. potential dead air). |
| segments* | SpeechSegment[] | Proxy |
Errors
- 400 Malformed request, a URL we could not use, or a parameter that is not valid. url_not_allowed, url_fetch_failed, invalid_json, missing_input, invalid_parameter, batch_too_large, upload_not_found
- 401 Invalid or revoked API key. /v1/batch and /v1/usage always need a key; the analysis routes also run without one, as a small keyless trial. invalid_api_key
- 402 A limit of your plan was reached: the calls or the data of the month, or a preset that needs a paid plan. The body carries usage where it applies. quota_exceeded, data_quota_exceeded, profile_requires_paid_plan, voice_minutes_exhausted
- 413 File too large for the route, or a recording too long to decode. file_too_large, track_too_long
- 429 Too many requests this minute. See Retry-After. rate_limited
- 500 Something went wrong on our side. The details are logged and never sent to you. storage_unavailable, analysis_error
Every error carries a stable code and a docs_url: the cause, the fix and an example for each.
Try it with your key
This sends your URL or file to the EU-hosted API, so the audio leaves your device and counts as a call. The API keeps no audio: a direct request is decoded and discarded inside the request, and only numbers come back. Details on /privacy. To measure without uploading, use the playground above.
POST /v1/batch Run one analysis route over up to 20 URLs in a single call 17 fields
One route over up to 20 sources in a single call: same engine, same metering as the single routes. Each item counts as one call plus its bytes, and a rate or quota stop ends the loop and is reported on the item where it fired. Provide urls and/or objectPaths, max 20 combined.
Request
| JSON field | Type | Notes |
|---|---|---|
| urls | string[] | Public http(s) URLs to audio files. |
| objectPaths | string[] | Storage objects minted via /api/upload-url under this key's key/<keyHash>/ namespace, for local files with no public URL. Readable for about an hour, so a second question about the same file needs no re-upload, then swept. |
| endpoint | string | Which analysis route to run for every URL. · mixlab/analyze | mixlab/check-target | mixlab/timeseries | mixlab/spectrum | voicelab/qa | voicelab/segments | signallab | signallab/index |
| args | object | Optional route-specific extras (e.g. target, waveformPoints), applied to every URL. |
| name | string | Optional session name; stored on every item's event (meta.batch) so history can group the run. |
Example
curl https://audiolab.tools/v1/batch \
-H "Authorization: Bearer $AUDIOLAB_KEY" \
-H "Content-Type: application/json" \
-d '{"urls": ["https://audiolab.tools/reference/is-original.mp3", "https://audiolab.tools/reference/is-muddy.mp3"], "endpoint": "mixlab/analyze"}'import os
import requests
r = requests.post(
"https://audiolab.tools/v1/batch",
headers={"Authorization": f"Bearer {os.environ['AUDIOLAB_KEY']}"},
json={
"urls": [
"https://audiolab.tools/reference/is-original.mp3",
"https://audiolab.tools/reference/is-muddy.mp3",
],
"endpoint": "mixlab/analyze",
},
timeout=300,
)
if not r.ok:
# error.code says what happened: https://audiolab.tools/docs/errors
raise RuntimeError(r.json()["error"]["code"])
print(r.json())const res = await fetch("https://audiolab.tools/v1/batch", {
method: "POST",
headers: { Authorization: `Bearer ${process.env.AUDIOLAB_KEY}`, "Content-Type": "application/json" },
body: JSON.stringify({
urls: [
"https://audiolab.tools/reference/is-original.mp3",
"https://audiolab.tools/reference/is-muddy.mp3",
],
endpoint: "mixlab/analyze",
}),
});
const body = await res.json();
// On an error, body.error.code says what happened: https://audiolab.tools/docs/errors
if (!res.ok) throw new Error(body.error.code);
console.log(body); Response
The answer of /v1/batch. The word next to a number says how far to trust it
(the five statuses); a margin says how far it can be off.
Add "measureStatus": true to the request to get the same status and margin inside the answer.
| Field | Type | Status | Notes |
|---|---|---|---|
| endpoint* | string | The analysis route that ran. | |
| count* | integer | Processed items; shorter than the request when a rate or quota stop fired. | |
| batchId | string | Unique id for this run; every item's event carries it in meta.batch. | |
| name | string | The session name, when provided. | |
| summary.ok | integer | ||
| summary.failed | integer | ||
| summary.avgLufs | number | ||
| summary.loudest.ref | string | ||
| summary.loudest.lufs | number | ||
| summary.quietest.ref | string | ||
| summary.quietest.lufs | number | ||
| summary.overTruePeakCount | integer | ||
| summary.totalDurationSec | number | ||
| summary.passed | integer | check-target only. | |
| summary.passRate | number | check-target only, 0..1. | |
| summary.headline | string | One-line human/agent-readable overview. | |
| results* | object[] |
Errors
- 400 Malformed request, a URL we could not use, or a parameter that is not valid. url_not_allowed, url_fetch_failed, invalid_json, missing_input, invalid_parameter, batch_too_large, upload_not_found
- 401 Invalid or revoked API key. /v1/batch and /v1/usage always need a key; the analysis routes also run without one, as a small keyless trial. invalid_api_key
- 429 Too many requests this minute. See Retry-After. rate_limited
- 500 Something went wrong on our side. The details are logged and never sent to you. storage_unavailable, analysis_error
Every error carries a stable code and a docs_url: the cause, the fix and an example for each.
Try it with your key
This sends your URL or file to the EU-hosted API, so the audio leaves your device and counts as a call. The API keeps no audio: a direct request is decoded and discarded inside the request, and only numbers come back. Details on /privacy. To measure without uploading, use the playground above.
GET /v1/usage Where your key stands this month 11 fields
Your plan, the calls and data used this month against the limits the API enforces, and your requests per minute. The same numbers as /account. Needs a key. Reading it is free and does not count as a call; it is limited to 30 a minute.
Example
curl https://audiolab.tools/v1/usage \
-H "Authorization: Bearer $AUDIOLAB_KEY"import os
import requests
r = requests.get(
"https://audiolab.tools/v1/usage",
headers={"Authorization": f"Bearer {os.environ['AUDIOLAB_KEY']}"},
timeout=30,
)
if not r.ok:
# error.code says what happened: https://audiolab.tools/docs/errors
raise RuntimeError(r.json()["error"]["code"])
print(r.json())const res = await fetch("https://audiolab.tools/v1/usage", {
headers: { Authorization: `Bearer ${process.env.AUDIOLAB_KEY}` },
});
const body = await res.json();
// On an error, body.error.code says what happened: https://audiolab.tools/docs/errors
if (!res.ok) throw new Error(body.error.code);
console.log(body); Response
Where a key stands this month. The same numbers as /account. The word next to a number says how far to trust it
(the five statuses); a margin says how far it can be off.
Add "measureStatus": true to the request to get the same status and margin inside the answer.
| Field | Type | Status | Notes |
|---|---|---|---|
| plan* | string | The plan of the account. custom is a hand-issued key without a subscription. · free | plus | studio | enterprise | founder | custom | |
| period.start* | string | First day of the month (UTC). | |
| period.end* | string | Last day of the month (UTC). | |
| period.resetsAt* | string | When the counters start again. | |
| calls.used* | integer | Accepted requests this month, across all the keys of the account. A request that then fails still counts. | |
| calls.limit* | integer | Calls allowed per month; null when there is no limit. | |
| calls.remaining* | integer | limit minus used, never below 0; null when there is no limit. | |
| data.usedBytes* | integer | Bytes of audio processed this month. | |
| data.limitBytes* | integer | Bytes allowed per month; null when there is no limit. | |
| data.remainingBytes* | integer | limitBytes minus usedBytes, never below 0; null when there is no limit. | |
| requestsPerMinute* | integer | Requests per minute this key may make to the analysis routes. |
Errors
- 401 Invalid or revoked API key. /v1/batch and /v1/usage always need a key; the analysis routes also run without one, as a small keyless trial. invalid_api_key
- 429 Too many requests this minute. See Retry-After. rate_limited
- 500 Something went wrong on our side. The details are logged and never sent to you. storage_unavailable, analysis_error
Every error carries a stable code and a docs_url: the cause, the fix and an example for each.
Try it with your key
A GET without a body. It is free to call and does not count against your limits.
This asks the EU-hosted API where your key stands. No audio is involved.
Shared types Band · VoiceSegment · TimelineRegion · SeriesArray · SpeechSegment
| sub | number | |
| bass | number | |
| lowMid | number | |
| mid | number | |
| highMid | number | |
| presence | number | |
| air | number |
| startSec | number | |
| endSec | number | |
| meanRms | number | |
| levelDb | number | Phrase level, dBFS. |
| clipFraction | number | Share of clipped samples in the phrase. |
| decayScore | number | Energy left in the pause after this phrase, relative to the phrase itself, 0 to 1. Noise or reverb, and this number does not separate the two, so it is not a reverberation measure. Same limit as roomEchoScore. Null when no usable pause follows the phrase. |
| tailDecayDbPerSec | number | Decay slope of the phrase tail, dB per second. Null when no usable tail. |
| flankFlatness | number | Background character on the phrase flanks, 0 tonal to 1 noise-like. |
| startSec | number | |
| endSec | number | |
| kind | string | voice | silence | music | noise | clipping |
| confidence | number |
| intervalSec | number | Seconds between samples. |
| count | integer | Number of samples. |
| samples | number[] | |
| unit | string | Unit of measurement (e.g. "LUFS", "peak (-1..1)"). |
| startSec | number | |
| endSec | number | |
| durationSec | number | |
| meanRmsDb | number | Mean RMS of the segment in dBFS. |
* required · generated from public/openapi.yaml, the lens registry and lib/code-samples.mjs
Keys and limits
The hosted API uses a bearer key in the Authorization
header, generated from your dashboard. Keys are self-serve and live, with a free tier of 1,000 calls and
2 GB a month, no card. The playground above still needs no key at all: it runs the engine in your
browser and your audio never leaves the device.
Authorization: Bearer al_live_xxx issued from the dashboard
Limits
What the API enforces today. These are ceilings, not quotas: see pricing for calls and data per month, and errors for what each failure means.
| File sent directly in the request body | 150 MB |
File via a signed upload URL, or read back by objectPath | 150 MB |
Longest track for the analysis routes, at 48 kHz stereo. Lower sample rates and mono fit longer. The loudness routes (mixlab/*) do handle long files. | ~7.7 min |
Items in one /v1/batch call | 20 |
Loudness, measured against the standard
Don’t take the loudness numbers on faith. Here is the engine run against the canonical EBU Tech 3341/3342 and ITU-R BS.1770-4 reference signals, checked against the standard’s own tolerances. The signals are the standard’s own, not ours; you can synthesise them and hold any meter to the same expected values. Every case we run is listed below, pass or fail, with the value the standard expects beside the value we measured.
The cases cover loudness, loudness range and true peak against the EBU signals. Every other measure
carries a status of its own: 94 validated against a standard or a known value, 56 proxies and
5 beta (no ground-truth test yet, which does not mean wrong). Scene, content type, intelligibility and
speaking rate are proxies, and the room-echo score was withdrawn: it is not a reverb measure. Each measure, with
what it was tested against and what it was not, is on the
lenses page, how to read a status is in
Read a response, and the OpenAPI document marks each
response field with x-validation.
Every case, with the expected and the measured value 5 compliance · 2 extended
- Integrated loudness 3341-1
Stereo 1 kHz sine at −23 dBFS → −23.0 LUFS (absolute calibration)
-22.99 LUFStarget -23 · ±0.1 LUPass - Integrated loudness 3341-2
Stereo 1 kHz sine at −33 dBFS → −33.0 LUFS (linearity)
-32.99 LUFStarget -33 · ±0.1 LUPass - Integrated loudness gate-abs
Silence → −23 dBFS tone → silence: −70 LUFS absolute gate excludes silence → −23.0 LUFS
-23.04 LUFStarget -23 · ±0.1 LUPass - Integrated loudness gate-rel
−23 dBFS then −40 dBFS: −10 LU relative gate drops the quiet block → −23.0 LUFS
-23.06 LUFStarget -23 · ±0.1 LUPass - True peak tp-997
997 Hz sine at 0 dBFS → true peak 0.0 dBTP (BS.1770-4 true-peak)
0 dBTPtarget 0 · +0.2 / −0.4 dBPass
Extended observations
- Loudness range 3342-1
−20 dBFS and −30 dBFS blocks → loudness range ≈ 10 LU (EBU 3342)
10 LUtarget 10 · ±1 LUPass - True peak tp-isp
12 kHz 0 dBFS, π/4 phase: samples sit at −3.0 dBFS, inter-sample peak detection (true peak should rise above sample peak)
0.1 dBTPtarget 0 · +0.2 / −0.4 dBPass
We measure what we claim. Per-task accuracy (scene, pitch, beat) and server-side research benchmarks aren’t claimed until the same kind of harness backs them.
Use it from an AI agent
AudioLab ships an MCP server + a Claude Code plugin so any AI agent (Claude Code, Claude Desktop, Cursor) can analyze audio (ten tools, one install), plus AI-native docs so an agent can read the engine without scraping marketing.
Add to Claude →
- analyze_loudness · check_target
- analyze_voice · get_spectrum
- + 6 more · A/B compare, batch, profiles
Ten tools over MCP. One command in Claude Code wires up the skill + server; bring an API key.
llms.txt
An LLM-readable map of the engine, API, and labs. The convention agents look for first.
CLAUDE.md
Drop it in your repo so your agent knows how to reach AudioLab, with the honest preview scope baked in.
The same job, three ways. The difference is who hosts the audio, how many tools you wire together, and who maintains it.
Scripting ffmpeg yourself
Free, but you build and babysit the pipeline. It only does loudness, there is no voice or signal layer, and it lives in your infra.
A cloud audio API
Capable, but every file is uploaded to a server you do not control, usually US-hosted, and most vendors do exactly one thing.
AudioLab
One key for loudness, voice and signal, EU-hosted, with the loudness DSP checked against the EBU Tech 3341/3342 test cases and every case published on this page. The same engine also powers the in-browser labs, which analyse on your device without uploading your audio; those need no key at all.
Built for teams who ship audio
LUFS-QC every episode before publish, without uploading unreleased audio.
Loudness and clarity QC on generated speech, right in your pipeline.
Embed analysis without building or hosting the DSP yourself.
Show creators their loudness and dynamics before they submit.
Building on audio? Get a key.
The hosted API is live at audiolab.tools/v1/*. Self-serve keys with a free tier, live now. Add it to Claude or any agent in one page, or call the REST API directly. Building something bigger? Email partners@audiolab.tools.