Errors and troubleshooting

When the scoring call fails

POST /api/v1/score has a small, closed set of failures: seven status codes, one machine-readable error field, and two failure modes that never produce an error because they are recording problems rather than API problems — which include=quality now tells you about. This page is all of them, with the cause and the fix beside each one.

The status codes

POST /api/v1/score
Statuserror fieldWhat it means
200—Scored. Every phoneme in the target text is described, flagged or not
401api_key_required, or absentNo usable key. Either no Authorization header, a credential we do not recognise, or a browser session - this API is key-only
403insufficient_scopeThe key authenticated but does not carry the api:score ability. required_ability names what is missing
413— (not JSON)Rejected by the web server above 6 MB, before the API saw it. The only error here whose body is not JSON
422— (errors object)Validation. audio missing, empty or over 5 MB; text over 500 characters, or missing with no phones or choices; choices not 2-4 candidates. Nothing was scored and nothing counted against your allowance. error: input_rejected (no errors object) means the engine could not use the request - phones or choices it cannot read, audio it cannot decode, or a recording over 30 seconds or under 0.15 seconds - with its reason in message
429rate_limit_exceeded or daily_quota_exceededA per-key limit. The first is the per-minute burst window, the second the daily cap. Retry-After and retry_after_seconds both say how long to wait
501input_not_supportedphones, choices, or an include value you named (quality, alternatives, confidence) cannot be honoured right now; message names which. No score is returned - never a score for text in place of the target you named, and never a response silently missing a block you asked for. The same recording without that input will score
502scoring_failedThe request was well-formed and authorised; the engine did not answer. Retrying after a short pause is reasonable

Branch on the error string, not on the message. Messages are written for a human reading a log; the error field is the stable part, and it is what separates two cases that share a status code — a per-minute limit from a daily one, for instance.

The shape of an error body

Two shapes, and one exception. Validation failures use Laravel's field-keyed form: a message and an errors object naming each field that failed. Everything else carries a message plus a machine-readable error string, and sometimes a field specific to that failure. The exception is the 413, which is produced by the web server before the API runs and is therefore not JSON at all.

422 — validation

A missing file
HTTP/1.1 422 Unprocessable Content

{
  "message": "The audio field is required.",
  "errors": {
    "audio": ["The audio field is required."]
  }
}

The errors object is where the useful part lives; message is only the first of them. Read the keys, not the prose. One validation case has its own wording, because it is common enough to deserve a message a user could almost read:

An empty upload
HTTP/1.1 422 Unprocessable Content

{
  "message": "That recording came through empty. Please try again.",
  "errors": {
    "audio": ["That recording came through empty. Please try again."]
  }
}

401 and 403 — credentials

No usable key
HTTP/1.1 401 Unauthorized

{
  "message": "This endpoint requires an ArticScore API key. Send it as: Authorization: Bearer <key>",
  "error": "api_key_required"
}
A key without the scoring ability
HTTP/1.1 403 Forbidden

{
  "message": "This API key is not permitted to use this endpoint (missing ability "api:score").",
  "error": "insufficient_scope",
  "required_ability": "api:score"
}

429 — rate limits

The per-minute window
HTTP/1.1 429 Too Many Requests
Retry-After: 37
X-RateLimit-Limit: <your per-minute limit>
X-RateLimit-Remaining: 0

{
  "message": "Too many scoring requests. This API key is limited to <n> requests per minute.",
  "error": "rate_limit_exceeded",
  "retry_after_seconds": 37
}

error is rate_limit_exceeded for the per-minute burst window and daily_quota_exceeded for the daily cap. Both limits are per key. GET /api/v1/info reports the ones in force for yours, and needs no key to call — so a client can enforce them on its own side instead of discovering them the hard way.

502 — the engine did not answer

Upstream failure
HTTP/1.1 502 Bad Gateway

{
  "message": "Pronunciation scoring failed. Please try again.",
  "error": "scoring_failed",
  "request_id": "9f2b1c84-1f2a-4d5e-9a77-0c3b6e5d4a10",
  "detail": null
}

detail is always null in production. The request_id is the part to keep: it identifies the exact scoring run in our logs.

Cause and fix

Every failure we have seen in a first integration
What you seeWhat is actually wrongWhat to do about it
401 on every callThe header is missing, malformed, or you are relying on a cookieSend Authorization: Bearer <key> verbatim. A logged-in browser session on our site is not a key and never will be - the endpoint rejects session auth on purpose
401 after it used to workThe key was revoked or replacedAsk us. Nothing about the request can fix a credential we no longer recognise
403 insufficient_scopeThe key exists but was issued without api:scoreA property of the key, not the request. Retrying will not help - ask us to re-issue it
422 'The audio field is required'The multipart body did not parse - usually a hand-set Content-Type headerLet your HTTP client write Content-Type. Setting it yourself drops the multipart boundary and the file disappears server-side
422 'That recording came through empty'A zero-byte upload: the classic shape of a microphone-permission raceCheck the blob has bytes before you send it, and discard takes under about half a second in the browser
422 on audio sizeThe file is over the 5 MB ceiling the API enforcesRecord shorter takes, or stop uploading uncompressed audio. A few seconds of speech is a few hundred kilobytes
422 on text lengthThe target text is over 500 charactersScore a word or a phrase per call. Split a passage into items - you want per-item results anyway
413Over 6 MB, refused by the web server before the API ranYou are far past the ceiling, not marginally over. Check you are not sending raw PCM or a much longer recording than the endpoint is for
429 rate_limit_exceededThe per-minute burst window for this keyBack off for retry_after_seconds, then retry. Queue client-side rather than firing a batch at once
429 daily_quota_exceededThe daily cap for this keyWaiting will clear it at the window boundary. If your real volume needs more, tell us - the limits are per key and negotiable
422 input_rejectedThe engine could not use the request - a symbol in phones it does not know, audio it cannot decode, or a recording over 30 seconds or under 0.15 secondsRead message: it quotes the engine's reason. ARPAbet is upper-case with optional stress digits (K AE1 T); IPA is space-separated. Name the alphabet with phone_alphabet if detection guessed wrong
501 input_not_supportedphones, choices or a named include block was sent while that input cannot be honouredSend the request without it for now (text instead of phones / choices, the block left out of include) and retry the original later. Nothing misleading was returned
502 scoring_failedThe engine did not answer this callRetry once after a short pause. If it persists, send us the request_id from the body - it is the handle to the exact scoring run
No status code at allDNS, TLS, connectivity, or your own client timeout firingThe request never reached us, so there is nothing to look up. Keep the audio, retry, and surface it to the user as a connection problem rather than a scoring result

The two failures that return 200

These are the ones that cost the most time, because nothing in the scores says anything went wrong. Both are recording problems, and from the API's side nothing failed: it was handed audio and a target text, and it scored the one against the other.

Ask for include=quality and you are told. The response gains a quality block, and quality.usable is false when the recording had no speech (reason: "no_speech"), sounded like a different word (wrong_word), carried extra voices or words (extra_speech), was cut off (cut_off) or held only part of the word (incomplete). When it is false, show “let’s try again”, never the score. The checks were validated on synthetic failures built from children’s read speech (speechocean762), not yet on children with speech sound disorders, so the advice below still applies. The quality block.

There is no speech in the recording

A completely empty upload is caught — it comes back 422 with That recording came through empty. Please try again. But a file that contains bytes and no audible speech is a valid recording as far as the API is concerned. It gets scored, and what comes back is a real response at the floor, typically with omissions, which is exactly what a genuinely terrible production would also look like. There is no way to tell the two apart from the scores; quality reports it as no_speech.

  • Check the level before you upload. Decode the blob and look at its amplitude, or watch an AnalyserNode while recording. A take with no signal in it is worth discarding client-side.
  • Discard very short takes. Under about half a second is a mis-tap, not an attempt.
  • Suspect the constraints on Apple devices. Safari's noiseSuppression and autoGainControl can hand back audio so quiet it is effectively silent. Our own product asks for channelCount: 1 and nothing else there.
  • Check the microphone actually opened. A permission race produces a zero-byte blob, which is the 422 case - but a stream that opened on the wrong device produces a real file of near-silence, which is this one.

The audio does not match the text you sent

The expected phoneme sequence is derived from text and aligned against the audio. If the two do not correspond — the child said a different word, the prompt advanced before the recording did, the text carries a word the speaker never attempted — the engine still aligns the sequence it was given against the audio it was given. The result is a well-formed response full of numbers that mean nothing.

  • Compare the echoed text against the prompt. The response returns the target text, normalised. If your prompt and that string can ever disagree, they eventually will.
  • Watch for empty extent arrays. extent is [start_ms, end_ms] when timing was reported for a phoneme and empty when it was not. A response where timing is thin is worth a listen.
  • Beware of off-by-one prompts. The most common version of this bug is a UI that advances the target word while the previous recording is still uploading. Capture the target at record time and send that captured value, not whatever is on screen when the upload fires.
  • Remember there is no transcription. The engine is not an open-vocabulary recogniser and will not tell you what the speaker said instead. With include=quality it will tell you that it did not sound like the target (wrong_word); to ask which of a few known words was said, send them as choices.

Timeouts and retries

Scoring normally returns in about a second. A 30-second client timeout is a reasonable ceiling: long enough that nothing healthy trips it, short enough that a child is not left watching a spinner. Time out on your side rather than waiting indefinitely, and keep the recording so a retry does not mean recording again.

What is worth sending again
FailureRetry?Why
401NeverThe credential is wrong. Retrying repeats the same wrong credential
403NeverThe key lacks an ability. Only a re-issued key changes this
413 / 422NeverThe request itself is the problem. Fix it, then send a different one
501Later, or now with text aloneThe input you chose is unavailable at the moment; the rest of the API is not
429After retry_after_secondsHonour the value in the body and the Retry-After header. Do not retry sooner
502Once, after a short pauseThen give up and surface it. A second failure is not a transient one
Timeout / networkOnce or twice, with backoffKeep the audio so the user does not have to record again

There is no idempotency key. A retry is a new scoring request: it is scored again and it counts against your rate limit again. That makes an aggressive retry loop the fastest route to a 429, so back off on failure rather than hammering. A refused 429 itself was never scored and was not counted against anything else.

What to include in a support request

  • The request_id. Every scored response and every 502 carries one. It is the handle to the exact scoring run in our logs, and it is the single most useful thing you can send.
  • The time, with a timezone. Roughly when, if you cannot be exact.
  • The status code and the whole JSON body, not a paraphrase. The error field distinguishes cases that look identical from the outside.
  • The exact text you sent. Including capitalisation and punctuation — the expected phoneme sequence is derived from it.
  • The audio's format, size and duration, plus the browser and device it was recorded on. Most reports that turn out to be recording bugs are diagnosed from this line alone.
  • Whether it reproduces, and how often. Three in five is a different investigation from one in a thousand.
  • Not the recording. We do not need it and we would rather not have it. Partner audio is scored and dropped — never retained, never used for training, never tied to a persistent identifier for the speaker — and the request_id means we can find the call without it.
A report we can act on immediately
request_id:   9f2b1c84-1f2a-4d5e-9a77-0c3b6e5d4a10
when:         2026-08-28 14:07 UTC
status:       502
body:         {"message":"Pronunciation scoring failed. Please try again.",
               "error":"scoring_failed","request_id":"9f2b...","detail":null}
text sent:    "rabbit"
audio:        audio/webm;codecs=opus, 41 KB, ~1.4 s, Chrome 141 on Android
reproducible: 3 of 5 attempts in the last hour, same key
what I expected: a scored response

Send it through our contact page, or paste it into the support chat in the corner of any page. If the problem turns out to be in your recording pipeline rather than in ours — which is where a good share of them are — audio requirements is the page that covers it.

Hitting something that is not on this page?

Tell us what you saw and quote the request_id. ArticScore is in limited early access and the failure list is short by design - anything outside it is something we want to know about.

Request access

Error handling questions

What does a 401 from the ArticScore API mean?
There is no usable API key on the request. Either no Authorization header was sent, the credential is not one we recognise, or the request authenticated as a browser session rather than a key - this API is key-only by design, so that a logged-in visitor to our own site can never satisfy an authorization check meant for a partner. When a session was presented, the error field reads api_key_required.
What is the difference between a 401 and a 403?
A 401 means we could not authenticate the request at all. A 403 means the key is valid but was issued without the api:score ability the scoring endpoint requires, and the response names the missing ability in required_ability. A 403 is a property of the key rather than of the request, so retrying will not help - the key has to be re-issued.
How should I handle a 429 from the scoring API?
Wait, then retry. Two limits are enforced per key and the error field says which you hit: rate_limit_exceeded for the per-minute burst window, daily_quota_exceeded for the daily cap. Both responses carry retry_after_seconds in the body and Retry-After as a header. The refused request was not scored and was not counted against anything else. GET /api/v1/info reports the limits in force for your key, so a client can enforce them itself rather than discovering them.
What does scoring_failed mean and should I retry it?
It means the request was well-formed and authorised, and the engine did not answer it. It arrives as a 502 with error scoring_failed and a request_id. Retrying once after a short pause is reasonable. If it persists, quote the request_id - it is the handle to the exact scoring run, and it means you never have to send us the recording.
The API returned 200 but the scores are all at the floor. Is that an error?
No, and this is the most common non-error people report. Two recording problems both produce a valid, low-scoring response rather than an error: a clip with no speech in it, and a clip whose audio does not match the text you sent. Neither has a status code, because from the API's side nothing failed. Check the text field echoed back in the response against what the speaker was actually prompted with, and check that your recording contains audible speech before you upload it.
Why did setting Content-Type break my request?
Because a multipart body needs a boundary parameter that your HTTP client generates when it serialises the form. Setting Content-Type: multipart/form-data by hand overwrites that header without the boundary, the server cannot find the file part, and you get a 422 saying the audio field is required - which looks like a missing file rather than a header problem. Let the client set it.