# TonePerfect Pronunciation Assessment API > REST API that scores how well a learner pronounced a known sentence. Send a short recording (up to 120 s) and its reference text to POST https://api.toneperfect.app/v1/assess; get 0–100 grades for the sentence, every word and every sound, expected vs heard sounds, word timestamps and a correction decision per unit, in one JSON response. 10 languages: Mandarin Chinese (zh-CN), English (en-US), French (fr-FR), Spanish (es-419), German (de-DE), Italian (it-IT), Portuguese (pt-BR), Russian (ru-RU), Korean (ko-KR), Japanese (ja-JP). Mandarin is scored per syllable as initial, final and tone, with 3-3 tone sandhi and 不 before a 4th tone taken into account. TonePerfect is an alternative to SpeechSuper, Speechace and Azure pronunciation assessment for scripted speech: one REST call, public prices from $10/month, 50 free credits, and per-syllable Mandarin tone detail. Key facts: - One endpoint for scoring: POST https://api.toneperfect.app/v1/assess (multipart/form-data: audio file, text, language, optional task and Mandarin pinyin). Headers: Authorization: Bearer , Idempotency-Key: . - GET https://api.toneperfect.app/v1/capabilities lists installed languages, dimensions and limits; no key needed. - Scripted speech only (the caller supplies the expected text). Not speech-to-text, not unscripted or exam scoring. - Audio: WAV, MP3, M4A, Ogg or WebM, 0.15–120 seconds, up to 10 MB. Audio is deleted after scoring and not used for training; JSON results are kept up to 24 hours for idempotent retries. - Pricing: Starter $10/month for 2,500 credits; Builder $50/month for 15,000 credits; Scale $150/month for 50,000 credits. One credit per started 15 s of audio (eight at the 120 s maximum). Failed and unassessable requests are free. Unused credits roll over. Free trial: every new account gets 50 free credits (no card, no subscription); the homepage has a live demo with no account. - Limits: 60 assessment requests per minute and 2 concurrent assessments per account (per-plan values in GET /v1/terms). Test keys (tp_test_) return fixed example results, never spend credits and are safe in browser code. - Input by language: Mandarin as characters or numbered pinyin; Russian in Cyrillic (stress and vowel reduction come from the transcription); Korean in Hangul with normal spacing (aspiration scored on k/p/t; tense consonants mapped to plain); Japanese in kana and/or kanji, split into bunsetsu-like units, kanji read with a dictionary, pitch accent not scored. Other languages: up to 40 words, numbers written as words. - Every language pack is labelled experimental. Grades are display scores calibrated against native-speaker references, not accuracy percentages; act on the per-unit `decision` (review / uncertain / no_correction). ## Docs - [5-minute quickstart](https://api.toneperfect.app/quickstart/): the first call in cURL, Python and Node.js, and a real response - [API reference](https://api.toneperfect.app/docs): authentication, request fields, response structure, errors, rate limits, credits, languages - [OpenAPI 3.1 description](https://api.toneperfect.app/openapi.json): machine-readable spec for POST /v1/assess, GET /v1/capabilities, POST /v1/lint (free reference-text check), GET /v1/status (synthetic probe) and GET /v1/terms - [SDKs](https://api.toneperfect.app/sdk/): single-file Python and Node.js clients, with a toneperfect-lint command line - [Starter projects](https://api.toneperfect.app/starters/): Next.js, Expo + Cloudflare Worker, Flutter + Firebase Function, each keeping the key on a server - [Evaluation kit](https://api.toneperfect.app/capsule/): synthetic test recordings with expected outcomes and a runner - [How scoring works](https://api.toneperfect.app/methodology/): grades vs decisions, what "experimental" means ## Mandarin Chinese - [Chinese pronunciation API](https://api.toneperfect.app/chinese-pronunciation-api/): per-character initial, final and tone scoring - [Mandarin tone scoring API](https://api.toneperfect.app/mandarin-tone-scoring-api/): expected vs heard tone, probabilities, decisions - [Pinyin pronunciation API](https://api.toneperfect.app/pinyin-pronunciation-api/): score against numbered pinyin - [Mandarin language pack: coverage and known limits](https://api.toneperfect.app/languages/mandarin-chinese/) ## Pricing and comparisons - [Pricing](https://api.toneperfect.app/pricing/) - [Speechace vs SpeechSuper vs Azure pricing](https://api.toneperfect.app/blog/pronunciation-assessment-api-pricing-compared/) - [Speechace alternative](https://api.toneperfect.app/compare/speechace-alternative/) - [SpeechSuper alternative: TonePerfect vs SpeechSuper](https://api.toneperfect.app/compare/speechsuper/) - [Azure pronunciation assessment alternative](https://api.toneperfect.app/compare/azure-pronunciation-assessment/) - [Free pronunciation assessment APIs (2026)](https://api.toneperfect.app/blog/free-pronunciation-assessment-apis/) - [Vendor comparisons](https://api.toneperfect.app/compare/) ## Optional - [All language packs](https://api.toneperfect.app/languages/): [Mandarin Chinese](https://api.toneperfect.app/languages/mandarin-chinese/), [English](https://api.toneperfect.app/languages/english/), [French](https://api.toneperfect.app/languages/french/), [Spanish](https://api.toneperfect.app/languages/spanish/), [German](https://api.toneperfect.app/languages/german/), [Italian](https://api.toneperfect.app/languages/italian/), [Portuguese](https://api.toneperfect.app/languages/portuguese/), [Russian](https://api.toneperfect.app/languages/russian/), [Korean](https://api.toneperfect.app/languages/korean/), [Japanese](https://api.toneperfect.app/languages/japanese/) - [Full documentation in one file](https://api.toneperfect.app/llms-full.txt) - [Blog](https://api.toneperfect.app/blog/) - [Create an account](https://api.toneperfect.app/signup) --- # API reference URL: https://api.toneperfect.app/docs Documentation / Getting started ## Pronunciation assessment API reference Send a recording and its reference text to one endpoint. Get a 0–100 score for the sentence, every word and every sound, plus Mandarin initial, final and tone detail, in one JSON response. - Create an account and generate a secret key in your dashboard. - Send your first recordings with the 50 free credits every account starts with — no card needed. Choose a monthly plan when you need more. - Send a short recording to POST /v1/assess. cURL ``` curl https://api.toneperfect.app/v1/assess \ -H "Authorization: Bearer $TONEPERFECT_API_KEY" \ -H "Idempotency-Key: quickstart-hello-0001" \ -F "audio=@hello.wav" \ -F "text=你好" \ -F "language=zh-CN" ``` New here? The 5-minute quickstart has the same call in cURL, Python and Node.js, how to make a test recording, and the real response. Prefer a client library? The Python and Node.js SDKs wrap this request with the same fields. Store your API key on your server. Browser and mobile applications should call your own backend, which makes the authenticated assessment request (see browser & mobile apps). ### Authentication Pass your secret key in the Authorization: Bearer header (an X-Api-Key header is also accepted). Keys start with tp_live_ and are shown once when created. You can create up to 20 active keys and revoke them in your dashboard without changing your account password. HTTP ``` Authorization: Bearer tp_live_YOUR_SECRET_KEY ``` #### Test keys A tp_test_ key (dashboard → API keys → Test key) runs the whole request path — key, multipart upload, Idempotency-Key, reference rules, response schema — but returns a fixed example result instead of scoring the audio. It never spends credits and never counts as usage, and it has its own limit of 60 requests a minute per test key, so a copied demo key cannot slow down your live key. That makes it safe in browser code, a public demo or a CI job. Idempotency works as with live keys (same key and body: replay; same key, different body: 409). Responses carry "test": true and the header X-TonePerfect-Test: true. Rehearse error handling with TonePerfect-Test-Outcome: unassessable, invalid_assessment, engine_unavailable, subscription_required, insufficient_credits, account_busy or rate_limit. cURL ``` curl https://api.toneperfect.app/v1/assess \ -H "Authorization: Bearer tp_test_YOUR_TEST_KEY" \ -H "Idempotency-Key: ci-check-0001" \ -H "TonePerfect-Test-Outcome: insufficient_credits" \ -F audio=@speech-zh-nihao.ogg -F text=你好 -F language=zh-CN ``` Never include a secret in a URL, public source code, or client application. Revoke an exposed key and generate a replacement. GET /v1/capabilities needs no key. ### Assess a recording POST https://api.toneperfect.app/v1/assess accepts multipart/form-data. Let your HTTP client set the multipart content type and boundary. Two headers are required: Authorization and Idempotency-Key. | | Field | Type | Meaning | audio | File · required | The recording: WAV, MP3, M4A, Ogg or WebM. 0.15 to 120 seconds, at most 10 MB. Decoded to 16 kHz mono on the server. | text | String | The expected words, up to 500 characters. Required unless Mandarin pinyin is supplied. Mandarin: up to 100 characters; other languages: up to 40 words in the language’s own script (Latin, Cyrillic for Russian, Hangul for Korean, kana and/or kanji for Japanese), numbers written as words. See input rules by language. | language | String | A locale: zh-CN (default), en-US, fr-FR, es-419, de-DE, it-IT, pt-BR, ru-RU, ko-KR, or ja-JP. Check live capabilities. | task | String · optional | general (default), connected_speech, or isolated_word. Declares the practice context. Mandarin component correction decisions from the human-judgment model apply with connected_speech. | pinyin | String · optional | Mandarin only. Numbered tones, space-separated, one syllable per character, e.g. ni3 hao3 (v for ü; 5 or 0 for the neutral tone). JavaScript · server-side (Node 18+) ``` // Node 18+ (global fetch, FormData and Blob). Save as assess.mjs, run: node assess.mjs import { readFile } from 'node:fs/promises'; import { randomUUID } from 'node:crypto'; const form = new FormData(); form.append('audio', new Blob([await readFile('hello.wav')], { type: 'audio/wav' }), 'hello.wav'); form.append('text', '你好'); form.append('language', 'zh-CN'); const response = await fetch('https://api.toneperfect.app/v1/assess', { method: 'POST', headers: { Authorization: `Bearer ${process.env.TONEPERFECT_API_KEY}`, // One key per recording attempt; reuse it only to retry the same attempt. 'Idempotency-Key': `quickstart-${randomUUID()}`, }, body: form, }); const result = await response.json(); if (!response.ok) throw new Error(`${response.status} ${result.error.code}: ${result.error.message}`); console.log(result.scores.overall); for (const word of result.words) { console.log(word.word, word.pinyin, word.score, word.phones.map(p => [p.dimension, p.expected, p.heard, p.decision])); } ``` Python ``` import os import uuid import requests # pip install requests with open("hello.wav", "rb") as audio: response = requests.post( "https://api.toneperfect.app/v1/assess", headers={ "Authorization": f"Bearer {os.environ['TONEPERFECT_API_KEY']}", # One key per recording attempt; reuse it only to retry the same attempt. "Idempotency-Key": f"quickstart-{uuid.uuid4()}", }, files={"audio": ("hello.wav", audio, "audio/wav")}, data={"text": "你好", "language": "zh-CN"}, timeout=90, ) result = response.json() if not response.ok: raise SystemExit(f"{response.status_code} {result['error']['code']}: {result['error']['message']}") print(result["scores"]["overall"]) for word in result["words"]: print(word["word"], word["pinyin"], word["score"], [(p["dimension"], p["expected"], p["heard"], p["decision"]) for p in word["phones"]]) ``` The response includes assessable, scores, words, billing, and the loaded model_version. Scores use a 0–100 display scale. Unsupported dimensions are null; a missing measurement is not a zero score. A pronunciation grade is a display score, not a percentage of native likeness or a guaranteed probability of correctness. Use the explicit decision field for feedback; do not turn every low display grade into a correction. Sound-level detail and language capabilities depend on the loaded model. Mandarin human-judgment evidence is currently experimental; its correction policy applies only when task is connected_speech. ### Check reference text first POST https://api.toneperfect.app/v1/lint runs the engine's reference rules on a lesson's text without audio: script, digits, length, and for Mandarin whether the pinyin parses and matches the characters. It is free and needs no key, so run it when you author content, not when a learner is waiting. A text that passes can still be refused by a newer engine; the assessment's own invalid_assessment error stays authoritative. cURL ``` curl https://api.toneperfect.app/v1/lint \ -H "Content-Type: application/json" \ -d '{"text": "我有3个苹果", "language": "zh-CN"}' ``` Response ``` { "ok": false, "language": "zh-CN", "issues": [ { "code": "non_chinese_characters", "message": "Write numbers and foreign words in their spoken Mandarin form (for example 三 not 3), or provide pinyin.", "field": "text", "severity": "error" } ], "units": 5 } ``` Issues with severity: "warning" never block. The SDKs and the CLI (toneperfect lint "你好" --language zh-CN) call the same endpoint. ### Response structure One assessment connects the overall result to individual words and sounds: recording → word or character → sound. A successful request returns HTTP 200 with the fields below; a recording that cannot be assessed also returns 200, with assessable: false. | | Field | Meaning | id | The request id (req_…), also in the X-Request-Id header. | assessable, status, reason | true / completed when scores were produced. Otherwise false / unassessable (or partial when some words fall outside the phonetic inventory) with a reason such as no_speech or different_content. Unassessable requests are not charged. | scores | 0–100 grades: overall, pronunciation, and the pack’s dimensions: initial, final, tone (Mandarin); integrity (completeness), fluency, rhythm (other packs, where supported). Unsupported dimensions are null. | words[] | One entry per word (per character in Mandarin): word, score, start_ms, end_ms; pinyin (Mandarin) or expected_ipa, decision and omission_suspected (other languages). | words[].phones[] | Sound-level rows: expected, heard, score. Mandarin rows have a dimension (initial, final, tone), a decision and flagged; other languages have per-phoneme timing, an alignment operation and diagnosis_status: "experimental". | syllables[] | Mandarin only. expected (pinyin, initial, final, tone), span_ms and components.initial|final|tone, each with expected, heard, agrees_with_prompt, score, confidence, alternatives (ranked labels with probabilities), decision and advice. | heard | What the engine estimates was said: syllables[] (Mandarin initial, final and tone per syllable) or phonetic_transcription (other languages). | model_version, model | The model that actually scored the request, with calibration notes and limitations. | duration_seconds, timing_ms, timing | Decoded audio length, engine time, and (Mandarin) pause and speaking-rate measurements. | warnings[] | Non-fatal notes about the recording. | billing | credits charged for this request, source (free or plan), the remaining plan balance_credits and the remaining free_credits. | raw | Engine diagnostics. Not a stable contract; do not build on it. Every decision is one of review (the evidence for an error is strong), uncertain, or no_correction; unassessable, partial and legacy_correction also occur. Treat any other value as “no decision”. Heard sounds are acoustic estimates. A different heard category can be acceptable in context, such as Mandarin tone sandhi, so use the decision rather than treating every mismatch as an error. A real response for 你好, trimmed to the documented fields (the full body also carries engine diagnostics and probabilities): JSON · 200 OK ``` { "id": "req_example", "language": "zh", "locale": "zh-CN", "model_version": "mandarin-human-v2-85741cdf3967-fbf73d85ee88", "task": "general", "status": "completed", "assessable": true, "reason": null, "scores": { "overall": 99.6, "pronunciation": 97.8, "initial": 98.2, "final": 97.3, "tone": 98, "fluency": null, "rhythm": null, "integrity": null }, "words": [ { "word": "你", "pinyin": "ni3", "score": 99.3, "start_ms": 0, "end_ms": 155, "phones": [ { "dimension": "initial", "expected": "n", "heard": "n", "score": 96.6, "decision": "no_correction", "flagged": false }, { "dimension": "final", "expected": "i", "heard": "i", "score": 96.1, "decision": "no_correction", "flagged": false }, { "dimension": "tone", "expected": 3, "heard": 2, "score": 99.7, "decision": "no_correction", "flagged": false } ] }, { "word": "好", "pinyin": "hao3", "score": 99.5, "start_ms": 155, "end_ms": 424, "phones": [ { "dimension": "initial", "expected": "h", "heard": "h", "score": 99.8, "decision": "no_correction", "flagged": false }, { "dimension": "final", "expected": "ao", "heard": "ao", "score": 98.5, "decision": "no_correction", "flagged": false }, { "dimension": "tone", "expected": 3, "heard": 3, "score": 96.3, "decision": "no_correction", "flagged": false } ] } ], "syllables": [ { "index": 0, "expected": { "pinyin": "ni3", "initial": "n", "final": "i", "tone": 3 }, "span_ms": [ 0, 155 ], "score": 99.3, "components": { "initial": { "expected": "n", "heard": "n", "agrees_with_prompt": true, "score": 96.6, "confidence": "high", "decision": "no_correction", "advice": null }, "final": { "expected": "i", "heard": "i", "agrees_with_prompt": true, "score": 96.1, "confidence": "high", "decision": "no_correction", "advice": null }, "tone": { "expected": 3, "heard": 2, "agrees_with_prompt": false, "score": 99.7, "confidence": "high", "decision": "no_correction", "advice": null } } }, { "index": 1, "expected": { "pinyin": "hao3", "initial": "h", "final": "ao", "tone": 3 }, "span_ms": [ 155, 424 ], "score": 99.5, "components": { "initial": { "expected": "h", "heard": "h", "agrees_with_prompt": true, "score": 99.8, "confidence": "high", "decision": "no_correction", "advice": null }, "final": { "expected": "ao", "heard": "ao", "agrees_with_prompt": true, "score": 98.5, "confidence": "high", "decision": "no_correction", "advice": null }, "tone": { "expected": 3, "heard": 3, "agrees_with_prompt": true, "score": 96.3, "confidence": "high", "decision": "no_correction", "advice": null } } } ], "heard": { "syllables": [ { "initial": "n", "final": "i", "tone": 2 }, { "initial": "h", "final": "ao", "tone": 3 } ] }, "duration_seconds": 0.54, "billing": { "credits": 1, "balance_credits": 2499 } } ``` Note the first syllable: 你 is a 3rd tone that was heard as a 2nd, which is what a 3rd tone becomes before another 3rd tone, so its decision is no_correction. ### Compact schema (stable) Import the generated Postman collection · Subscribe to source changelog RSS The default response carries everything the engine reports, including engine diagnostics (syllables, probabilities, model) that can change between engine versions. Send TonePerfect-Response: compact to get the documented, versioned schema toneperfect.compact.v1 instead: the scores, and per word the units with what was expected, what was heard, a grade and a decision. Dimensions a pack does not measure are null, never missing. Additive changes keep v1; anything else ships as v2 with v1 served for at least 90 days. HTTP ``` TonePerfect-Response: compact ``` Compact response ``` { "schema": "toneperfect.compact.v1", "id": "req_example", "assessable": true, "reason": null, "locale": "zh-CN", "task": "general", "model_version": "mandarin-human-v2-85741cdf3967-fbf73d85ee88", "scores": { "overall": 99.6, "pronunciation": 97.8, "initial": 98.2, "final": 97.3, "tone": 98, "fluency": null, "rhythm": null, "completeness": null }, "words": [ { "text": "你", "pinyin": "ni3", "score": 99.3, "start_ms": 0, "end_ms": 155, "decision": null, "units": [ { "dimension": "initial", "expected": "n", "heard": "n", "score": 96.6, "decision": "no_correction" }, { "dimension": "final", "expected": "i", "heard": "i", "score": 96.1, "decision": "no_correction" }, { "dimension": "tone", "expected": "3", "heard": "2", "score": 99.7, "decision": "no_correction" } ] }, { "text": "好", "pinyin": "hao3", "score": 99.5, "start_ms": 155, "end_ms": 424, "decision": null, "units": [ { "dimension": "initial", "expected": "h", "heard": "h", "score": 99.8, "decision": "no_correction" }, { "dimension": "final", "expected": "ao", "heard": "ao", "score": 98.5, "decision": "no_correction" }, { "dimension": "tone", "expected": "3", "heard": "3", "score": 96.3, "decision": "no_correction" } ] } ], "warnings": [], "billing": { "credits": 1, "balance_credits": 2499 } } ``` Engine internals. The default response's raw field is the engine's internal output. It is included up to 64 KB; larger output is replaced by {"omitted": true, "bytes": N}. Send TonePerfect-Debug: raw to receive it in full when debugging. Do not build on it: it changes with engine versions. raw is never stored, so a replay with the same Idempotency-Key does not contain it; results larger than 256 KB are kept for replay in the compact schema. The format header is not part of the idempotent identity, so a retry may ask for either format. Your own pass marks belong in your code: see the rubric adapter example, which maps the compact schema to your thresholds without treating a grade as a certification. ### Mandarin tones, initials and finals Every Mandarin syllable is scored as three components (initial, final and tone), each with an expected value, a heard estimate, a 0–100 grade and a decision. Third-tone sandhi (3-3 → 2-3) and the 不 change before a 4th tone are taken into account; the 一 change is not applied. Send characters in text, or numbered pinyin in pinyin, or both (one syllable per character). Fluency and rhythm are not scored for Mandarin. See the tone scoring guide for a syllable with a wrong tone, the pinyin guide for pinyin input, and the Mandarin pack page for known limits. ### Languages & capabilities GET https://api.toneperfect.app/v1/capabilities needs no key. It returns model_version, a languages[] array (locale, name, available, status, dimensions, tasks, limits and notes per pack) and the request limits (max_audio_seconds, max_upload_bytes, concurrent_requests_per_account). Inspect it before choosing a language; each assessment identifies the model actually used. cURL ``` curl https://api.toneperfect.app/v1/capabilities ``` | | Language | Locale | Scored dimensions | Mandarin Chinese | zh-CN | pronunciation · initials · finals · tones | English | en-US | pronunciation · completeness · fluency · rhythm | French | fr-FR | pronunciation · completeness · fluency · rhythm | Spanish | es-419 | pronunciation · completeness | German | de-DE | pronunciation · completeness | Italian | it-IT | pronunciation · completeness | Portuguese | pt-BR | pronunciation · completeness | Russian | ru-RU | pronunciation · completeness | Korean | ko-KR | pronunciation · completeness | Japanese | ja-JP | pronunciation · completeness Every pack is currently labelled experimental. Live status, limits and per-pack notes come from GET /v1/capabilities. #### Input rules by language | | Locale | Reference text | zh-CN | Chinese characters (simplified or traditional), up to 100, or numbered pinyin in pinyin; numbers and foreign words written in Chinese. | en-US, fr-FR, es-419, de-DE, it-IT, pt-BR | Up to 40 words in the Latin script, numbers written as words. | ru-RU | Cyrillic, up to 40 words, numbers written as words. Stress and vowel reduction come from the transcription; no stress marks needed. | ko-KR | Hangul with normal spacing; each space-separated word is scored. Aspiration is scored on k, p and t; aspirated s and tɕ (ㅊ) and the tense consonants are mapped to their plain counterparts. | ja-JP | Kana and/or kanji, no spaces needed. Text is split into bunsetsu-like units (日本語を / 勉強しています) and kanji are read with a dictionary, which can miss context-dependent readings such as 今日は (write those in kana). Pitch accent is not scored. Correction decisions are made per word in every language except Mandarin, where they are made per initial, final and tone. A recording of a different sentence or word scores near zero in every language. More detail, known limits and sample responses are on each language page; vendor comparisons are on /compare/. ### Credits & billing Choose a $10, $50, or $150 monthly subscription in your dashboard through Stripe. Plans include 2,500, 15,000, or 50,000 credits each month, credited after successful payment. Unused credits roll over. Higher plans include 10% or 20% off extra credit packs. Every new account also gets 50 free credits once; they work without a subscription and are spent before plan credits. When fewer free credits are left than a recording needs, it uses the ones that are left instead of being refused (billing.credits_waived says how many were waived). A successful, assessable request uses one credit for each started 15 seconds of audio: | | Decoded audio duration | Charge | Up to 15 seconds | 1 credit | More than 15, up to 120 seconds | 2 credits | Failed or unassessable request | 0 credits Once the free credits are used, an active paid subscription is required to assess and buy extra packs. Plan changes take effect at the next renewal. Canceling stops renewal and keeps access through the paid period; your remaining credits stay in the account and can be used when you resubscribe. The supported terms, including what happens to credits after a plan ends, are published from the billing code at /terms/ and as JSON at GET /v1/terms. The final charge and remaining balance are returned in billing. Payment confirmation can arrive after the Stripe success redirect; your ledger is updated when payment is verified. See pricing. ### Limits & rate limits | | Limit | Value | When exceeded | Assessment requests per account | 60 per minute | 429 rate_limit | Assessments in progress per account | 2 at a time on every plan today; the per-plan values are in GET /v1/terms → limits.concurrent_requests_by_plan | 429 account_busy with Retry-After | Engine time per assessment | 90 seconds, every plan | 503 engine_unavailable, not charged | Request through api.toneperfect.app | 26 seconds (website proxy) | 504 from the proxy; retry with the same key | Reference checks (/v1/lint) | 120 per minute per IP address | 429 rate_limit | Audio duration | 0.15 to 120 seconds | 422 invalid_audio | Upload size | 10 MB | 413 upload_too_large | Upload time | 30 seconds | 408 upload_timeout | Reference text | 2,000 characters (Mandarin: 600 characters; other languages: 320 words) | 422 invalid_reference or invalid_assessment | Pinyin | 1,000 characters, 100 syllables | 422 invalid_reference or invalid_assessment | Active API keys | 20 per account | Revoke one to create another When the service itself is at capacity it answers 503 gateway_busy (with a Retry-After header) or decoder_busy; nothing is charged and the same key should be retried. Most short clips return in a second or two (median 1.2 s, 90th percentile 2.2 s on customer calls since September); the first request after a quiet period can take several seconds. #### Capacity, honestly Today one server scores one recording at a time and holds up to three more in a queue shared by all customers. Beyond that, or after 20 seconds in the queue, a request gets a finalized 503 engine_unavailable (not charged; retry with a new key after a short delay). That is plenty for an app whose learners record at different moments, and not enough for a class of 30 pressing "record" in the same second: queue on your side or spread those submissions over a few seconds. Talk to us before a launch or a classroom roll-out and we will size it with you. Requests through api.toneperfect.app pass a proxy with a 26-second timeout, so use a client timeout above that and keep the same Idempotency-Key if the outcome is unknown. Each response carries X-TonePerfect-Credits-Charged, X-TonePerfect-Plan-Credits, X-TonePerfect-Free-Credits and X-RateLimit-Limit headers. Refused requests (402, 429 and the rest) are listed in your dashboard so you can see what your integration ran into. ### Safe retries Send a unique Idempotency-Key for each recording attempt. If a network error prevents you from receiving a result, retry with the same key and identical audio, reference text, language, pinyin, and practice type. A completed request is returned without charging again (with an Idempotent-Replayed: true header), including a completed failure. Use a new key for a different attempt or to try again after a definite failed response. Reusing a key with a different request returns a conflict. Keep the same key when the result is unknown or still pending, including an HTML, empty, or unrecognized 502/504 proxy response. An HTTP status alone does not prove that the assessment stopped. A finalized backend failure includes a req_… result ID, billing.credits: 0, and a completed error code such as engine_unavailable. Do not generate a new key inside an automatic retry loop. Stored results expire after 24 hours. An expired key returns a conflict and never causes another charge. ### Errors Errors are JSON: {"error": {"code": "…", "message": "…"}, "request_id": "req_…"}; request_id is present where available and every response carries an X-Request-Id header. A finalized failure (marked below) is stored under its key and carries id and billing instead of request_id. Branch on error.code, not on the message. | | Status | Code | Meaning | What to do | 400 | invalid_multipart | The body is not readable multipart/form-data. | Send multipart fields audio, text and language; let your HTTP client set the boundary. | 401 | invalid_api_key | The key is missing, unknown or revoked. | Send Authorization: Bearer YOUR_KEY (or X-Api-Key). Create a key in the dashboard. | 402 | subscription_required | Free credits are used up and there is no active paid monthly plan. | Start or renew a plan in the dashboard. Existing credits are kept. | 402 | insufficient_credits | The plan balance does not cover this recording (1 credit per started 15 s). Free credits never cause this: a clip longer than the free credits left uses what is left. | Buy an extra credit pack or wait for the monthly renewal. | 402 | payment_balance_due | A refunded or disputed purchase left a balance to settle. | The next purchase settles it first. | 408 | upload_timeout | The upload took longer than 30 seconds. Nothing was charged. | Retry with the same key. | 409 | idempotency_conflict | This Idempotency-Key was used with different audio or fields. | Use a new key for a new attempt. | 409 | request_pending | The assessment for this key is still running. | Retry the same key shortly. | 409 | idempotency_expired | The stored result for this key is older than 24 hours. | Use a new key. An expired key never charges again. | 413 | upload_too_large | The upload is over 10 MB. | Send a shorter or compressed recording. | 422 | invalid_idempotency_key | The Idempotency-Key header is missing or malformed. | Send 8–128 letters, digits, dots, dashes, underscores or colons. | 422 | missing_audio | No file in the audio field. | Attach the recording as a file part named audio. | 422 | invalid_reference | No text or pinyin, or a field is too long. | Send text (up to 500 characters) or Mandarin pinyin (up to 1,000 characters). | 422 | invalid_task | task is not one of the accepted values. | Use general, connected_speech or isolated_word. | 422 | invalid_test_outcome | TonePerfect-Test-Outcome names an outcome a test key cannot rehearse. | Use success, unassessable, invalid_assessment, engine_unavailable, subscription_required, insufficient_credits, account_busy or rate_limit, or drop the header. | 422 | invalid_input | text, pinyin or language is not a string. | Send them as plain form fields. | 422 | unsupported_language | The locale is not available. | Choose an available locale from GET /v1/capabilities. | 422 | invalid_audio | The file could not be decoded, or is shorter than 0.15 s or longer than 120 s. | Send WAV, MP3, M4A, Ogg or WebM between 0.15 and 120 seconds. | 422 | invalid_assessment · finalized | The engine rejected this reference and recording, for example digits or Latin letters in a Mandarin reference, pinyin that does not match the characters, or text in the wrong script for the locale. | Check the reference text against the language. Not charged; a new attempt needs a new key. | 429 | rate_limit | More than 60 assessment requests in a minute for this account. | Slow down and retry the same key after a short delay. | 429 | account_busy | The account already has its concurrent-assessment limit running (2 by default; see limits in GET /v1/capabilities). | Wait for one to finish, then retry the same key. | 429 | free_capacity | Free credits have reached today’s shared daily limit. Nothing was charged. | Retry tomorrow with a new Idempotency-Key, or start a plan to use plan credits now. | 503 | gateway_busy | The upload gateway is at capacity. Nothing was charged. | Retry the same key after the Retry-After delay. | 503 | decoder_busy | The audio decoder is at capacity. | Retry the same key shortly. | 503 | audio_decoder_unavailable | Audio decoding is temporarily unavailable. | Retry the same key shortly. | 503 | engine_unavailable · finalized | The engine could not finish the assessment, or its queue was full (or, from GET /v1/capabilities, is unreachable). | If the body has an id and billing, it is finalized and not charged: retry with a new key after a short delay. Otherwise retry the same key. | 503 | request_interrupted · finalized | The request was interrupted before the engine finished. Not charged. | Retry with a new key. If the audio cannot be meaningfully assessed, the result is instead a successful response with assessable: false and a reason. Do not show it to your learner as a pronunciation score of zero. | | Unassessable reason | Meaning | no_speech / no_audio | Silence or no usable speech. | too_short | Less speech than the engine needs. | different_content | The recording does not match the reference text. | language_model_not_installed | The language pack is not installed on this server. | unsupported_phonetic_symbols | Every word uses sounds outside the pack’s inventory. | no_calibrated_units | No unit could be graded against the native references. ### Browser & mobile apps Every /v1 route answers CORS preflight requests (Access-Control-Allow-Origin: *, no cookies), so a browser can call the API directly, for example from a prototype or an internal tool. Anyone who opens that page can read a key you put in it and spend your credits, so for anything public keep the key on a server and forward the recording: a 10-line proxy is on the embed page. Mobile apps should do the same; key names like prodapp in a shipped binary are extractable. A test key is the exception: it cannot spend anything, so publish it freely in demos while you build. There are no live “publishable” keys: an origin restriction is a header any script can forge, so it would not protect your credits. Three small proxies (Next.js route, Cloudflare Worker for Expo, Firebase Function for Flutter) are in the starter projects. ### MCP for AI agents https://api.toneperfect.app/v1/mcp is a Model Context Protocol server (Streamable HTTP, JSON responses). Tools: list_languages, check_reference_text (free), explain_tone_error (free) and assess_pronunciation, which takes base64 audio and returns the compact schema, billed exactly like /v1/assess. Put your key in the client's Authorization: Bearer header. For desktop agents that work with files, npx toneperfect-mcp runs a local bridge that also accepts audio_path. MCP client configuration ``` { "mcpServers": { "toneperfect": { "url": "https://api.toneperfect.app/v1/mcp", "headers": { "Authorization": "Bearer tp_live_YOUR_SECRET_KEY" } } } } ``` Requests through api.toneperfect.app pass a proxy with a 26-second limit; long recordings from agents are safer through the SDKs. ### OpenAPI & llms.txt An OpenAPI 3.1 description of the public endpoints is at /openapi.json; import it into Postman, Insomnia or a client generator. A plain-text summary for AI assistants is at /llms.txt, and the full documentation as one text file at /llms-full.txt. ### Guides & comparisons - 5-minute quickstart and Python and Node.js SDKs - How scoring works: grades, decisions and what “experimental” means; how pronunciation scoring works in general - Mandarin: Chinese pronunciation API, tone scoring, pinyin input - Language packs: Mandarin Chinese, English, French, Spanish, German, Italian, Portuguese, Russian, Korean, Japanese - Comparisons: SpeechSuper alternative, Speechace alternative, Azure pronunciation assessment alternative, free pronunciation assessment APIs, pricing --- # Score your first recording in five minutes URL: https://api.toneperfect.app/quickstart/ One multipart POST to /v1/assess with a recording and the text it should contain. Below: how to get a key, how to make a test recording, the same call in cURL, Python and Node.js, and the real response it returns. ### 1. Get an API key Create an account, open API keys in the dashboard and create a key. It starts with tp_live_ and is shown once. Your account starts with 50 free credits, so you can run this whole quickstart without a card; after that, the Starter plan is $10 for 2,500 credits. One credit covers a recording up to 15 seconds. Failed and unassessable requests are free. Then export the key in your shell: Shell ``` export TONEPERFECT_API_KEY="tp_live_YOUR_SECRET_KEY" ``` Keep the key on a server. Browser and mobile apps should call your own backend, which calls the API. ### 2. Make a test recording No microphone handy? Download a hosted sample: a synthetic voice saying 你好, made with the open-source eSpeak NG synthesizer (more samples, with expected outcomes, in the evaluation kit). Then use hello.ogg in the commands below. Shell ``` curl -o hello.ogg https://api.toneperfect.app/capsule/speech-zh-nihao.ogg ``` Or record yourself saying 你好 (nǐ hǎo) for a second or two. A phone voice memo works: the API accepts WAV, MP3, M4A, Ogg and WebM up to 120 seconds and 10 MB, so you can send the file as it is and change hello.wav in the commands below to its name. On a Mac you can also synthesise one: Shell · macOS ``` say -v Tingting -o hello.aiff "你好" afconvert -f WAVE -d LEI16@16000 hello.aiff hello.wav ``` Or convert any recording with ffmpeg: ffmpeg -i memo.m4a -ac 1 -ar 16000 hello.wav. The server converts to 16 kHz mono itself, so this step is optional. ### 3. Send it Pick one. Each is a complete, copy-paste program. #### cURL cURL ``` curl https://api.toneperfect.app/v1/assess \ -H "Authorization: Bearer $TONEPERFECT_API_KEY" \ -H "Idempotency-Key: quickstart-hello-0001" \ -F "audio=@hello.wav" \ -F "text=你好" \ -F "language=zh-CN" ``` Change the Idempotency-Key for every new recording; repeating a request with the same key and the same audio returns the stored result without charging again. #### Python Python 3 · requests ``` import os import uuid import requests # pip install requests with open("hello.wav", "rb") as audio: response = requests.post( "https://api.toneperfect.app/v1/assess", headers={ "Authorization": f"Bearer {os.environ['TONEPERFECT_API_KEY']}", # One key per recording attempt; reuse it only to retry the same attempt. "Idempotency-Key": f"quickstart-{uuid.uuid4()}", }, files={"audio": ("hello.wav", audio, "audio/wav")}, data={"text": "你好", "language": "zh-CN"}, timeout=90, ) result = response.json() if not response.ok: raise SystemExit(f"{response.status_code} {result['error']['code']}: {result['error']['message']}") print(result["scores"]["overall"]) for word in result["words"]: print(word["word"], word["pinyin"], word["score"], [(p["dimension"], p["expected"], p["heard"], p["decision"]) for p in word["phones"]]) ``` #### Node.js Node.js 18+ · no dependencies ``` // Node 18+ (global fetch, FormData and Blob). Save as assess.mjs, run: node assess.mjs import { readFile } from 'node:fs/promises'; import { randomUUID } from 'node:crypto'; const form = new FormData(); form.append('audio', new Blob([await readFile('hello.wav')], { type: 'audio/wav' }), 'hello.wav'); form.append('text', '你好'); form.append('language', 'zh-CN'); const response = await fetch('https://api.toneperfect.app/v1/assess', { method: 'POST', headers: { Authorization: `Bearer ${process.env.TONEPERFECT_API_KEY}`, // One key per recording attempt; reuse it only to retry the same attempt. 'Idempotency-Key': `quickstart-${randomUUID()}`, }, body: form, }); const result = await response.json(); if (!response.ok) throw new Error(`${response.status} ${result.error.code}: ${result.error.message}`); console.log(result.scores.overall); for (const word of result.words) { console.log(word.word, word.pinyin, word.score, word.phones.map(p => [p.dimension, p.expected, p.heard, p.decision])); } ``` Prefer a client library? The Python and Node.js SDKs are single files that wrap the same call and turn errors into typed exceptions. ### 4. Read the response This is a real response for 你好, trimmed to the documented fields; the full body also carries engine diagnostics. Your numbers will differ. JSON · 200 OK ``` { "id": "req_example", "language": "zh", "locale": "zh-CN", "model_version": "mandarin-human-v2-85741cdf3967-fbf73d85ee88", "task": "general", "status": "completed", "assessable": true, "reason": null, "scores": { "overall": 99.6, "pronunciation": 97.8, "initial": 98.2, "final": 97.3, "tone": 98, "fluency": null, "rhythm": null, "integrity": null }, "words": [ { "word": "你", "pinyin": "ni3", "score": 99.3, "start_ms": 0, "end_ms": 155, "phones": [ { "dimension": "initial", "expected": "n", "heard": "n", "score": 96.6, "decision": "no_correction", "flagged": false }, { "dimension": "final", "expected": "i", "heard": "i", "score": 96.1, "decision": "no_correction", "flagged": false }, { "dimension": "tone", "expected": 3, "heard": 2, "score": 99.7, "decision": "no_correction", "flagged": false } ] }, { "word": "好", "pinyin": "hao3", "score": 99.5, "start_ms": 155, "end_ms": 424, "phones": [ { "dimension": "initial", "expected": "h", "heard": "h", "score": 99.8, "decision": "no_correction", "flagged": false }, { "dimension": "final", "expected": "ao", "heard": "ao", "score": 98.5, "decision": "no_correction", "flagged": false }, { "dimension": "tone", "expected": 3, "heard": 3, "score": 96.3, "decision": "no_correction", "flagged": false } ] } ], "syllables": [ { "index": 0, "expected": { "pinyin": "ni3", "initial": "n", "final": "i", "tone": 3 }, "span_ms": [ 0, 155 ], "score": 99.3, "components": { "initial": { "expected": "n", "heard": "n", "agrees_with_prompt": true, "score": 96.6, "confidence": "high", "decision": "no_correction", "advice": null }, "final": { "expected": "i", "heard": "i", "agrees_with_prompt": true, "score": 96.1, "confidence": "high", "decision": "no_correction", "advice": null }, "tone": { "expected": 3, "heard": 2, "agrees_with_prompt": false, "score": 99.7, "confidence": "high", "decision": "no_correction", "advice": null } } }, { "index": 1, "expected": { "pinyin": "hao3", "initial": "h", "final": "ao", "tone": 3 }, "span_ms": [ 155, 424 ], "score": 99.5, "components": { "initial": { "expected": "h", "heard": "h", "agrees_with_prompt": true, "score": 99.8, "confidence": "high", "decision": "no_correction", "advice": null }, "final": { "expected": "ao", "heard": "ao", "agrees_with_prompt": true, "score": 98.5, "confidence": "high", "decision": "no_correction", "advice": null }, "tone": { "expected": 3, "heard": 3, "agrees_with_prompt": true, "score": 96.3, "confidence": "high", "decision": "no_correction", "advice": null } } } ], "heard": { "syllables": [ { "initial": "n", "final": "i", "tone": 2 }, { "initial": "h", "final": "ao", "tone": 3 } ] }, "duration_seconds": 0.54, "billing": { "credits": 1, "balance_credits": 2499 } } ``` - scores.overall is the sentence grade (0–100). For Mandarin, initial, final and tone are averaged across syllables; fluency and rhythm are null because Mandarin does not score them. null means “not measured”, never zero. - words[] has one entry per character with its pinyin, a score and timestamps. Its phones[] rows give the expected and heard initial, final and tone, a score and a decision. - Look at 你: the expected tone is 3 and the heard tone is 2. Before another 3rd tone, a 3rd tone is said as a 2nd (tone sandhi), so the decision is no_correction. Act on decision, not on every expected/heard mismatch. - billing.credits is what this request cost and billing.balance_credits what is left. ### 5. Next steps - Other languages use the same call: change text and language, for example -F "text=I like sunny days" -F "language=en-US". See all 10 language packs. - For Mandarin sentence practice send task=connected_speech; that is the context the component correction decisions are designed for. See Mandarin tone scoring. - Handle errors by error.code and retry safely with the same Idempotency-Key: the error table and retry rules list every case. - Generate a client from the OpenAPI description, or check live languages with curl https://api.toneperfect.app/v1/capabilities (no key needed). ## FAQ Q: Is there a free tier for the quickstart? A: Yes. Every new account gets 50 free credits — up to 50 recordings of 15 seconds through the real API, no card and no subscription. GET /v1/capabilities needs no key. After that the Starter plan is $10/month for 2,500 credits, and failed or unassessable requests are not charged. Q: Which audio formats work? A: WAV, MP3, M4A, Ogg and WebM, from 0.15 to 120 seconds and up to 10 MB. The server converts everything to 16 kHz mono before scoring. Q: Why do I need an Idempotency-Key? A: It makes retries safe: if the network drops and you resend the same audio with the same key, you get the stored result and are not charged twice. Use a new key for each new recording. --- # Simple monthly plans. Pay per assessment. URL: https://api.toneperfect.app/pricing/ Every plan includes all 10 languages and every score dimension. The only difference is how many credits you get and how much extra packs cost. Start free. Every new account gets 50 free credits — up to 50 recordings of 15 seconds through the real API, with your own key. No card, no subscription. Create an account or try the live demo first. ### Plans #### Starter $10/ month 2,500 credits every month - All 10 languages - Unused credits roll over - Add extra credits anytime - API keys, playground & usage Choose Starter #### Builder Most popular $50/ month 15,000 credits every month - All 10 languages - Unused credits roll over - 10% off extra credit packs - API keys, playground & usage Choose Builder #### Scale $150/ month 50,000 credits every month - All 10 languages - Unused credits roll over - 20% off extra credit packs - API keys, playground & usage Choose Scale Credits arrive after each successful payment and roll over while you subscribe. Plan changes apply at renewal. Cancel anytime; access continues to the end of the paid period. Unused plan credits stay in the account but can only be spent while a monthly plan is paid; they unlock again when a plan restarts. ### What a credit buys | | Recording length | Credits | Starter | Builder | Scale | Up to 15 seconds | 1 | $0.0040 | $0.0033 | $0.0030 | 15 to 30 seconds | 2 | $0.0080 | $0.0066 | $0.0060 | Failed or unassessable | 0 | free | free | free Prices per recording are the plan price divided by included credits. A request that fails, times out, or returns assessable: false (silence, wrong sentence, unreadable audio) is not charged. A retried request with the same Idempotency-Key returns the original result and is not charged again. ### Extra credit packs | | Pack | Credits | List price | On Builder (10% off) | On Scale (20% off) | Starter pack | 2,500 | $10 | $9 | $8 | Builder pack | 10,000 | $40 | $36 | $32 | Scale pack | 25,000 | $100 | $90 | $80 Packs are one-time purchases available to any active subscription and never expire while you subscribe. They are the right tool for a launch spike or a school term; if you need packs every month, the next plan up is cheaper. ### Three examples - A prototype with 50 testers doing ten sentences a day: about 15,000 recordings a month → Builder, $50. - A classroom product with 2,000 students doing one five-sentence exercise a day on school days: about 200,000 recordings a month → Scale ($150, 50,000 credits) plus six Scale packs at $80 → $630, or $0.0032 per recording. - A flashcard app where 5% of 10,000 monthly users try the speaking feature five times: 2,500 recordings → Starter, $10. ### Limits on every plan - Up to 120 seconds and 10 MB of audio per request; up to 320 words (600 Mandarin characters) of reference text. - 2 assessments in progress at once per account, on every plan and on free credits; 60 requests per minute. Queue on your side if you burst higher; an over-limit request returns 429 account_busy with Retry-After and is not charged. - The engine gets 90 seconds per assessment on every plan, then the request fails with 503 engine_unavailable, uncharged. Through api.toneperfect.app the website proxy closes a request after 26 seconds; typical assessments take 1–2 seconds (90th percentile 2.2 s). - Scripted speech only: the learner reads the text you send. Need higher concurrency or an invoice? Email us. For how these prices compare with SpeechSuper, Speechace and Azure, see the pricing comparison; for every free tier and trial on the market, see free pronunciation assessment APIs. Ready to build? The quickstart gets you from key to first score in five minutes. ## FAQ Q: Is there a free tier or trial? A: Yes. Every new account gets 50 free credits — up to 50 recordings of 15 seconds through the real API, no card and no subscription. You can also record yourself in the live demo on the homepage without an account. Plans start at $10/month when you need more. Q: Do unused credits expire? A: Not while you subscribe: monthly and pack credits roll over. If the plan ends, unused credits stay in your account but cannot be spent until a plan is paid again; they unlock when you resubscribe. Your 50 free credits work without a plan. Q: Can I change plans? A: Yes. Upgrades and downgrades take effect at the next renewal. Extra packs are available immediately on any active plan. Q: What payment methods are accepted? A: Card payments through Stripe. Invoicing for larger volumes is available on request. Q: Is pricing the same for every language? A: Yes. A Mandarin recording and an English recording of the same length cost the same number of credits, and every plan includes all 10 languages. --- # How TonePerfect scores pronunciation URL: https://api.toneperfect.app/methodology/ What the numbers mean, how they are produced, and what we check before a model reaches the API. Written so you can decide how much to trust each field. ### Inputs A scripted recording (up to 120 seconds) and its reference text. The text is converted to expected units: phonemes for English, French, Spanish, German, Italian, Portuguese, Russian, Korean and Japanese (Japanese text is first split into bunsetsu-like units, and kanji are read with a dictionary); an initial, a final and a tone for every Mandarin syllable. Sandhi contexts where two realisations are correct (3rd tone before a 3rd tone; 不 before a 4th tone) are accepted at scoring time; see the sandhi post for why 一 is not. ### Alignment and per-unit evidence A self-supervised speech encoder produces frame-level representations; forced alignment places each expected unit in time (the start_ms / end_ms you see on words, and the shared syllable interval on Mandarin components). For each unit the engine derives (a) a measure of how well the frames match the expected unit, and (b) an estimate of which unit was actually produced when it differs — the heard field. Heard values are estimates, not transcriptions. ### Display grades Grades are 0–100 and calibrated per unit against native-speaker references (score_scale.calibration = native_reference_per_unit in /v1/capabilities). A unit that native speakers produce with wide variation is graded more leniently than one they produce consistently. Word grades aggregate their units; sentence grades aggregate words plus sentence-level signals (pausing for fluency, timing regularity for rhythm, aligned-word count for completeness). Because calibration is per language, a 70 in Portuguese and a 70 in English are not the same distance from native. Grades are for display and progress; they are not accuracy percentages and not comparable across vendors. A recording of a different sentence or word than the reference scores near zero in every language. ### Correction decisions Separately from the grade, the engine emits a decision: review (evidence of an error is strong enough to correct), uncertain (some evidence, not enough to interrupt), no_correction. For every language except Mandarin the decision is made at word level (a bunsetsu-like unit in Japanese) and phoneme rows are supporting evidence (correction_level: word, phoneme_detail: experimental evidence in capabilities). For Mandarin with task=connected_speech, decisions are made per initial, final and tone by a separate human-judgment model trained on expert-labelled learner recordings. Decisions and grades can disagree by design: a modest grade with no_correction means "not native-like, not worth stopping for". ### What we measure before a release - Error detection on expert-labelled learners. For Mandarin, an evaluation set rated by three expert annotators at initial, final and tone level; we track the area under the ROC curve for detecting rated errors per component and per context (for example 3rd + 3rd tone sequences). - False accusations on native speakers. The share of native units flagged review, measured on held-out speakers from public corpora (AISHELL-1 and AISHELL-3 for Mandarin; native reference sets for the other packs). This is the number that decides whether learners trust the product, and we report it internally for every candidate model. - Robustness. Stability of scores and flags under gain changes, small time shifts, lossy encoding (AAC) and tempo changes. Tempo stretching is the known weak spot for Mandarin tone flags and is tracked as an open gap. - Retake consistency. Agreement between two takes of the same sentence by the same learner, compared with the agreement of a commercial reference engine on the same pairs. We do not publish a single accuracy percentage because there is no such number: detection rates depend on the error type, the context, the learner's first language and the false-accusation budget you accept. Ask us for the evaluation report for a specific pack and we will share the current figures with their denominators. ### What "experimental" means Every pack is currently labelled status: experimental. Concretely: the native calibration set behind the pack is still small (Spanish, German, Italian and Portuguese in particular, and Russian, Korean and Japanese, added in October 2026, are still being calibrated), some sounds are outside the acoustic inventory and are returned as unassessed (for example the English rhotic ɝ, German ʏ, some Portuguese nasal glides), and learner score bands have not been validated against human ratings for every language. The limitations field in /v1/capabilities lists the current gaps per pack, and every response carries the model_version that scored it, so you can detect when a pack changes. ### What the engine does not do - It does not transcribe or score unscripted speech. - It does not score lexical stress, intonation or Japanese pitch accent; fluency and rhythm are available on English and French only. - It does not identify the speaker, estimate age or accent, or store audio; recordings are deleted after scoring. ### Reading a response JSON (Mandarin, abbreviated) ``` { "model_version": "mandarin-human-v2-…", "scores": { "overall": 86, "pronunciation": 90, "initial": 91, "final": 89, "tone": 80, "fluency": null }, "words": [{ "word": "世", "pinyin": "shi4", "score": 68, "start_ms": 1260, "end_ms": 1640, "phones": [ { "dimension": "initial", "expected": "sh", "heard": "s", "score": 78, "decision": "uncertain" }, { "dimension": "final", "expected": "i", "heard": "i", "score": 83, "decision": "no_correction" }, { "dimension": "tone", "expected": 4, "heard": 2, "score": 62, "decision": "review" } ] }] } ``` null means a dimension is not supported by the pack, never "zero". Unassessed symbols are listed per word so your UI can say "not assessed" instead of "wrong". ## FAQ Q: Are scores comparable across languages? A: No. Each pack is calibrated against its own native references, so a score is a distance from native for that language. Compare a learner to their own history, not to a fixed pass mark. Q: How accurate is it? A: There is no single number. We track error detection on expert-labelled learner recordings and false accusations on held-out native speakers per unit and context, and share the current figures with their denominators on request. Packs are labelled experimental until those figures meet our targets across the board. Q: Why do the grade and the decision sometimes disagree? A: Because they answer different questions: the grade is how native-like the unit was; the decision is whether the evidence of an error is strong enough to interrupt the learner. Show the grade; act on the decision. --- # Python and Node.js clients URL: https://api.toneperfect.app/sdk/ Two thin, dependency-free clients that wrap exactly the public endpoints — POST /v1/assess and GET /v1/capabilities — with the real field names, Authorization: Bearer and Idempotency-Key retries. Each is a single file you copy into your project; the documentation stays the reference. Version 0.2 adds lint() (check a reference text before recording, free), status() (the synthetic engine probe), the compact response option, test keys, and a toneperfect-lint command line for whole lesson files. Complete apps that use the API through a small server proxy are on the starter projects page. These clients are not on PyPI or npm yet. Until they are, download the file below — it is the whole client — and keep it in your repository. Package releases will be announced on the blog. ### Python One module, standard library only, Python 3.9+. Files: toneperfect.py · README · example script · MIT licence. Shell ``` curl -O https://api.toneperfect.app/sdk/python/toneperfect.py # drop it next to your code ``` Python ``` from toneperfect import TonePerfect client = TonePerfect(api_key="tp_live_...") # or set TONEPERFECT_API_KEY result = client.assess("hello.wav", text="你好", language="zh-CN") print(result.assessable, result.scores["overall"], result.billing) for word in result.words: print(word["word"], word["score"], [p for p in word["phones"] if p["decision"] == "review"]) ``` assess() returns an Assessment: .data is the untouched JSON body; .assessable, .reason, .scores, .words, .billing, .model_version, .id and .replayed mirror its documented fields. Every non-2xx status raises TonePerfectError with the API's own error.code, plus retryable, needs_new_key, finalized and, for a finalized failure, its id and billing. ### Node.js Plain JavaScript, ESM and CommonJS, no build step, Node 18+ (global fetch). Files: index.cjs · index.mjs (ESM wrapper) · index.d.ts (types) · package.json · README · example script · MIT licence. Shell ``` mkdir -p vendor/toneperfect && cd vendor/toneperfect curl -O https://api.toneperfect.app/sdk/node/index.cjs -O https://api.toneperfect.app/sdk/node/index.mjs \ -O https://api.toneperfect.app/sdk/node/index.d.ts -O https://api.toneperfect.app/sdk/node/package.json ``` JavaScript · server-side ``` import { TonePerfect } from './vendor/toneperfect/index.mjs'; // or require('./vendor/toneperfect/index.cjs') const client = new TonePerfect({ apiKey: 'tp_live_...' }); // or set TONEPERFECT_API_KEY const result = await client.assess('hello.wav', { text: '你好', language: 'zh-CN' }); console.log(result.assessable, result.scores.overall, result.billing); for (const word of result.words) { console.log(word.word, word.score, word.phones.filter(p => p.decision === 'review')); } ``` audio is a file path, Buffer, ArrayBuffer or Blob. Errors reject with TonePerfectError (status, code, detail, requestId, retryable, needsNewKey, finalized, id, billing). ### What both clients do | | Method | Endpoint | Notes | capabilities() | GET /v1/capabilities | No key needed. Languages with status (all experimental today), dimensions, limits, model_version. | assess(audio, text, language, pinyin, task, idempotency key) | POST /v1/assess | Multipart audio, text, language, optional pinyin (numbered tones) and task (general, connected_speech, isolated_word). A key is generated per attempt when you do not pass one. Accounts, API keys, usage and billing are managed in the dashboard and are deliberately not wrapped: those routes are session-based. Keep your key on your server; browser and mobile apps should call your own backend. The decision on every word and sound is one of review, uncertain or no_correction; unassessable (no evidence for that unit), partial (a word only partly covered by the phonetic inventory) and legacy_correction (Mandarin components decided by the legacy policy) also occur. Treat any other value as "no decision". ### Safe retries Every attempt carries a unique Idempotency-Key (generated when you do not pass one). If a network error prevents you from receiving a result, retry with the same key and identical audio, reference text, language, pinyin and practice type: a completed request is returned without charging again, including a completed failure. Use a new key for a different attempt or to try again after a definite failed response. Reusing a key with a different request returns a conflict. Keep the same key when the result is unknown or still pending, including an HTML, empty or unrecognized 502/504 proxy response — an HTTP status alone does not prove that the assessment stopped. A finalized backend failure includes a req_… result id, billing.credits: 0 and a completed error code such as engine_unavailable. Do not generate a new key inside an automatic retry loop. Stored results expire after 24 hours; an expired key returns a conflict and never causes another charge. The clients decide this from the response body, not the status: retryable means nothing was finalized and the same key should be retried later (rate_limit, account_busy, gateway_busy, decoder_busy, audio_decoder_unavailable, upload_timeout, request_pending, transport timeouts and non-JSON 502/504); needs_new_key / needsNewKey means the failure was stored under the key (finalized: id and billing present) or the key has expired, and a new attempt needs a new key. ### What the numbers mean Grades are 0–100 display scores calibrated per unit against native-speaker references; they are not accuracy percentages and not comparable across vendors. Correction decisions are separate from grades: show the grade, act on the decision. Every language pack is currently labelled experimental. The full account is on the methodology page. ### Other languages The API is one multipart POST; any HTTP client works. The documentation has a cURL example, and the two clients above are short enough to port in an afternoon. Want a client for another language listed here? Tell us. ## FAQ Q: Do the SDKs add any behaviour the API does not have? A: No. They build the multipart request, set the two headers, generate an idempotency key when you do not pass one, parse the JSON and turn error responses into a typed exception that says whether to retry with the same key. Every field you read comes straight from the API response. Q: Why is there no pip or npm package yet? A: The clients are single files, so copying them into your repository is the whole installation. Registry packages will follow; the file you download today is what will be published. Q: Can I use them in a browser or a mobile app? A: They run on a server (Python 3.9+ or Node 18+). API keys are secrets: browser and mobile apps should call your own backend, which then calls the API. Q: Is there a free tier for trying the SDK? A: Yes. Every new account gets 50 free credits — up to 50 recordings of 15 seconds through the real API, no card and no subscription. GET /v1/capabilities needs no key at all. --- # Ten languages, one response shape URL: https://api.toneperfect.app/languages/ Every pack returns sentence, word and sound scores in the same JSON. Some add fluency and rhythm; Mandarin adds initials, finals and tones. Pick a language for its coverage, limits and a sample response. 中文 ### Mandarin Chinese zh-CN pronunciation · initials · finals · tones Coverage, limits and example → English ### English en-US pronunciation · completeness · fluency · rhythm Coverage, limits and example → Français ### French fr-FR pronunciation · completeness · fluency · rhythm Coverage, limits and example → Español ### Spanish es-419 pronunciation · completeness Coverage, limits and example → Deutsch ### German de-DE pronunciation · completeness Coverage, limits and example → Italiano ### Italian it-IT pronunciation · completeness Coverage, limits and example → Português ### Portuguese pt-BR pronunciation · completeness Coverage, limits and example → Русский ### Russian ru-RU pronunciation · completeness Coverage, limits and example → 한국어 ### Korean ko-KR pronunciation · completeness Coverage, limits and example → 日本語 ### Japanese ja-JP pronunciation · completeness Coverage, limits and example → Live availability comes from GET /v1/capabilities; Russian, Korean and Japanese were added on 5 October 2026. New to the API? Start with the 5-minute quickstart; for Mandarin see the Chinese pronunciation API, tone scoring and pinyin guides. Comparing vendors? SpeechSuper, Speechace and Azure alternatives, and free options. Need a language that is not listed? Tell us. --- # Mandarin Chinese pronunciation assessment API URL: https://api.toneperfect.app/languages/mandarin-chinese/ The only pronunciation API that scores Mandarin the way it is taught: every syllable as an initial, a final and a tone, each with its own verdict. Built by the team behind the TonePerfect Mandarin app. Locale zh-CN Scored dimensions Pronunciation · Initials · Finals · Tones Levels Sentence · word · initial / final / tone Input Audio ≤ 30 s + reference text Mandarin learners do not mispronounce "words" — they mispronounce a tone, an initial or a final. A phoneme-level English-style scorer hides that: it tells you the syllable shì scored 62 and leaves your app to guess why. TonePerfect splits every syllable into its three parts and reports each one separately, so your feedback can say "you said a 2nd tone; this is a 4th" or "you said s; this needs the retroflex sh". ### Tone sandhi is understood When a 3rd tone precedes another 3rd tone, native speakers say the first as a 2nd tone (nǐ hǎo → ní hǎo). TonePerfect expects that change, so a learner who applies sandhi correctly is not told they used the wrong tone. Read the detail in How to handle tone sandhi in pronunciation scoring. ### Decisions per component With task=connected_speech, each initial, final and tone carries a decision: review when the evidence for an error is strong, uncertain when it is not, and no_correction otherwise. Display grades and decisions are separate on purpose: a score can be modest while the decision is "leave it" — for instance a slightly off-centre vowel that no teacher would stop the class for. ### Learner errors this pack is built to show - Dental for retroflex — s where sh was expected, z for zh, c for ch — reported as an initial with expected vs heard values. - Aspiration contrasts — b / p, d / t, g / k — the most common substitution for speakers of languages without the contrast. - The -n / -ng ending pair (in / ing, an / ang). - Tone shape errors: a 4th tone that rises, a 3rd tone with no dip, a 2nd tone that stays flat — each named as expected tone vs heard tone. ### What it does not do The Mandarin pack scores scripted practice only — you supply the characters. It reports pronunciation, initial, final and tone dimensions; fluency and rhythm are not scored for Mandarin today. Known weak spots we are working on: the neutral tone is not reliably distinguished from a 1st tone, the ü / i contrast is the least sensitive final, and a retroflex produced where a dental was expected (sh for s) is caught far less often than the reverse. The pack is labelled experimental in /v1/capabilities, which means we are still growing the native and learner calibration data behind it; the response's model_version tells you exactly which model scored a request. ### Mandarin guides with real responses - Chinese pronunciation API: per-character initial, final and tone scoring. - Mandarin tone scoring API: expected vs heard tone, tone probabilities and decisions, with a wrong-tone example. - Pinyin pronunciation API: scoring against numbered pinyin, with or without characters. ### Example request Reference text: 你好世界. Send the recording, the text and the locale; nothing else is required. cURL ``` curl https://api.toneperfect.app/v1/assess \ -H "Authorization: Bearer $TONEPERFECT_API_KEY" \ -H "Idempotency-Key: lesson-42-attempt-1" \ -F "audio=@take.wav" \ -F "text=你好世界" \ -F "language=zh-CN" \ -F "task=connected_speech" ``` ### What comes back The same structure as every other language: sentence-level scores, a words[] array with timestamps and, under each word, phones[] with expected and heard values. Abbreviated to the one flagged word: JSON (abbreviated) ``` { "language": "zh", "locale": "zh-CN", "task": "connected_speech", "status": "completed", "assessable": true, "model_version": "mandarin-human-v2-…", "scores": { "overall": 86, "pronunciation": 90, "initial": 91, "final": 89, "tone": 80 }, "words": [ { "word": "世", "pinyin": "shi4", "score": 68, "start_ms": 1020, "end_ms": 1400, "phones": [ { "dimension": "initial", "expected": "sh", "heard": "s", "score": 78, "decision": "uncertain" }, { "dimension": "final", "expected": "i", "heard": "i", "score": 83, "decision": "no_correction" }, { "dimension": "tone", "expected": 4, "heard": 2, "score": 62, "decision": "review" } ] } ], "billing": { "credits": 1, "balance_credits": 2499 } } ``` Reference text goes in as text: Chinese characters (simplified or traditional) or numbered pinyin such as ni3 hao3. Send characters for connected speech so the correction policy can apply. See the response reference for every field. ## FAQ Q: Do I send characters or pinyin? A: Either. Chinese characters are the normal input; numbered pinyin (ni3 hao3) is accepted for isolated-word drills or when your content is stored as pinyin. Connected-speech correction decisions currently require matching characters. Q: Does it handle tone sandhi? A: Third-tone sandhi (3-3 → 2-3) and the 不 change before a 4th tone are taken into account, so correctly applied sandhi is not marked as a tone error. The 一 change is deliberately not applied: it depends on the word (ordinals and counting keep tone 1), so the canonical tone stays the target there. Q: Is traditional Chinese supported? A: The pack is zh-CN (Mainland Mandarin). Traditional characters are accepted as input; pronunciation is assessed against standard Mandarin, not Taiwanese Mandarin conventions. Q: How is the overall score built? A: From the syllable components: each initial, final and tone is compared against native-speaker references and the per-unit scores are aggregated into word and sentence grades on a 0–100 scale. See the methodology page for how grades and decisions differ. --- # English pronunciation assessment API URL: https://api.toneperfect.app/languages/english/ Sentence, word and phoneme scores for American English, with fluency and rhythm at sentence level and timestamps on every word — the same JSON shape TonePerfect uses for every other language. Locale en-US Scored dimensions Pronunciation · Completeness · Fluency · Rhythm Levels Sentence · word · phoneme Input Audio ≤ 30 s + reference text The English pack returns an overall grade and four sentence-level dimensions — pronunciation, completeness, fluency and rhythm — then a score for every word and, underneath each word, the expected phoneme, the phoneme we estimate was said, and a score. The word carries the correction decision; phoneme rows are evidence to show the learner where the word went wrong. ### Typical use Reading practice, sentence drills, vocabulary flashcards with a "say it" step, and kids' read-aloud apps. Because every word has start_ms and end_ms, you can highlight a word and replay just that slice of the learner's recording. ### Learner errors this pack is built to show - Vowel quality — ʌ / ɑ (sun / son vs father), ɪ / i (ship / sheep), æ / e (bad / bed). - Final consonant devoicing — bag heard as back — common for German, Russian and Turkish speakers. - θ / ð replaced by s / z or t / d (think, this). - r / l and v / w substitutions. - Dropped final consonants and reduced clusters (tests, asked), which also lower the completeness score. Phoneme rows are evidence, not validated corrections: they show a learner where a word went wrong, while the word-level decision is what your app should act on. ### Coverage notes The pack is calibrated against native American English references. The rhotic vowel ɝ (as in bird) is outside the pack's phonetic coverage today, so words containing it are scored on their other sounds; the response marks any unassessed symbols. Lexical stress and intonation are not scored. The pack is labelled experimental: word decisions are validated, phoneme detail is evidence rather than a validated correction. ### Example request Reference text: I like sunny days. Send the recording, the text and the locale; nothing else is required. cURL ``` curl https://api.toneperfect.app/v1/assess \ -H "Authorization: Bearer $TONEPERFECT_API_KEY" \ -H "Idempotency-Key: lesson-42-attempt-1" \ -F "audio=@take.wav" \ -F "text=I like sunny days" \ -F "language=en-US" ``` ### What comes back The same structure as every other language: sentence-level scores, a words[] array with timestamps and, under each word, phones[] with expected and heard values. Abbreviated to the one flagged word: JSON (abbreviated) ``` { "language": "en", "locale": "en-US", "task": "scripted_speech", "status": "completed", "assessable": true, "model_version": "multilingual-native-v1-en-…", "scores": { "overall": 88, "pronunciation": 86, "completeness": 100, "fluency": 91, "rhythm": 87 }, "words": [ { "word": "sunny", "expected_ipa": "sʌni", "score": 72, "start_ms": 1020, "end_ms": 1400, "decision": "review", "phones": [ { "phone": "s", "expected": "s", "heard": "s", "score": 93, "diagnosis_status": "experimental" }, { "phone": "ʌ", "expected": "ʌ", "heard": "ɑ", "score": 61, "diagnosis_status": "experimental" }, { "phone": "n", "expected": "n", "heard": "n", "score": 89, "diagnosis_status": "experimental" }, { "phone": "i", "expected": "i", "heard": "i", "score": 90, "diagnosis_status": "experimental" } ] } ], "billing": { "credits": 1, "balance_credits": 2499 } } ``` Reference text goes in as text: The English text the learner was asked to read, up to 40 words. See the response reference for every field. ## FAQ Q: Does it score British English? A: The pack is en-US. British recordings are assessed against American references, which will penalise some vowels; a separate en-GB pack is not available yet. Q: Does it detect word stress or intonation? A: No. Fluency and rhythm are scored at sentence level, but lexical stress and intonation are listed as unsupported dimensions in /v1/capabilities. Q: How long can the recording be? A: Up to 120 seconds and 320 words per request. One credit covers each started 15 seconds: two cover 30, eight cover 120. --- # French pronunciation assessment API URL: https://api.toneperfect.app/languages/french/ Word and sound scores for metropolitan French, with fluency and rhythm at sentence level. The nasal vowels and the uvular r that give learners trouble show up as expected-vs-heard rows your app can explain. Locale fr-FR Scored dimensions Pronunciation · Completeness · Fluency · Rhythm Levels Sentence · word · phoneme Input Audio ≤ 30 s + reference text French learners' most common errors are the four nasal vowels (un, on, an, in), the uvular r, and final consonants that should be silent. The French pack returns each word's score with the expected and estimated heard phoneme underneath, so a learner who said un as a plain è sees exactly that. ### Sentence-level dimensions Alongside pronunciation and completeness, the French pack scores fluency (pausing and pace) and rhythm at sentence level — useful for liaison-heavy sentences where word scores alone miss the point. ### Learner errors this pack is built to show - The four nasal vowels produced as oral vowels or as vowel + n. - u / ou (tu / tout) and é / è confusions. - The uvular r produced as an English or Spanish r. - Final consonants pronounced where they should be silent (petit, trop) — appears as an inserted sound. - Word-final stress carried over from English, which lowers the rhythm score. Phoneme rows are evidence, not validated corrections: they show a learner where a word went wrong, while the word-level decision is what your app should act on. ### Coverage notes Calibrated against native fr-FR references. Some consonants and glides in the grapheme-to-phoneme step remain unassessed and are marked as such in the response. Stress and intonation are not scored. The pack is labelled experimental. ### Example request Reference text: Je voudrais un café. Send the recording, the text and the locale; nothing else is required. cURL ``` curl https://api.toneperfect.app/v1/assess \ -H "Authorization: Bearer $TONEPERFECT_API_KEY" \ -H "Idempotency-Key: lesson-42-attempt-1" \ -F "audio=@take.wav" \ -F "text=Je voudrais un café" \ -F "language=fr-FR" ``` ### What comes back The same structure as every other language: sentence-level scores, a words[] array with timestamps and, under each word, phones[] with expected and heard values. Abbreviated to the one flagged word: JSON (abbreviated) ``` { "language": "fr", "locale": "fr-FR", "task": "scripted_speech", "status": "completed", "assessable": true, "model_version": "multilingual-native-v1-fr-…", "scores": { "overall": 84, "pronunciation": 83, "completeness": 100, "fluency": 88, "rhythm": 85 }, "words": [ { "word": "un", "expected_ipa": "œ̃", "score": 64, "start_ms": 1020, "end_ms": 1400, "decision": "review", "phones": [ { "phone": "œ̃", "expected": "œ̃", "heard": "ɛ", "score": 64, "diagnosis_status": "experimental" } ] } ], "billing": { "credits": 1, "balance_credits": 2499 } } ``` Reference text goes in as text: The French sentence the learner was asked to read, up to 40 words. See the response reference for every field. ## FAQ Q: Is Canadian French supported? A: Not as a separate pack. Recordings are assessed against metropolitan French references. Q: Does it score liaison? A: Not as a named feature. Sentence-level fluency and rhythm reflect it indirectly, and missing or added sounds appear in the word and phoneme rows. --- # Spanish pronunciation assessment API URL: https://api.toneperfect.app/languages/spanish/ Sentence, word and sound scores for Latin American Spanish (es-419). The rolled rr, the tap r and the pure vowels are all reported as expected-vs-heard rows. Locale es-419 Scored dimensions Pronunciation · Completeness Levels Sentence · word · phoneme Input Audio ≤ 30 s + reference text Spanish is often described as "easy to pronounce", yet English speakers reliably reduce its vowels, aspirate p t k, and replace the trilled rr with a tap or an English r. Each of those appears as a low-scoring row under the word, with the expected and estimated heard sound side by side. ### Dimensions The Spanish pack returns pronunciation and completeness at sentence level, word scores, and phoneme evidence. Fluency and rhythm are not scored for Spanish today. ### Learner errors this pack is built to show - Trill rr produced as a tap, an English r or a French r. - Aspirated p / t / k at the start of words. - Reduced vowels — casa ending in a schwa instead of a full a. - b / v and d / ð allophones in the wrong position. - ñ produced as n + y, and ll / y variation, which is scored leniently because natives vary. Phoneme rows are evidence, not validated corrections: they show a learner where a word went wrong, while the word-level decision is what your app should act on. ### Coverage notes The pack uses a Latin American reference set (es-419). Its native calibration set is still small, so learner score bands are not yet validated; the pack is labelled experimental and reports the model used with every response. ### Example request Reference text: Buenos días cómo estás. Send the recording, the text and the locale; nothing else is required. cURL ``` curl https://api.toneperfect.app/v1/assess \ -H "Authorization: Bearer $TONEPERFECT_API_KEY" \ -H "Idempotency-Key: lesson-42-attempt-1" \ -F "audio=@take.wav" \ -F "text=Buenos días cómo estás" \ -F "language=es-419" ``` ### What comes back The same structure as every other language: sentence-level scores, a words[] array with timestamps and, under each word, phones[] with expected and heard values. Abbreviated to the one flagged word: JSON (abbreviated) ``` { "language": "es", "locale": "es-419", "task": "scripted_speech", "status": "completed", "assessable": true, "model_version": "multilingual-native-v1-es-…", "scores": { "overall": 85, "pronunciation": 84, "completeness": 100 }, "words": [ { "word": "cómo", "expected_ipa": "komo", "score": 66, "start_ms": 1020, "end_ms": 1400, "decision": "review", "phones": [ { "phone": "k", "expected": "k", "heard": "k", "score": 94, "diagnosis_status": "experimental" }, { "phone": "o", "expected": "o", "heard": "oʊ", "score": 58, "diagnosis_status": "experimental" }, { "phone": "m", "expected": "m", "heard": "m", "score": 92, "diagnosis_status": "experimental" }, { "phone": "o", "expected": "o", "heard": "o", "score": 90, "diagnosis_status": "experimental" } ] } ], "billing": { "credits": 1, "balance_credits": 2499 } } ``` Reference text goes in as text: The Spanish sentence the learner was asked to read, up to 40 words. See the response reference for every field. ## FAQ Q: Is Castilian Spanish (Spain) supported? A: The pack is es-419 (Latin American). Castilian distinción (the θ sound in "cielo") is not modelled as a separate reference; such words are scored against the seseo pronunciation. Q: Does it score the rolled rr? A: Yes. The trill is a distinct expected sound; a tap or an English r in its place shows as a different heard sound with a lower score. --- # German pronunciation assessment API URL: https://api.toneperfect.app/languages/german/ Sentence, word and sound scores for standard German. The soft and hard ch, the rounded front vowels and final devoicing are reported as expected-vs-heard rows. Locale de-DE Scored dimensions Pronunciation · Completeness Levels Sentence · word · phoneme Input Audio ≤ 30 s + reference text German's signature difficulties for learners — ich vs ach sounds, ü and ö, the uvular r, and voiced consonants that devoice at the end of a word — are exactly the sounds a word score alone cannot explain. The German pack returns the expected and estimated heard phoneme for every sound so your feedback can name the substitution ("sh instead of the soft ch"). ### Dimensions Pronunciation and completeness at sentence level, word scores and phoneme evidence. Fluency and rhythm are not scored for German today. ### Learner errors this pack is built to show - ich-sound ç produced as ʃ or k; ach-sound x produced as k. - Rounded front vowels ü / ö produced as u / o or i / e. - Final devoicing missed — Hund ending in a voiced d. - Vowel length — Stadt / Staat, bitten / bieten. - Initial st / sp produced without the ʃ, and z produced as z instead of ts. Phoneme rows are evidence, not validated corrections: they show a learner where a word went wrong, while the word-level decision is what your app should act on. ### Coverage notes The short rounded vowel ʏ (as in Hütte) is outside the acoustic inventory today and is marked unassessed where it occurs. The native calibration set is small; the pack is labelled experimental. ### Example request Reference text: Guten Morgen wie geht es dir. Send the recording, the text and the locale; nothing else is required. cURL ``` curl https://api.toneperfect.app/v1/assess \ -H "Authorization: Bearer $TONEPERFECT_API_KEY" \ -H "Idempotency-Key: lesson-42-attempt-1" \ -F "audio=@take.wav" \ -F "text=Guten Morgen wie geht es dir" \ -F "language=de-DE" ``` ### What comes back The same structure as every other language: sentence-level scores, a words[] array with timestamps and, under each word, phones[] with expected and heard values. Abbreviated to the one flagged word: JSON (abbreviated) ``` { "language": "de", "locale": "de-DE", "task": "scripted_speech", "status": "completed", "assessable": true, "model_version": "multilingual-native-v1-de-…", "scores": { "overall": 86, "pronunciation": 85, "completeness": 100 }, "words": [ { "word": "wie", "expected_ipa": "viː", "score": 64, "start_ms": 1020, "end_ms": 1400, "decision": "review", "phones": [ { "phone": "v", "expected": "v", "heard": "w", "score": 52, "diagnosis_status": "experimental" }, { "phone": "iː", "expected": "iː", "heard": "iː", "score": 91, "diagnosis_status": "experimental" } ] } ], "billing": { "credits": 1, "balance_credits": 2499 } } ``` Reference text goes in as text: The German sentence the learner was asked to read, up to 40 words. See the response reference for every field. ## FAQ Q: Is Austrian or Swiss German supported? A: The pack is de-DE (standard German). Regional variants are assessed against standard references. Q: Does it handle compound words? A: Yes — the reference text is converted to phonemes word by word, so long compounds are scored as one word with many sounds underneath. --- # Italian pronunciation assessment API URL: https://api.toneperfect.app/languages/italian/ Sentence, word and sound scores for Italian. Geminate consonants, the rolled r and the open/closed e and o appear as expected-vs-heard rows. Locale it-IT Scored dimensions Pronunciation · Completeness Levels Sentence · word · phoneme Input Audio ≤ 30 s + reference text Italian rewards learners who get the double consonants right and punishes the ones who do not — pena and penna are different words. The Italian pack models geminates as distinct expected sounds, so a single n where a double was required shows up as a substitution with a lower score. The trilled r and the seven-vowel system are handled the same way. ### Dimensions Pronunciation and completeness at sentence level, word scores and phoneme evidence. Fluency and rhythm are not scored for Italian today. ### Learner errors this pack is built to show - Single consonants where geminates are required — pena / penna, caro / carro. - Trill r produced as an English approximant. - gl and gn produced as gl / gn clusters instead of ʎ / ɲ. - Diphthongised vowels — o as oʊ, e as eɪ — common for English speakers. - c / g before e / i produced hard instead of tʃ / dʒ. Phoneme rows are evidence, not validated corrections: they show a learner where a word went wrong, while the word-level decision is what your app should act on. ### Coverage notes The native calibration set is small and one native control recording scored below 50 in our September 2026 evaluation, so the lower score band needs improvement before you treat scores under 50 as reliable. The pack is labelled experimental; the methodology page explains what that means in practice. ### Example request Reference text: Buongiorno come stai. Send the recording, the text and the locale; nothing else is required. cURL ``` curl https://api.toneperfect.app/v1/assess \ -H "Authorization: Bearer $TONEPERFECT_API_KEY" \ -H "Idempotency-Key: lesson-42-attempt-1" \ -F "audio=@take.wav" \ -F "text=Buongiorno come stai" \ -F "language=it-IT" ``` ### What comes back The same structure as every other language: sentence-level scores, a words[] array with timestamps and, under each word, phones[] with expected and heard values. Abbreviated to the one flagged word: JSON (abbreviated) ``` { "language": "it", "locale": "it-IT", "task": "scripted_speech", "status": "completed", "assessable": true, "model_version": "multilingual-native-v1-it-…", "scores": { "overall": 85, "pronunciation": 84, "completeness": 100 }, "words": [ { "word": "Buongiorno", "expected_ipa": "bwondʒorno", "score": 70, "start_ms": 180, "end_ms": 560, "decision": "review", "phones": [ { "phone": "dʒ", "expected": "dʒ", "heard": "dʒ", "score": 93, "diagnosis_status": "experimental" }, { "phone": "o", "expected": "o", "heard": "o", "score": 90, "diagnosis_status": "experimental" }, { "phone": "r", "expected": "r", "heard": "ɹ", "score": 56, "diagnosis_status": "experimental" }, { "phone": "n", "expected": "n", "heard": "n", "score": 95, "diagnosis_status": "experimental" } ] } ], "billing": { "credits": 1, "balance_credits": 2499 } } ``` Reference text goes in as text: The Italian sentence the learner was asked to read, up to 40 words. See the response reference for every field. ## FAQ Q: Does it score double consonants? A: Yes. Geminates are distinct expected sounds in the reference, so a short consonant in their place lowers the score and appears as a different heard sound. Q: Are open and closed e/o distinguished? A: They are distinct in the reference inventory. Because many speakers vary regionally, expect modest rather than severe penalties for the wrong quality. --- # Portuguese pronunciation assessment API URL: https://api.toneperfect.app/languages/portuguese/ Sentence, word and sound scores for Brazilian Portuguese. Nasal vowels and diphthongs (não, bem), the lh and nh sounds, and final -e raising are reported as expected-vs-heard rows. Locale pt-BR Scored dimensions Pronunciation · Completeness Levels Sentence · word · phoneme Input Audio ≤ 30 s + reference text The first thing a Portuguese learner gets wrong is não: the nasal diphthong collapses into English "now". The pt-BR pack expects the nasal vowel and reports what it estimates was said, so the feedback can say so. The same applies to ã, õe, lh, nh and the palatalised ti / di of Brazilian speech. ### Dimensions Pronunciation and completeness at sentence level, word scores and phoneme evidence. Fluency and rhythm are not scored for Portuguese today. ### Learner errors this pack is built to show - Nasal vowels and diphthongs — não, mãe, bem — produced without nasality. - lh / nh produced as l + y / n + y. - Word-final -e and -o not raised to i and u (tarde, carro). - ti / di produced without Brazilian palatalisation (tʃi / dʒi). - The r family — initial and double rr as h-like, intervocalic r as a tap — swapped. Phoneme rows are evidence, not validated corrections: they show a learner where a word went wrong, while the word-level decision is what your app should act on. ### Coverage notes Nasalised glides are partly unassessed and marked as such in the response. In our September 2026 evaluation native controls scored a median of about 69 on this pack, so absolute scores run lower than in the other languages and learner score bands need improvement; compare learners to each other and to their own history rather than to a fixed pass mark. The pack is labelled experimental. ### Example request Reference text: Obrigado pela ajuda. Send the recording, the text and the locale; nothing else is required. cURL ``` curl https://api.toneperfect.app/v1/assess \ -H "Authorization: Bearer $TONEPERFECT_API_KEY" \ -H "Idempotency-Key: lesson-42-attempt-1" \ -F "audio=@take.wav" \ -F "text=Obrigado pela ajuda" \ -F "language=pt-BR" ``` ### What comes back The same structure as every other language: sentence-level scores, a words[] array with timestamps and, under each word, phones[] with expected and heard values. Abbreviated to the one flagged word: JSON (abbreviated) ``` { "language": "pt", "locale": "pt-BR", "task": "scripted_speech", "status": "completed", "assessable": true, "model_version": "multilingual-native-v1-pt-…", "scores": { "overall": 86, "pronunciation": 86, "completeness": 100 }, "words": [ { "word": "Obrigado", "expected_ipa": "obɾiɡadu", "score": 72, "start_ms": 180, "end_ms": 560, "decision": "review", "phones": [ { "phone": "ɡ", "expected": "ɡ", "heard": "ɡ", "score": 93, "diagnosis_status": "experimental" }, { "phone": "a", "expected": "a", "heard": "a", "score": 95, "diagnosis_status": "experimental" }, { "phone": "d", "expected": "d", "heard": "d", "score": 90, "diagnosis_status": "experimental" }, { "phone": "u", "expected": "u", "heard": "o", "score": 60, "diagnosis_status": "experimental" } ] } ], "billing": { "credits": 1, "balance_credits": 2499 } } ``` Reference text goes in as text: The Portuguese sentence the learner was asked to read, up to 40 words. See the response reference for every field. ## FAQ Q: Is European Portuguese supported? A: No. The pack is pt-BR; European Portuguese recordings would be assessed against Brazilian references. Q: Why are Portuguese scores lower on average? A: The pack's native calibration is still small and its native controls score lower than in other languages. Treat scores as relative within Portuguese, and watch model_version in responses as the pack improves. --- # Russian pronunciation assessment API URL: https://api.toneperfect.app/languages/russian/ Sentence, word and sound scores for Russian read in Cyrillic. Soft (palatalised) vs hard consonants, vowel reduction in unstressed syllables and the ы vowel are reported as expected-vs-heard rows. Locale ru-RU Scored dimensions Pronunciation · Completeness Levels Sentence · word · phoneme Input Audio ≤ 30 s + reference text Russian spelling hides two things learners get wrong all the time: which consonants are soft and which vowels are reduced. Спасибо is not "spa-see-bo": the с before и is palatalised and the unstressed а and о shrink to ɐ and ə. The Russian pack converts the text to the expected phonemes, stress and reduction included, then reports for every sound what it expected and what it estimates was said. ### Dimensions Pronunciation and completeness at sentence level, word scores and phoneme evidence. Fluency and rhythm are not scored for Russian today. Correction decisions are made per word; phoneme rows are evidence. ### Learner errors this pack is built to show - Soft consonants produced hard — сʲ, тʲ, лʲ said as с, т, л (спасибо, мать, соль). - Unreduced vowels — a full o in an unstressed syllable where natives say ɐ or ə (молоко, хорошо). - ы produced as и (мы / ми, быть / бить). - The trilled р produced as an English or French r. - Missing final devoicing — хлеб ending in a voiced b. Phoneme rows are evidence, not validated corrections: they show a learner where a word went wrong, while the word-level decision is what your app should act on. ### Coverage notes The pack is new (October 2026) and labelled experimental. It is built the same way as the Spanish, German, Italian and Portuguese packs, on native read speech, and its calibration is still being tuned, so treat absolute scores as provisional and watch model_version in responses. Stress placement comes from the dictionary transcription of the text; a learner who stresses the wrong syllable will mostly show up through vowel-quality rows, because lexical stress is not scored as its own dimension. A recording of a different sentence scores near zero. ### Example request Reference text: Спасибо за помощь. Send the recording, the text and the locale; nothing else is required. cURL ``` curl https://api.toneperfect.app/v1/assess \ -H "Authorization: Bearer $TONEPERFECT_API_KEY" \ -H "Idempotency-Key: lesson-42-attempt-1" \ -F "audio=@take.wav" \ -F "text=Спасибо за помощь" \ -F "language=ru-RU" ``` ### What comes back The same structure as every other language: sentence-level scores, a words[] array with timestamps and, under each word, phones[] with expected and heard values. Abbreviated to the one flagged word: JSON (abbreviated) ``` { "language": "ru", "locale": "ru-RU", "task": "scripted_speech", "status": "completed", "assessable": true, "model_version": "multilingual-native-v1-ru-…", "scores": { "overall": 84, "pronunciation": 83, "completeness": 100 }, "words": [ { "word": "Спасибо", "expected_ipa": "spɐsʲibə", "score": 69, "start_ms": 180, "end_ms": 560, "decision": "review", "phones": [ { "phone": "s", "expected": "s", "heard": "s", "score": 94, "diagnosis_status": "experimental" }, { "phone": "p", "expected": "p", "heard": "p", "score": 92, "diagnosis_status": "experimental" }, { "phone": "sʲ", "expected": "sʲ", "heard": "s", "score": 57, "diagnosis_status": "experimental" }, { "phone": "i", "expected": "i", "heard": "i", "score": 90, "diagnosis_status": "experimental" }, { "phone": "b", "expected": "b", "heard": "b", "score": 93, "diagnosis_status": "experimental" } ] } ], "billing": { "credits": 1, "balance_credits": 2499 } } ``` Reference text goes in as text: The Russian sentence the learner was asked to read, in Cyrillic, up to 40 words, with numbers written as words. Stress and vowel reduction come from the transcription of the text, so you do not need to mark stress. See the response reference for every field. ## FAQ Q: Do I need to mark stress in the text? A: No. Send ordinary Cyrillic text; stress and vowel reduction come from the transcription. Write numbers as words (три, not 3). Q: Does it score word stress or intonation? A: Not as separate dimensions. Wrong stress usually changes the vowel qualities the pack expects, so it lowers the affected word, but there is no stress score. --- # Korean pronunciation assessment API URL: https://api.toneperfect.app/languages/korean/ Sentence, word and sound scores for Korean read from Hangul. Aspiration on ㅋ ㅍ ㅌ, vowel quality and final consonants are reported as expected-vs-heard rows under each word. Locale ko-KR Scored dimensions Pronunciation · Completeness Levels Sentence · word · phoneme Input Audio ≤ 30 s + reference text The classic Korean learner error is the three-way stop contrast: plain ㄱ, aspirated ㅋ and tense ㄲ. The Korean pack scores aspiration on k, p and t: a 커피 said without the puff of air on ㅋ shows as an expected kʰ heard as a plain k. The rest of the sentence is scored sound by sound, with word scores and timestamps. ### Dimensions Pronunciation and completeness at sentence level, word scores (one per space-separated word) and phoneme evidence. Fluency and rhythm are not scored for Korean today. Correction decisions are made per word. ### Learner errors this pack is built to show - Aspirated ㅋ ㅍ ㅌ produced as plain stops (커피, 포도, 토요일). - Vowel confusions — ㅓ / ㅗ (어 / 오) and ㅡ / ㅜ. - Final consonants released or dropped, which also lowers completeness. - An English r or l where ㄹ is a tap or a lateral. Phoneme rows are evidence, not validated corrections: they show a learner where a word went wrong, while the word-level decision is what your app should act on. ### Coverage notes The pack is new (October 2026) and labelled experimental, built on native read speech like the Spanish, German, Italian and Portuguese packs, with calibration still being tuned. Two simplifications matter for Korean courses: aspiration is scored on k, p and t only, and the aspirated affricate ㅊ, aspirated s and the tense consonants (ㄲ ㄸ ㅃ ㅆ ㅉ) are mapped to their plain counterparts, so the plain / tense contrast is not scored. Pitch and intonation are not scored. A recording of a different sentence scores near zero. ### Example request Reference text: 커피 한 잔 주세요. Send the recording, the text and the locale; nothing else is required. cURL ``` curl https://api.toneperfect.app/v1/assess \ -H "Authorization: Bearer $TONEPERFECT_API_KEY" \ -H "Idempotency-Key: lesson-42-attempt-1" \ -F "audio=@take.wav" \ -F "text=커피 한 잔 주세요" \ -F "language=ko-KR" ``` ### What comes back The same structure as every other language: sentence-level scores, a words[] array with timestamps and, under each word, phones[] with expected and heard values. Abbreviated to the one flagged word: JSON (abbreviated) ``` { "language": "ko", "locale": "ko-KR", "task": "scripted_speech", "status": "completed", "assessable": true, "model_version": "multilingual-native-v1-ko-…", "scores": { "overall": 85, "pronunciation": 84, "completeness": 100 }, "words": [ { "word": "커피", "expected_ipa": "kʰʌpʰi", "score": 70, "start_ms": 180, "end_ms": 560, "decision": "review", "phones": [ { "phone": "kʰ", "expected": "kʰ", "heard": "k", "score": 56, "diagnosis_status": "experimental" }, { "phone": "ʌ", "expected": "ʌ", "heard": "ʌ", "score": 90, "diagnosis_status": "experimental" }, { "phone": "pʰ", "expected": "pʰ", "heard": "pʰ", "score": 87, "diagnosis_status": "experimental" }, { "phone": "i", "expected": "i", "heard": "i", "score": 94, "diagnosis_status": "experimental" } ] } ], "billing": { "credits": 1, "balance_credits": 2499 } } ``` Reference text goes in as text: The Korean sentence the learner was asked to read, in Hangul with normal spacing, up to 40 words. See the response reference for every field. ## FAQ Q: Does it tell plain, aspirated and tense consonants apart? A: Partly. Aspiration is scored on k, p and t (ㅋ ㅍ ㅌ vs ㄱ ㅂ ㄷ). Tense consonants and the aspirated ㅊ and s are mapped to their plain counterparts, so an ㄲ said as ㄱ is not flagged. Q: How are words split? A: By the spaces in your Hangul text: each space-separated word (어절) gets its own score, timestamps and phoneme rows. Q: Can I send romanised Korean? A: No. Send Hangul; romanisation is not converted. --- # Japanese pronunciation assessment API URL: https://api.toneperfect.app/languages/japanese/ Phrase and sound scores for Japanese read from kana, kanji or both. Long vowels, double consonants, the moraic ん and the r tap are reported as expected-vs-heard rows. Locale ja-JP Scored dimensions Pronunciation · Completeness Levels Sentence · word · phoneme Input Audio ≤ 30 s + reference text Japanese learners lose points on length, not on exotic sounds: おばさん vs おばあさん, きて vs きって. The Japanese pack expects long vowels and double consonants as distinct sounds, so a shortened ō in ありがとう shows as an expected oː heard as o. ### Words are bunsetsu units Japanese text has no spaces, so the API splits the reference into bunsetsu-like units — a content word plus its particles or endings, such as 日本語を / 勉強しています. Each unit is one entry in words[] with its score, timestamps and phoneme rows, and carries the correction decision. Kanji are read with a dictionary. ### Learner errors this pack is built to show - Long vowels shortened or short vowels lengthened (ありがとう, おばあさん / おばさん). - Double consonants (っ) not held (きって, ちょっと). - ら行 produced as an English r or l instead of a tap. - ふ and つ produced as fu / tsu with English consonants. - Moraic ん dropped or merged into the next sound. Phoneme rows are evidence, not validated corrections: they show a learner where a word went wrong, while the word-level decision is what your app should act on. ### Coverage notes The pack is new (October 2026) and labelled experimental, built on native read speech like the Spanish, German, Italian and Portuguese packs, with calibration still being tuned. Pitch accent is not scored: はし (chopsticks) and はし (bridge) are the same target. Kanji readings come from a dictionary and can miss readings that depend on context — 今日は may be read as こんにちは or きょうは — so send kana for words whose reading is ambiguous. A recording of a different sentence scores near zero. ### Example request Reference text: ありがとうございます. Send the recording, the text and the locale; nothing else is required. cURL ``` curl https://api.toneperfect.app/v1/assess \ -H "Authorization: Bearer $TONEPERFECT_API_KEY" \ -H "Idempotency-Key: lesson-42-attempt-1" \ -F "audio=@take.wav" \ -F "text=ありがとうございます" \ -F "language=ja-JP" ``` ### What comes back The same structure as every other language: sentence-level scores, a words[] array with timestamps and, under each word, phones[] with expected and heard values. Abbreviated to the one flagged word: JSON (abbreviated) ``` { "language": "ja", "locale": "ja-JP", "task": "scripted_speech", "status": "completed", "assessable": true, "model_version": "multilingual-native-v1-ja-…", "scores": { "overall": 86, "pronunciation": 85, "completeness": 100 }, "words": [ { "word": "ありがとう", "expected_ipa": "aɾiɡatoː", "score": 71, "start_ms": 180, "end_ms": 560, "decision": "review", "phones": [ { "phone": "ɾ", "expected": "ɾ", "heard": "ɾ", "score": 91, "diagnosis_status": "experimental" }, { "phone": "ɡ", "expected": "ɡ", "heard": "ɡ", "score": 93, "diagnosis_status": "experimental" }, { "phone": "t", "expected": "t", "heard": "t", "score": 94, "diagnosis_status": "experimental" }, { "phone": "oː", "expected": "oː", "heard": "o", "score": 58, "diagnosis_status": "experimental" } ] } ], "billing": { "credits": 1, "balance_credits": 2499 } } ``` Reference text goes in as text: The Japanese sentence the learner was asked to read, in kana and/or kanji, with no spaces needed. The API splits it into bunsetsu-like units (日本語を / 勉強しています) and reads kanji with a dictionary; it can miss context-dependent readings such as 今日は, so write those in kana when the reading matters. See the response reference for every field. ## FAQ Q: Should I send kanji or kana? A: Either, or a mix. Kana is unambiguous; kanji are read with a dictionary, which can pick the wrong reading where it depends on context (今日は). Use kana for those words. Q: Is pitch accent scored? A: No. Pitch accent is not a scored dimension; words that differ only in pitch have the same target. Q: What counts as a word in Japanese? A: A bunsetsu-like unit: a content word with its particles or endings, for example 日本語を and 勉強しています. Each unit gets its own score, timestamps and decision. --- # Chinese pronunciation API URL: https://api.toneperfect.app/chinese-pronunciation-api/ Send a recording and the Chinese text it should contain. Get a grade for the sentence and for every character, with each character split into its initial, final and tone, each with what was expected, what was heard and whether to correct it. ### What it scores Chinese learners rarely mispronounce a whole character. They miss a tone, swap sh for s, or blur -n and -ng. One POST /v1/assess call returns, for every character: - a 0–100 grade and timestamps (words[], one entry per character, with its pinyin); - the initial, final and tone as separate rows, each with expected, heard, a grade and a decision; - sentence-level overall, pronunciation, initial, final and tone grades. | | Field | Per syllable, for the initial, the final and the tone | expected | What the reference requires: an initial such as sh, a final such as ang, or a tone number 1–5 (5 = neutral). | heard | The engine’s estimate of what was said, in the same units. | score | 0–100 display grade against native-speaker references. | decision | review, uncertain or no_correction: whether your app should correct this component. | agrees_with_prompt, confidence, alternatives | In syllables[].components: whether heard equals expected, a confidence label, and the ranked candidates with probabilities. | start_ms, end_ms | The syllable’s position in the recording, for replaying exactly that slice. ### A real response A short recording of 你好, scored by the deployed Mandarin model: JSON · 200 OK (real response, trimmed) ``` { "id": "req_example", "language": "zh", "locale": "zh-CN", "model_version": "mandarin-human-v2-85741cdf3967-fbf73d85ee88", "task": "general", "status": "completed", "assessable": true, "reason": null, "scores": { "overall": 99.6, "pronunciation": 97.8, "initial": 98.2, "final": 97.3, "tone": 98, "fluency": null, "rhythm": null, "integrity": null }, "words": [ { "word": "你", "pinyin": "ni3", "score": 99.3, "start_ms": 0, "end_ms": 155, "phones": [ { "dimension": "initial", "expected": "n", "heard": "n", "score": 96.6, "decision": "no_correction", "flagged": false }, { "dimension": "final", "expected": "i", "heard": "i", "score": 96.1, "decision": "no_correction", "flagged": false }, { "dimension": "tone", "expected": 3, "heard": 2, "score": 99.7, "decision": "no_correction", "flagged": false } ] }, { "word": "好", "pinyin": "hao3", "score": 99.5, "start_ms": 155, "end_ms": 424, "phones": [ { "dimension": "initial", "expected": "h", "heard": "h", "score": 99.8, "decision": "no_correction", "flagged": false }, { "dimension": "final", "expected": "ao", "heard": "ao", "score": 98.5, "decision": "no_correction", "flagged": false }, { "dimension": "tone", "expected": 3, "heard": 3, "score": 96.3, "decision": "no_correction", "flagged": false } ] } ], "syllables": [ { "index": 0, "expected": { "pinyin": "ni3", "initial": "n", "final": "i", "tone": 3 }, "span_ms": [ 0, 155 ], "score": 99.3, "components": { "initial": { "expected": "n", "heard": "n", "agrees_with_prompt": true, "score": 96.6, "confidence": "high", "decision": "no_correction", "advice": null }, "final": { "expected": "i", "heard": "i", "agrees_with_prompt": true, "score": 96.1, "confidence": "high", "decision": "no_correction", "advice": null }, "tone": { "expected": 3, "heard": 2, "agrees_with_prompt": false, "score": 99.7, "confidence": "high", "decision": "no_correction", "advice": null } } }, { "index": 1, "expected": { "pinyin": "hao3", "initial": "h", "final": "ao", "tone": 3 }, "span_ms": [ 155, 424 ], "score": 99.5, "components": { "initial": { "expected": "h", "heard": "h", "agrees_with_prompt": true, "score": 99.8, "confidence": "high", "decision": "no_correction", "advice": null }, "final": { "expected": "ao", "heard": "ao", "agrees_with_prompt": true, "score": 98.5, "confidence": "high", "decision": "no_correction", "advice": null }, "tone": { "expected": 3, "heard": 3, "agrees_with_prompt": true, "score": 96.3, "confidence": "high", "decision": "no_correction", "advice": null } } } ], "heard": { "syllables": [ { "initial": "n", "final": "i", "tone": 2 }, { "initial": "h", "final": "ao", "tone": 3 } ] }, "duration_seconds": 0.54, "billing": { "credits": 1, "balance_credits": 2499 } } ``` 你 was heard with a 2nd tone where a 3rd is written. Before another 3rd tone, that is the correct pronunciation (3-3 tone sandhi), so the decision is no_correction. Third-tone sandhi and the 不 change before a 4th tone are taken into account; the 一 change is not, because it depends on the word. ### Request cURL ``` curl https://api.toneperfect.app/v1/assess \ -H "Authorization: Bearer $TONEPERFECT_API_KEY" \ -H "Idempotency-Key: lesson-42-attempt-1" \ -F "audio=@take.wav" \ -F "text=你好" \ -F "language=zh-CN" \ -F "task=connected_speech" ``` Send simplified or traditional characters in text (up to 100 characters, numbers written in Chinese), or numbered pinyin in pinyin; see the pinyin guide. Use task=connected_speech for sentences: that is the context the component correction decisions are designed for. Audio can be WAV, MP3, M4A, Ogg or WebM, up to 120 seconds. ### What it does not do It scores scripted speech, so you supply the text; it is not speech recognition. Fluency and rhythm are not scored for Mandarin. Pronunciation is assessed against standard Mainland Mandarin. Known weak spots of the current Mandarin model: the neutral tone is not reliably told apart from a 1st tone, ü / i is the least sensitive final, and a retroflex said where a dental was expected (sh for s) is caught far less often than the reverse. The pack is labelled experimental in /v1/capabilities, and every response names the model_version that scored it. More on the Mandarin pack page and the methodology. ## FAQ Q: Is this a Chinese speech recognition API? A: No. It is a pronunciation assessment API: you send the text the learner was asked to say and the recording, and it scores how each character was pronounced. If the recording does not match the text, the result is unassessable (different_content) and not charged. Q: Does it accept traditional characters? A: Yes. Traditional characters are accepted as input and assessed against standard Mandarin pronunciation. Q: How much does it cost? A: Plans start at $10/month for 2,500 credits. One credit covers a recording up to 15 seconds; failed and unassessable requests are free. --- # Mandarin tone scoring API URL: https://api.toneperfect.app/mandarin-tone-scoring-api/ For every syllable, TonePerfect returns the tone the text requires, the tone it heard, the ranked tone candidates with probabilities, a 0–100 grade and a decision on whether to correct it, so your app can say “that was a 3rd tone; this word needs a 4th”. ### Per-tone results Tone is scored separately from the initial and the final of each syllable. In words[].phones[] the row with "dimension": "tone" holds expected and heard as tone numbers (1–4, 5 for neutral), a grade and a decision. The same syllable in syllables[].components.tone adds agrees_with_prompt, a confidence label and alternatives: every tone the engine considered, with its probability. | | Field | Per syllable, for the initial, the final and the tone | expected | What the reference requires: an initial such as sh, a final such as ang, or a tone number 1–5 (5 = neutral). | heard | The engine’s estimate of what was said, in the same units. | score | 0–100 display grade against native-speaker references. | decision | review, uncertain or no_correction: whether your app should correct this component. | agrees_with_prompt, confidence, alternatives | In syllables[].components: whether heard equals expected, a confidence label, and the ranked candidates with probabilities. | start_ms, end_ms | The syllable’s position in the recording, for replaying exactly that slice. ### A wrong tone, scored The reference text was 我想卖书 (“I want to sell books”, mài, 4th tone); the recording said 我想买书 (“buy books”, mǎi, 3rd tone). The real response for that syllable, trimmed to the documented fields: JSON · 200 OK (real response, trimmed) ``` { "id": "req_example", "language": "zh", "locale": "zh-CN", "model_version": "mandarin-human-v2-85741cdf3967-fbf73d85ee88", "task": "connected_speech", "status": "completed", "assessable": true, "reason": null, "scores": { "overall": 99.3, "pronunciation": 97.8, "initial": 97.8, "final": 97.9, "tone": 81.7, "fluency": null, "rhythm": null, "integrity": null }, "words": [ { "word": "卖", "pinyin": "mai4", "score": 96.4, "start_ms": 423, "end_ms": 645, "phones": [ { "dimension": "initial", "expected": "m", "heard": "m", "score": 96.7, "decision": "no_correction", "flagged": false }, { "dimension": "final", "expected": "ai", "heard": "ai", "score": 98.1, "decision": "no_correction", "flagged": false }, { "dimension": "tone", "expected": 4, "heard": 3, "score": 60, "decision": "uncertain", "flagged": false } ] } ], "syllables": [ { "index": 2, "expected": { "pinyin": "mai4", "initial": "m", "final": "ai", "tone": 4 }, "span_ms": [ 423, 645 ], "score": 96.4, "components": { "initial": { "expected": "m", "heard": "m", "agrees_with_prompt": true, "score": 96.7, "confidence": "high", "decision": "no_correction", "advice": null }, "final": { "expected": "ai", "heard": "ai", "agrees_with_prompt": true, "score": 98.1, "confidence": "high", "decision": "no_correction", "advice": null }, "tone": { "expected": 4, "heard": 3, "agrees_with_prompt": false, "score": 60, "confidence": "high", "alternatives": [ { "label": 4, "probability": 0.0004 }, { "label": 3, "probability": 0.9986 }, { "label": 2, "probability": 0.0008 }, { "label": 1, "probability": 0.0002 } ], "decision": "uncertain", "advice": null } } } ], "heard": { "syllables": [ { "initial": "m", "final": "ai", "tone": 3 } ] }, "duration_seconds": 0.989, "billing": { "credits": 1, "balance_credits": 2499 } } ``` The tone was heard as a 3rd (probability 0.9986) where a 4th was expected, and its grade dropped to 60 while the initial and final stayed above 96. The decision is uncertain, not review: the correction policy only says review when its evidence for an error is strong, and the display grade and the decision answer different questions. A practical UI shows the expected and heard tone for review, offers a gentle hint for uncertain, and stays quiet for no_correction. ### Reading tones in code Python ``` for word in result["words"]: tone = next(p for p in word["phones"] if p["dimension"] == "tone") if tone["decision"] == "review": print(f'{word["word"]}: expected tone {tone["expected"]}, heard tone {tone["heard"]}') elif tone["decision"] == "uncertain": print(f'{word["word"]}: listen again to the tone ({tone["score"]})') ``` ### Tone sandhi When a 3rd tone precedes another 3rd tone it is said as a 2nd (nǐ hǎo → ní hǎo), and 不 becomes 2nd tone before a 4th. The engine expects both changes, so a learner who applies them is not told the tone is wrong: in the quickstart response, 你 is heard as tone 2 with decision no_correction. The 一 change is deliberately not applied. Details in tone sandhi in pronunciation scoring. ### Request cURL ``` curl https://api.toneperfect.app/v1/assess \ -H "Authorization: Bearer $TONEPERFECT_API_KEY" \ -H "Idempotency-Key: lesson-42-attempt-1" \ -F "audio=@take.wav" \ -F "text=我想卖书" \ -F "language=zh-CN" \ -F "task=connected_speech" ``` Known weak spots of the current Mandarin model: the neutral tone is not reliably told apart from a 1st tone, ü / i is the least sensitive final, and a retroflex said where a dental was expected (sh for s) is caught far less often than the reverse. The pack is labelled experimental in /v1/capabilities, and every response names the model_version that scored it. More on the Mandarin pack page and the methodology. ## FAQ Q: Can it tell which tone was said, not just that it was wrong? A: Yes. heard is the tone number the engine estimates, and syllables[].components.tone.alternatives ranks every candidate tone with a probability. Q: Does it score the neutral tone? A: The neutral tone is a known weak spot: it is not reliably distinguished from a 1st tone today, so treat neutral-tone results with care. Q: Can I score a single syllable? A: Yes. Send one character (or one numbered pinyin syllable) with task=isolated_word. Recordings must be at least 0.15 seconds. --- # Pinyin pronunciation API URL: https://api.toneperfect.app/pinyin-pronunciation-api/ If your content is stored as pinyin, or you drill syllables that are not words, send numbered pinyin instead of characters. Every syllable comes back with its initial, final and tone scored separately, using the same pinyin units. ### Sending pinyin Put the reading in the pinyin field: numbered tones, one syllable per space, for example ni3 hao3. Use v (or ü) for ü, as in lv4, and 5 or 0 for the neutral tone. Up to 100 syllables. Tone marks (nǐ) are not accepted in pinyin. cURL · pinyin only ``` curl https://api.toneperfect.app/v1/assess \ -H "Authorization: Bearer $TONEPERFECT_API_KEY" \ -H "Idempotency-Key: lesson-42-attempt-1" \ -F "audio=@take.wav" \ -F "pinyin=ni3 hao3" \ -F "language=zh-CN" \ -F "task=isolated_word" ``` You can also send characters and pinyin together, with exactly one syllable per character, to fix the reading of a character with several pronunciations (还 hai2 or huan2). If the counts differ, the request is rejected with invalid_assessment and nothing is charged. ### A real response The same 你好 clip sent as pinyin=ni3 hao3 with no characters. Each words[] entry is one syllable, named by its pinyin: JSON · 200 OK (real response, trimmed) ``` { "id": "req_example", "language": "zh", "locale": "zh-CN", "model_version": "mandarin-hubert-baseline-85741cdf3967", "task": "isolated_word", "status": "completed", "assessable": true, "reason": null, "scores": { "overall": 99.6, "pronunciation": 97.8, "initial": 98.2, "final": 97.3, "tone": 98, "fluency": null, "rhythm": null, "integrity": null }, "words": [ { "word": "ni3", "pinyin": "ni3", "score": 99.3, "start_ms": 0, "end_ms": 155, "phones": [ { "dimension": "initial", "expected": "n", "heard": "n", "score": 96.6, "decision": "no_correction", "flagged": false }, { "dimension": "final", "expected": "i", "heard": "i", "score": 96.1, "decision": "no_correction", "flagged": false }, { "dimension": "tone", "expected": 3, "heard": 2, "score": 99.7, "decision": "no_correction", "flagged": false } ] }, { "word": "hao3", "pinyin": "hao3", "score": 99.5, "start_ms": 155, "end_ms": 424, "phones": [ { "dimension": "initial", "expected": "h", "heard": "h", "score": 99.8, "decision": "no_correction", "flagged": false }, { "dimension": "final", "expected": "ao", "heard": "ao", "score": 98.5, "decision": "no_correction", "flagged": false }, { "dimension": "tone", "expected": 3, "heard": 3, "score": 96.3, "decision": "no_correction", "flagged": false } ] } ], "heard": { "syllables": [ { "initial": "n", "final": "i", "tone": 2 }, { "initial": "h", "final": "ao", "tone": 3 } ] }, "duration_seconds": 0.54, "billing": { "credits": 1, "balance_credits": 2499 } } ``` Note the model_version: pinyin-only requests are scored by the baseline Mandarin model, without the human-judgment layer that characters enable. Grades and expected-vs-heard values are the same kind; for sentence practice with component correction decisions, send the characters too, with task=connected_speech. ### What comes back per syllable | | Field | Per syllable, for the initial, the final and the tone | expected | What the reference requires: an initial such as sh, a final such as ang, or a tone number 1–5 (5 = neutral). | heard | The engine’s estimate of what was said, in the same units. | score | 0–100 display grade against native-speaker references. | decision | review, uncertain or no_correction: whether your app should correct this component. | agrees_with_prompt, confidence, alternatives | In syllables[].components: whether heard equals expected, a confidence label, and the ranked candidates with probabilities. | start_ms, end_ms | The syllable’s position in the recording, for replaying exactly that slice. Initials and finals use pinyin spelling (zh, ü, iang), tones are numbers, and heard.syllables[] lists what the engine estimates was said for every syllable, so you can render the learner’s own pinyin next to the target. ### Common input errors - Tone marks or missing tone numbers in pinyin (nǐ, ni): write ni3. - Pinyin in text: the text field is for characters in Mandarin; use pinyin for pinyin. - Digits or Latin words in Chinese text: write numbers in Chinese or send the full reading as pinyin. Known weak spots of the current Mandarin model: the neutral tone is not reliably told apart from a 1st tone, ü / i is the least sensitive final, and a retroflex said where a dental was expected (sh for s) is caught far less often than the reverse. The pack is labelled experimental in /v1/capabilities, and every response names the model_version that scored it. More on the Mandarin pack page and the methodology. ## FAQ Q: Do I need to send characters? A: No. Numbered pinyin on its own is enough to score initials, finals and tones. Characters add the human-judgment correction layer used for connected speech. Q: Are tone marks supported? A: Not in the pinyin field. Convert nǐ hǎo to ni3 hao3; most pinyin libraries can output numbered tones. Q: How is ü written? A: As v or ü, for example lv4 or lü4. After j, q, x and y, u is read as ü automatically (ju, que, xue, yu).