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 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.
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.
Authorization: Bearer tp_live_YOUR_SECRET_KEYTest 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 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-CNNever 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). |
// 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]));
}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.
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 https://api.toneperfect.app/v1/lint \
-H "Content-Type: application/json" \
-d '{"text": "我有3个苹果", "language": "zh-CN"}'{
"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):
{
"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.
TonePerfect-Response: compact{
"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 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 30 seconds | 2 credits |
| Each further started 15 seconds, up to 120 seconds | +1 credit (8 at most) |
| 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.
{
"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