Developer API
Narrator Check developer API
The same ACX check and fix this site runs in your tab, over HTTP for scripts and production pipelines: measure or correct WAV, AIFF and MP3 chapters, normalize loudness, and have a manuscript read aloud for proof-listening.
Opening soon
The API is deployed and closed while it is tested. Until it opens, every endpoint answers
503 with the code not_open, and sign-in on the account page waits for it too.
Everything below is the contract it opens with.
Separate from the tools on this site
The tools on this site never upload your audio: every check and fix runs in this tab. The developer API is a separate, opt-in surface: it processes the files that key holders send to it, and nothing else. No page on this site calls it, and you never need it to use a tool.
What it does
acx-checkmeasures a chapter against ACX, Voices by INaudio, Spotify for Authors or Author’s Republic — RMS, peak, noise floor, room tone, sample rate, encoding and length — and returns the report as JSON.acx-fixcorrects what can be corrected and returns a 192 kbps constant-bitrate MP3, re-measured from its own encoded bytes before it is offered. A room too noisy to rescue is refused, not processed.loudness-fixnormalizes integrated loudness (LUFS) and true peak to a podcast, spoken-word mono or voice-over preset and returns the MP3.ttsreads up to 10,000 characters of your manuscript aloud with a built-in synthetic voice, so you can proof-listen before you record. The MP3 is labelled synthetic in its ID3 tags. ACX’s requirements say AI or text-to-speech narration is prohibited unless authorised, and this is not narration: it is a checking aid. Nothing here clones a voice. Upload the text as UTF-8; the job reports the voice, the model’s digest and how long the reading is. Expect it to take roughly as long as the audio it produces.
Input is WAV, AIFF or MP3. M4A, M4B and AAC are refused with 422; convert them first with the
M4A to MP3 converter, which runs in your browser.
How a job works
- Create it with a bodyless POST. Every parameter goes in the query string, including the file’s
exact size in bytes and its SHA-256 in base64. The answer is
201with a job id and a presigned upload URL. - PUT the file to that URL with the two headers the answer names,
content-lengthandx-amz-checksum-sha256. They are signed: storage refuses a file of any other length or content. The URL lasts 15 minutes. - Poll
GET /api/v1/jobs/{id}every few seconds. The job moves frompendingtorunningtosucceeded,failedorcancelled. - Download from the
download.urla succeeded job carries. It is signed for 15 minutes; poll again for a fresh one.
Why bodyless: the API sits behind the same CDN as this site, which signs every request it forwards. A request with a body would need a payload hash no ordinary HTTP client sends, so nothing here takes one. The audio never passes through the API at all; it goes straight to storage on the signed URL.
Quickstart with curl
Create a key on your developer account page, then:
KEY=sfk_live_your_key_here
FILE=chapter-01.wav
BYTES=$(wc -c < "$FILE" | tr -d ' ')
SHA=$(openssl dgst -sha256 -binary "$FILE" | base64)
# 1. create the job: a POST with no body, parameters in the query
JOB=$(curl -sS -G -X POST -H "X-Api-Key: $KEY" \
--data-urlencode "kind=acx-fix" --data-urlencode "target=acx" \
--data-urlencode "ext=wav" --data-urlencode "bytes=$BYTES" \
--data-urlencode "sha256=$SHA" \
https://narratorcheck.com/api/v1/jobs)
ID=$(echo "$JOB" | jq -r .jobId)
# 2. upload the file to the signed URL, with the signed checksum header
curl -sS -X PUT -H "x-amz-checksum-sha256: $SHA" \
--upload-file "$FILE" "$(echo "$JOB" | jq -r .upload.url)"
# 3. poll until the job finishes
while :; do
STATE=$(curl -sS -H "X-Api-Key: $KEY" https://narratorcheck.com/api/v1/jobs/$ID)
case $(echo "$STATE" | jq -r .state) in succeeded|failed|cancelled) break;; esac
sleep 5
done
echo "$STATE" | jq .result.meta
# 4. download the corrected MP3
curl -sS -o chapter-01.mp3 "$(echo "$STATE" | jq -r .download.url)"
--upload-file sets content-length itself, to the file’s size, which is the signed value.
-G puts the parameters in the URL, URL-encoding the base64 digest, and -X POST keeps the method.
The command-line client, and MCP
narratorcheck.mjs is a single file with no dependencies for Node 20 or newer. It computes the SHA-256, makes the bodyless POST, uploads with the signed headers, waits and downloads.
curl -sSO https://narratorcheck.com/developers/narratorcheck.mjs
export NARRATORCHECK_API_KEY=sfk_live_your_key_here
node narratorcheck.mjs run chapter-01.wav --kind acx-fix --target acx
node narratorcheck.mjs upload chapter-02.mp3 --kind acx-check # prints the job id
node narratorcheck.mjs wait <job-id>
node narratorcheck.mjs download <job-id> --out chapter-02-report.json
node narratorcheck.mjs usage
node narratorcheck.mjs mcp runs the same client as a Model Context Protocol server over stdio, with tools
to check, fix and normalize a local file, read a manuscript aloud, read a job and read your usage. Point an MCP
client at the command with NARRATORCHECK_API_KEY in its environment.
Authentication
Send your key as X-Api-Key on every call. Never as Authorization: Bearer: the CDN writes its
own signature into that header, so a bearer token is overwritten before it arrives. A key is shown once, when
you create it; we keep only a hash. Revoke it on the account page and it stops working at once.
Endpoints
| Endpoint | What it does |
|---|---|
| POST /api/v1/jobs | Create an audio job and get a presigned upload URL |
| POST /api/v1/tts | Create a text-to-speech job for proof-listening and get a presigned upload URL |
| GET /api/v1/jobs | List your recent jobs, newest first |
| GET /api/v1/jobs/{id} | Read a job, with a download URL once it has succeeded |
| DELETE /api/v1/jobs/{id} | Cancel a job that has not finished |
| GET /api/v1/usage | Your plan and this month's usage on each meter |
The full contract is the OpenAPI 3.1 document.
Plans and limits
- Free, free: 60 audio minutes and 10,000 text-to-speech characters a month, 1 job a minute, 20 a day.
- Pro, $12 a month (free during beta): 1,200 audio minutes and 200,000 text-to-speech characters a month, 5 jobs a minute.
A check costs its audio minutes once and a fix costs them twice; text-to-speech is charged per started thousand characters. A job is charged once, on what the worker actually measured, not on what was declared. Pro is $12 a month (free during beta): nothing is charged and no card is asked for.
Per file: at most 400 MiB, 120 minutes, 96 kHz and 2 channels; text-to-speech at most 10,000 characters. Across everybody, the API processes at most 3,000 audio minutes and 500,000 text-to-speech characters a day; past that it answers 429 site_daily_ceiling until midnight UTC.
What is kept, and for how long
Your upload is deleted when the job finishes, and results expire within 48 hours. The job record — its id, kind, state, timestamps and the report — is kept for three days so a client can read why a job failed, then deleted. No person listens to or reads what you send; it is processed by the same code the tools on this site run in a browser tab. Only send files you have the rights to. The privacy policy and terms are the binding version of this paragraph.
Status codes
| Status | Meaning |
|---|---|
| 200 / 201 | Done, or created. A create answers 201 with the upload URL. |
| 401 | No key, an unknown key or a revoked key: key_required, key_unknown, key_revoked. |
| 409 | A conflict: idempotency_key_reused, or job_finished when cancelling. |
| 410 | job_gone: no such job for your key — never created, expired, or not yours. |
| 422 | A parameter is missing or out of range, the format is refused, or the file can never fit your plan. |
| 429 | A limit, with Retry-After: rate_limited, daily_limit, quota_exceeded, site_daily_ceiling. |
| 503 | not_open until the API opens, or a missing configuration on the deployment. |
| 403 / 404 | A wrong path. The body is the CDN’s HTML error page, never a refusal from this API — this API never answers either. |
Decide on the status code, never on the body. A failed job carries error.code: for example
input_mismatch (the upload was not the file you declared), unreadable_audio,
too_long_audio, not_fixable (a room too noisy to rescue) or quota_exceeded;
for text-to-speech, text_not_utf8, empty_text or too_many_characters.