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:
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.
curl -o hello.ogg https://api.toneperfect.app/capsule/speech-zh-nihao.oggOr 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 30 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:
say -v Tingting -o hello.aiff "你好"
afconvert -f WAVE -d LEI16@16000 hello.aiff hello.wavOr 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 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
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 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.
{
"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.overallis the sentence grade (0–100). For Mandarin,initial,finalandtoneare averaged across syllables;fluencyandrhythmarenullbecause Mandarin does not score them.nullmeans “not measured”, never zero.words[]has one entry per character with itspinyin, a score and timestamps. Itsphones[]rows give theexpectedandheardinitial, final and tone, a score and adecision.- 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 ondecision, not on every expected/heard mismatch. billing.creditsis what this request cost andbilling.balance_creditswhat is left.
5. Next steps
- Other languages use the same call: change
textandlanguage, 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.codeand retry safely with the sameIdempotency-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).