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

MethodEndpointNotes
capabilities()GET /v1/capabilitiesNo key needed. Languages with status (all experimental today), dimensions, limits, model_version.
assess(audio, text, language, pinyin, task, idempotency key)POST /v1/assessMultipart 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.