What the ArticScore API returns
ArticScore takes a recording plus the word a child was asked to say, and returns one row per sound: a score from 0 to 100, the phoneme in ARPAbet and IPA, the millisecond span it occupied, the engine's best candidate for what was produced instead, a clinical error type, and a confidence tier. This page documents every field.
Interactive API reference (OpenAPI) — the live specification, generated from the running API rather than written by hand, so it is never out of date. Browse every endpoint and error response, or take the OpenAPI document to generate a client.
The request
One endpoint, one multipart POST. Usually you do not send phonemes — the API derives the expected phoneme sequence from the target text and aligns it against the audio itself. For targets a dictionary cannot spell, and for “which of these did they say?”, see sounds, syllables and choices.
POST /api/v1/score
Host: api.speechtherapymagic.com
Authorization: Bearer <your key>
Content-Type: multipart/form-data
audio = <file> # the recording
text = "rabbit" # the word or phrase the child was asked to say
include = "quality" # optional: extra blocks, comma-separated (see below)
dialect = "en-us" # optional: en-us (default) or en-gb (see "British English")
# optional, instead of or as well as text (see "Sounds, syllables and choices"):
phones = "R AA1" # an explicit pronunciation, for targets no dictionary spells
choices = ["key","tea"] # 2–4 candidates: which one was said?The response
A child saying “rabbit” as “wabbit”. The second phoneme is truncated for length; a real response contains a row for every sound in the word.
{
"request_id": "9f3c1a7e-2b4d-4c8a-9e11-6d0f2a5b7c34",
"text": "rabbit",
"overall_score": 61.4,
"words": [
{
"word": "rabbit",
"score": 61.4,
"phonemes": [
{
"phone": "R",
"phone_ipa": "ɹ",
"score": 41.2,
"sound_most_like": "W",
"sound_most_like_ipa": "w",
"extent": [120, 260],
"flagged": true,
"error_type": "substitution",
"tier": "flag"
},
{
"phone": "AE",
"phone_ipa": "æ",
"score": 88.1,
"sound_most_like": null,
"sound_most_like_ipa": null,
"extent": [260, 390],
"flagged": false,
"error_type": null,
"tier": "clean"
}
]
}
],
"tips": ["Try holding the /r/ a little longer before the vowel."],
"passed": false
}Top-level fields
| Field | Type | Meaning |
|---|---|---|
| request_id | string | Unique id for this call. Log it — quoting it in a support request is what lets us find the exact scoring run. |
| text | string | The target text you sent, normalised. This is what the expected phoneme sequence was derived from. Without text, your phones or the first of your choices. |
| overall_score | number 0–100 | Utterance-level score. Useful for a headline; not what to build therapy logic on. |
| overall_status | string | good (80+), fair (60–79) or poor (below 60), for the whole utterance. |
| overall_stars | integer 0–3 | The child-facing form of the score: 3 at 90+, 2 at 80+, 1 above the pass line. |
| overall_label | string | A short encouraging label, written to be shown to a child as-is. |
| words[] | array | One entry per word in the target text. |
| tips | array | Every coaching tip raised anywhere in the utterance, de-duplicated. Empty when nothing was flagged. |
| passed | boolean | Whether the utterance cleared the pass threshold of 70. |
| quality | object | With include=quality: whether the recording can be scored at all. See below. |
| input | object | Only when you sent phones: the reference that was scored, and the alphabet it was read in. |
| choices | object | Only when you sent choices: which candidate the recording sounds most like. See below. |
Phoneme fields
This is the part of the response that distinguishes a scoring API from an explanation API. The three fields to pay attention to are sound_most_like, error_type, and tier.
| Field | Type | Meaning |
|---|---|---|
| phone | string | The expected phoneme in ARPAbet. Covers the complete General American inventory — consonants, vowels and diphthongs — not a consonant subset. See the reference below. |
| phone_ipa | string | The same phoneme in IPA, e.g. "ɹ", "æ", "oʊ". Populated for every phone the engine scores. |
| score | number 0–100 | How well this phoneme was produced. Calibrated against human ratings; saturates near the top, so ranking beats thresholding. |
| sound_most_like | string | null | The engine's best candidate for what was actually produced, in ARPAbet. Populated whenever the phone is flagged. |
| sound_most_like_ipa | string | null | The same candidate in IPA. |
| extent | [number, number] | Start and end of this phoneme in the audio, in milliseconds. Use it to highlight or replay the fragment. |
| flagged | boolean | Whether this phone crossed the flag threshold. A convenience over score; the threshold is documented and stable. |
| error_type | string | null | One of substitution, omission or unclear. distortion is reserved in the schema and not returned today. Null when the phone is not flagged. |
| tier | string | flag, review, or clean. The confidence tier — see below. This is the field to branch product behaviour on. |
| alternatives | array | null | With include=alternatives: up to three sounds most likely produced instead, most likely first, each with p (its share of what was heard). Null on clean phonemes. |
| p_target | number 0–1 | null | With include=alternatives: the target sound's own share of what was heard. Null on clean phonemes. |
| p_error | number 0–1 | With include=confidence: the probability a trained listener would mark this sound wrong. See the calibration note below. |
| confidence | number 0–1 | With include=confidence: the probability that this sound's flag / no-flag verdict is right. |
Optional blocks: include
Some integrations want the scores and nothing else; others want the full clinical picture. The heavier parts of the response are therefore opt-in, through one comma-separated form field, include:
| Value | Adds | Cost |
|---|---|---|
| syllables | words[].syllables filled in: a score per syllable, its phone count, the dictionary stress (1 primary, 2 secondary, 0 unstressed) and its extent; phonemes[].syllable_index. | Under a millisecond; about 25% more JSON. |
| position | phonemes[].position — initial, medial or final in the word — and phonemes[].syllable_role (onset, nucleus, coda), plus a top-level sound_positions summary per sound and position. | Under a millisecond; about 65% more JSON (most of it the sound_positions summary). |
| timing | words[].extent: where each word falls in the audio, as [start_ms, end_ms]. | Negligible (about 2% more JSON). |
| tips | The child-facing coaching lines: the top-level tips list, words[].tips and phonemes[].tip. | Negligible. Included by default; see below. |
| quality | A top-level quality block: whether the recording can be scored at all — speech present, the target said, nothing cut off. When quality.usable is false, ask the child to try again instead of showing a score. | Computed on every call; about 200 bytes of JSON. |
| alternatives | phonemes[].alternatives — up to three sounds most likely produced instead, each with a probability — and phonemes[].p_target, on flag and review phonemes; null on clean ones. | Computed only when you ask for it. |
| confidence | phonemes[].p_error — the probability a trained listener would mark the sound wrong — and phonemes[].confidence, the probability the flag / no-flag verdict is right. | Computed only when you ask for it. |
| stress | words[].stress — which syllable was produced most prominently against the one the dictionary stresses, a 0–100 stress score, per-syllable prominence, and whether an unstressed syllable was left out. For words of two or more syllables. | Computed only when you ask for it. |
Leave include out and you get the standard response shown above, coaching tips included, exactly as before the parameter existed. Send it and you get the scores, statuses and per-phoneme detail plus exactly the blocks you name — so include= on its own is the leanest response there is, and include=syllables,tips adds syllables while keeping tips. Asking for more makes the response larger, not noticeably slower. An unknown value is a 422 that lists the valid ones.
Syllables come from the dictionary pronunciation of the word, and a syllable’s score is built from the scores of the sounds inside it. stress_level is the expected stress from the dictionary; whether the child stressed the right syllable is reported by include=stress (below), so stress_score here stays null. letters is null because syllables are split on the pronunciation, not the spelling.
"words": [{
"word": "rabbit",
"syllables": [
{ "letters": null, "phone_count": 2, "score": 48.0,
"stress_level": 1, "stress_score": null, "extent": [120, 390] },
{ "letters": null, "phone_count": 3, "score": 84.4,
"stress_level": 0, "stress_score": null, "extent": [390, 610] }
],
"extent": [120, 610],
"phonemes": [
{ "phone": "R", "score": 41.2, "position": "initial",
"syllable_role": "onset", "syllable_index": 0, ... }
]
}],
"sound_positions": [
{ "phone": "r", "position": "initial", "n": 1,
"mean_score": 41.2, "min_score": 41.2, "n_flagged": 1 }
]Was the recording usable? include=quality
A recording of silence, of a different word, of two voices, or one that starts after the child has already begun still produces numbers, and those numbers mean nothing. quality says whether the recording can be scored at all. When quality.usable is false, do not show the score: ask the child to try again. reason is the first problem found, in this order: no_speech, wrong_word, extra_speech, cut_off, incomplete. warnings lists every check that fired, including quiet and clipped, which never block on their own and point at the recording setup instead.
"quality": {
"usable": false,
"reason": "wrong_word",
"speech_detected": true,
"completeness": 0.92,
"matches_target": false,
"match_confidence": 0.81,
"cut_off": null,
"warnings": ["wrong_word", "quiet"]
}What else might it have been? alternatives and confidence
alternatives lists, for each flag or review phoneme, up to three sounds the child most likely produced instead, with p, each one’s share of what the engine heard there, and p_target, the target’s own share. It is a ranked list of candidates for a clinician to look at, not a transcription. confidence adds p_error, the probability a trained listener would mark the sound wrong, and confidence, the probability that the flag / no-flag verdict is right.
{ "phone": "R", "score": 41.2, "tier": "flag",
"alternatives": [
{ "phone": "W", "phone_ipa": "w", "p": 0.62 },
{ "phone": "L", "phone_ipa": "l", "p": 0.07 }
],
"p_target": 0.21,
"p_error": 0.83,
"confidence": 0.88, ... }Was the right syllable stressed? include=stress
Adds a stress block to every word. For each syllable of the dictionary pronunciation you get the expected stress (primary, secondary or unstressed), its prominence— how much it stood out, 0–1, summing to 1 across the word — and its extent. At word level, detected_primary is the syllable produced most prominently, match says whether it is the one the dictionary stresses (expected_primary), and scoreis the stressed syllable’s prominence times 100. weak_syllable_deletionis true when an unstressed syllable was left out altogether — “nana” for “banana”. Stress is measured from the recording’s duration, loudness, pitch and vowel quality.
Stress needs something to compare, so it applies to words of two or more syllables. On a one-syllable word applicable is false and score, match and detected_primary are null.
"words": [{
"word": "banana",
"stress": {
"applicable": true,
"score": 21.0,
"match": false,
"expected_primary": 1,
"detected_primary": 0,
"weak_syllable_deletion": false,
"syllables": [
{ "index": 0, "phones": ["B", "AH"], "expected": "unstressed", "prominence": 0.58,
"match": false, "deleted": false, "weak_syllable_deleted": false, "extent": [80, 260] },
{ "index": 1, "phones": ["N", "AE"], "expected": "primary", "prominence": 0.21,
"match": false, "deleted": false, "weak_syllable_deleted": false, "extent": [260, 430] },
{ "index": 2, "phones": ["N", "AH"], "expected": "unstressed", "prominence": 0.21,
"match": true, "deleted": false, "weak_syllable_deleted": false, "extent": [430, 600] }
]
},
...
}]Sounds, syllables and choices: phones and choices
text works for anything a dictionary spells. Two optional fields cover the rest, and text becomes optional when either one names the target.
phones: an explicit pronunciation
Send the expected sounds yourself for a non-word, a sound practised on its own, or a consonant-vowel syllable. ARPAbet, upper-case, with optional stress digits and words separated by | (DH AH0 | K AE1 T), or IPA in space-separated symbols. The alphabet is detected (all upper-case ARPAbet reads as ARPAbet) or can be named with phone_alphabet=arpabet|ipa. With phones the phones are the target and any text is only a label; the response adds an input block echoing what was scored.
# a non-word (no dictionary entry)
text = "blick" phones = "B L IH1 K"
# a sound on its own
phones = "S"
# a consonant-vowel syllable
phones = "R AA1"
# IPA works too (the engine's eSpeak symbols, space-separated)
phones = "ʃ iː" phone_alphabet = "ipa"choices: which one did they say?
Send 2–4 candidates — a minimal pair, a picture choice — as a JSON array of words, of {"text": …} or {"phones": …} objects, or one per line. The rest of the response still scores the target (text or phones, or the first candidate when you send neither), and a choices block says which candidate the recording sounds most like, with a probability and a score for each.
# request: which of the pair did the child say?
choices = ["key", "tea"] # no text: the first choice is the target
# response (excerpt): the scores describe "key"; choices says "tea" was heard
"text": "key",
"overall_score": 44.0,
"choices": {
"options": [
{ "text": "key", "phones": ["K", "IY1"], "p": 0.14, "score": 44.0, "is_target": true },
{ "text": "tea", "phones": ["T", "IY1"], "p": 0.86, "score": 88.5, "is_target": false }
],
"best": 1,
"said_target": false,
"p_target": 0.14
}If a phones or choices request cannot be honoured you get a 501 with error: "input_not_supported" and nothing is scored — never a score for text in place of the target you named. The same goes for an include value you name explicitly (quality, alternatives, confidence, stress): it is honoured or refused with a 501, never silently left out. Phones the engine cannot read are a 422 with error: "input_rejected" and the reason.
British English: dialect=en-gb
By default the target is scored against General American English (dialect=en-us). Send dialect=en-gb and it is scored against a British English pronunciation dictionary (Britfone, Standard Southern British, with a rule-based British fallback for words it does not list). The response has exactly the same shape, and phone is still ARPAbet: British vowels are reported as the nearest ARPAbet vowel.
The practical difference is /r/. British English is non-rhotic: an /r/ after a vowel, with no vowel following, is not pronounced. So for “car” or “butter” the British reference has no /r/ at all, and a child who leaves it out is saying the word correctly — nothing is flagged. An /r/ before a vowel, as in “red” or “carry”, is part of British English too, and is still scored like any other sound. Any other dialect value is a 422.
British English has been evaluated on adult British speech only. No British child speech exists under a licence we can use, so it has not been evaluated on children; American English remains the dialect evaluated on children.
The sound inventory
The engine scores 39phonemes — the full General American English inventory. Vowels and diphthongs are scored by the same model, on the same 0–100 scale, and carry the same tier and error_type fields as consonants; there is no separate vowel path and no reduced feature set for them. Every phone value the API can return appears in the tables below, alongside the phone_ipa you will receive with it.
| ARPAbet | IPA | Example |
|---|---|---|
| P | p | pig |
| B | b | ball |
| T | t | toe |
| M | m | moon |
| N | n | nose |
| HH | h | hat |
| W | w | water |
| K | k | cat |
| F | f | fish |
| D | d | dog |
| NG | ŋ | sing |
| G | ɡ | goat |
| V | v | van |
| Y | j | yes |
| S | s | sun |
| SH | ʃ | shoe |
| L | l | leaf |
| Z | z | zoo |
| CH | tʃ | chair |
| JH | dʒ | jump |
| TH | θ | think |
| ZH | ʒ | measure |
| DH | ð | that |
| R | ɹ | rabbit |
| ARPAbet | IPA | Example |
|---|---|---|
| IY | iː | bee |
| IH | ɪ | sit |
| EH | ɛ | bed |
| AE | æ | cat |
| AA | ɑ | father |
| UW | uː | moon |
| AH | ʌ | cup |
| AO | ɔ | saw |
| UH | ʊ | book |
| ER | ɝ | bird |
| ARPAbet | IPA | Example |
|---|---|---|
| EY | eɪ | cake |
| OW | oʊ | boat |
| AY | aɪ | bike |
| AW | aʊ | cow |
| OY | ɔɪ | boy |
Two notes on the mapping. ARPAbet AHcovers both the stressed vowel of “cup” (/ʌ/) and the reduced schwa of “about” (/ə/); CMU separates them only by a stress digit, which the API does not expose, so both arrive as AH. ARPAbet ERis the r-coloured vowel of “bird” and “water” (/ɝ/ stressed, /ɚ/ unstressed) — it is a vowel, not the consonant R, though clinically it is usually treated as part of /r/ work.
Consonant blends. A blend such as sp, bl or stris a sequence of the phones above, not a phone of its own, and the response has no blend-level field. “String” comes back as five results — S, T, R, IH, NG — each scored, tiered and timed independently, in the order they occur. That is the more useful output for a cluster anyway: cluster reduction is one element of the cluster going missing, so the per-phone results tell you which one, which a single blend score could not. To present a cluster as one target, group the consecutive phone results yourself and decide your own rule for combining them.
The three confidence tiers
Rather than making every integrator pick their own score threshold and get it slightly wrong, the API collapses its signals into three tiers that map onto the three decisions a product actually makes.
| Tier | Meaning | What to do with it |
|---|---|---|
| flag | The engine's lowest-scoring tier — its strongest signal that something went wrong, which on our evaluation set still meant most flags were false positives. | The top of a clinician's worklist. Surface it to a child as practice feedback only where being wrong is low-cost, and expect roughly two in three flags to be false positives at this tier. |
| review | Uncertain. The engine saw something but is not confident. | Send to a clinician's queue. Do not show a child a correction based on this alone. |
| clean | Nothing detected. | Treat as 'no signal', not as 'verified correct'. See the warning below. |
Error types
The error_type field uses the vocabulary a clinical note is written in, because that is what makes the output useful to a therapist rather than only to a scoring loop.
| Value | Meaning | How reliably we detect it |
|---|---|---|
| substitution | A different phoneme was produced in place of the target. | Detectable, but the weakest category — see the benchmark. |
| omission | The target phoneme was not produced at all. | The best-handled category. A deleted sound leaves an unambiguous gap. |
| distortion | The target was attempted but produced incorrectly, without becoming another phoneme. | Reserved: not returned today. Structural limitation — see below. |
| unclear | Something is wrong but the engine cannot characterise it. | Route to a human. |
Building on the output
Three patterns cover most of what teams actually do with this response.
Immediate practice feedback
Branch on tier === "flag" and use sound_most_liketo phrase the feedback concretely — “the R in rabbit came out as a W” instead of “try again”. Use extent to replay just that fragment. Keep review silent in a child-facing loop: at these precision levels an uncertain flag shown to a child is usually wrong, and being told you are wrong when you were right is the fastest way to lose a young user.
A clinician worklist
Rank recordings by score rather than thresholding them. In our clinical evaluation, reviewing only the worst-ranked productions recovered a disproportionate share of the clinician's error list — far more than reviewing the same number at random would have. That is, in our view, the highest-value thing you can build on this API today.
A rendering of all three
The ArticScore overview shows this exact response rendered three ways — a child-facing game screen, a practice summary, and a clinician's review row — so you can see which fields carry which part of each experience.
Progress tracking over time
Aggregate per-phoneme scores by sound and by word position across sessions. Because the API scores every phone in context, you can show that a child's initial /s/ improved while their /s/ in clusters did not — which is the shape of information an IEP goal is written in. Log the model field alongside the scores so a checkpoint change does not appear in your charts as clinical progress.
API reference questions
What does the ArticScore API return?
What audio format does ArticScore accept?
Do I need to send phonemes, or just the word?
What should my app do when a child says nothing, or says a different word?
What does the 'tier' field mean and why not just use the score?
Does a 'clean' tier mean the sound was definitely correct?
Is the score comparable between model versions?
Get the current specification
ArticScore is in limited early access, rolled out case by case. Tell us your use case and expected volume and we will come back to you.
Request access