Start
| Base URL | https://api.mnemecho.com/api/v1 |
|---|---|
| Authentication | Authorization: Bearer <key> or X-API-Key: <key> |
| Transport | HTTPS only |
| Format | Requests as multipart/form-data, answers as JSON |
Keys are given by request during early access: hello@mnemecho.com. Keep your key on your server, never in a web page or a shipped app: anyone holding it can spend your quota. If it leaks, tell us and we rotate it; the old key stops at once.
What is graded
- Graded: pronunciation, meaning whether each expected Dutch sound was said, and said right, and whether each word is the word in the text.
- Not graded: rhythm, pitch and vowel length. These need a native
recording of the same sentence to compare against, and your own text has none.
Every response says so (
graded_by,reference). - Never graded: voice quality. A deep, high, male, female or accented voice is not marked down for how it sounds, only for which sounds it makes.
POST /score
Score one recording against one Dutch text.
| Field | Type | Rules |
|---|---|---|
text | form field | The Dutch that should be said. 1 to 12 words. Punctuation is ignored. |
audio | file | One recording: wav, flac, ogg or mp3. Any sample rate, mono or stereo. At most 15 seconds and 4 MB. |
curl
curl -X POST https://api.mnemecho.com/api/v1/score \
-H "Authorization: Bearer $MNEMECHO_KEY" \
-F "text=In februari is het Valentijnsdag." \
-F "audio=@attempt.wav"
Python
import os, requests
resp = requests.post(
"https://api.mnemecho.com/api/v1/score",
headers={"Authorization": f"Bearer {os.environ['MNEMECHO_KEY']}"},
data={"text": "In februari is het Valentijnsdag."},
files={"audio": open("attempt.wav", "rb")},
timeout=30,
)
resp.raise_for_status()
result = resp.json()
print(result["score"], result["verdict"], result["worst_word"])
JavaScript (Node 18 and later)
import { readFile } from "node:fs/promises";
const form = new FormData();
form.append("text", "In februari is het Valentijnsdag.");
form.append("audio", new Blob([await readFile("attempt.wav")], { type: "audio/wav" }), "attempt.wav");
const resp = await fetch("https://api.mnemecho.com/api/v1/score", {
method: "POST",
headers: { Authorization: `Bearer ${process.env.MNEMECHO_KEY}` },
body: form,
});
if (!resp.ok) throw new Error(`${resp.status} ${(await resp.json()).detail}`);
const result = await resp.json();
console.log(result.score, result.verdict, result.worst_word);
Full runnable examples are in the GitHub repository.
The response
A real response for a native speaker saying "In februari is het Valentijnsdag".
Two words shown in full; the others look like "In".
{
"text": "In februari is het Valentijnsdag",
"score": 91.6,
"verdict": "pass",
"graded_by": ["pronunciation"],
"reference": false,
"words": [
{"word": "In", "ok": true, "wrong_word": false, "sounds": [
{"target": "ɪ", "heard": "ɪ", "ok": true, "quality": 0.9997, "hint": null},
{"target": "n", "heard": "n", "ok": true, "quality": 1.0, "hint": null}
]},
{"word": "februari", "ok": false, "wrong_word": false, "sounds": [
{"target": "f", "heard": "f", "ok": true, "quality": 0.9993, "hint": null},
{"target": "eː", "heard": "ɛ", "ok": false, "quality": 0.6629,
"hint": "the long ee in zee"},
{"target": "b", "heard": "b", "ok": true, "quality": 0.9999, "hint": null},
...
]},
...
],
"ipa_target": "ɪ n f eː b r y aː r i ɪ s h ə t v aː l ə n t ɛɪ n z d ɑ x",
"ipa_heard": "ɪ n f ɛ b r y aː r i ɪ s h ə t v aː l ə t ɛɪ n z d ɑ x",
"worst_word": "februari",
"flags": ["unobservable_phones=1", "no_reference"]
}
| Field | Meaning |
|---|---|
text | The words that were scored, punctuation removed. |
score | 0 to 100, one decimal. Set against native Dutch speakers, so 90 and up is native-like, not merely good. |
verdict | excellent, pass, marginal_fail or hard_fail. See verdicts. |
graded_by | Which parts made the score. Always ["pronunciation"] on this endpoint. |
reference | Always false: no native recording was compared. |
words | One entry per word, in order. |
words[].ok | false if any of its sounds counted against the score, or if it was heard as another word. |
words[].wrong_word | true when a different word was said in its place (maart for februari). The attempt cannot pass. |
words[].sounds | One entry per expected sound (phoneme) in the word. |
sounds[].target | The sound expected, in IPA. ː marks a long vowel. |
sounds[].heard | What was heard instead, on a wrong sound. Equal to target when ok. |
sounds[].ok | Whether this sound counted against the score. |
sounds[].quality | 0 to 1, how clearly the expected sound was heard (1 is clearly). |
sounds[].hint | On a wrong sound, a short English tip such as "the long ee in zee". Otherwise null. |
ipa_target | All expected sounds, space separated. |
ipa_heard | What was heard, sound by sound. Useful for debugging, too raw to show players. |
worst_word | The word to work on first, or null. |
flags | Notes on the attempt. See flags. |
A word swapped
The same recording sent with the text "In maart is het Valentijnsdag." The speaker said februari, so maart is marked as a wrong word and the attempt does not pass.
{
"score": 74.0,
"verdict": "marginal_fail",
"words": [
...
{"word": "maart", "ok": false, "wrong_word": true, "sounds": [
{"target": "m", "heard": "b", "ok": false, "quality": 0.0013, "hint": "the m in mens"},
...
]},
...
],
"worst_word": "maart",
"flags": ["unobservable_phones=1", "wrong_word=1", "no_reference"]
}
Verdicts
Use verdict for your logic, and worst_word plus the wrong sounds for feedback.
| Verdict | Score | Meaning | Suggested action |
|---|---|---|---|
excellent | 95 to 100 | Native-like. | Full reward. |
pass | 75 to 94.9 | Understood and correct, small slips. | Accept; show the one wrong sound, if any. |
marginal_fail | 55 to 74.9 | Recognisable, but a word or sounds were off. | Ask for another try; show worst_word. |
hard_fail | under 55 | Mostly wrong, or a different sentence. | Ask again; play your model audio first. |
Flags
| Flag | Meaning |
|---|---|
no_reference | Always present: rhythm and pitch were not graded. |
wrong_word=N | N words were heard as other words. See words[].wrong_word. |
unobservable_phones=N | N expected sounds were not heard in their place at all. A few are forgiven; more count as missing. |
missing_phones=N | N sounds were judged left out, and were charged. |
reduced_phones=N | N sounds that Dutch speakers often drop were dropped, and not charged. |
insertions=N | N extra sounds were heard that the text does not have. |
text_mismatch | Most of the text was not heard: something else was said, or the wrong clip was sent. |
New flags may be added within v1. Ignore flags you do not know.
Showing feedback
- Lead with the verdict in plain words.
- Highlight
worst_wordin the sentence. If itswrong_wordis true, say which word was expected. - Under it, show each sound where
okis false, with itshint. Skip the IPA unless your users know it. - Show one or two sounds per try. People fix one thing at a time.
GET /health
Checks your key and shows your limits. Needs a key.
curl https://api.mnemecho.com/api/v1/health -H "Authorization: Bearer $MNEMECHO_KEY"
{"ok": true, "client": "your-name",
"limits": {"per_minute": 20, "per_day": 500},
"request": {"max_words": 12, "max_audio_s": 15.0,
"formats": "wav, flac, ogg or mp3"}}
GET /status
Whether the service answers. No key, readable from any origin. The status page uses it.
curl https://api.mnemecho.com/api/v1/status
{"ok": true}
Errors
Every error is JSON with a detail message written for a developer, for example
{"detail": "send the Dutch text as the form field 'text'"}.
| Status | When | What to do |
|---|---|---|
| 400 | Empty audio, or audio with no samples. | Check the recording step. |
| 401 | Key missing or wrong. | Check the Authorization header. |
| 413 | Audio over 15 seconds, or upload over 4 MB. | Trim silence; send one sentence per call. |
| 415 | Audio not readable. | Send wav, flac, ogg or mp3. Browser webm recordings must be converted first. |
| 422 | Text missing, over 12 words, or nothing in the audio could be heard. | For "could not be heard", ask for another try, louder. This is not a low score. |
| 429 | Over your per minute or per day limit. | Wait the seconds in the Retry-After header. |
| 500 | Our fault. | Retry once; if it repeats, tell us the time. |
Limits
- Each key has a per minute and a per day limit;
GET /healthshows yours. The day resets at 00:00 UTC. - Failed requests (400, 413, 415, 422) count toward the limits; 429 answers do not.
- Our network also blocks bursts of more than 10 requests in 10 seconds.
Good practice
- Send sentences, not single words. One word said alone is often not heard at all (422). Three words or more is reliable.
- Record 16 kHz mono wav if you can choose. Other formats work.
- Start recording before the speaker starts and stop shortly after. Silence within the 15 seconds is fine.
- Background noise, music, or Bluetooth earbuds with noise suppression lower the score. A wired or built-in microphone in a quiet room works best.
- Write numbers out (twaalf, not 12). Names and English loanwords may be expected with a pronunciation your users do not use.
- Call from your server, not the user's device, so your key stays secret.
For machines
- openapi.json: the OpenAPI 3.1 description.
- llms.txt: a short guide for AI agents.
- GitHub: examples, changelog, and issues for bugs and requests.