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.

  1. Create an account and generate a secret key in your dashboard.
  2. Send your first recordings with the 50 free credits every account starts with — no card needed. Choose a monthly plan when you need more.
  3. 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.

FieldTypeMeaning
audioFile · requiredThe recording: WAV, MP3, M4A, Ogg or WebM. 0.15 to 120 seconds, at most 10 MB. Decoded to 16 kHz mono on the server.
textStringThe 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.
languageStringA 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.
taskString · optionalgeneral (default), connected_speech, or isolated_word. Declares the practice context. Mandarin component correction decisions from the human-judgment model apply with connected_speech.
pinyinString · optionalMandarin 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.

FieldMeaning
idThe request id (req_…), also in the X-Request-Id header.
assessable, status, reasontrue / 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.
scores0–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.
heardWhat the engine estimates was said: syllables[] (Mandarin initial, final and tone per syllable) or phonetic_transcription (other languages).
model_version, modelThe model that actually scored the request, with calibration notes and limitations.
duration_seconds, timing_ms, timingDecoded audio length, engine time, and (Mandarin) pause and speaking-rate measurements.
warnings[]Non-fatal notes about the recording.
billingcredits charged for this request, source (free or plan), the remaining plan balance_credits and the remaining free_credits.
rawEngine 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
LanguageLocaleScored dimensions
Mandarin Chinesezh-CNpronunciation · initials · finals · tones
Englishen-USpronunciation · completeness · fluency · rhythm
Frenchfr-FRpronunciation · completeness · fluency · rhythm
Spanishes-419pronunciation · completeness
Germande-DEpronunciation · completeness
Italianit-ITpronunciation · completeness
Portuguesept-BRpronunciation · completeness
Russianru-RUpronunciation · completeness
Koreanko-KRpronunciation · completeness
Japaneseja-JPpronunciation · completeness

Every pack is currently labelled experimental. Live status, limits and per-pack notes come from GET /v1/capabilities.

Input rules by language

LocaleReference text
zh-CNChinese 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-BRUp to 40 words in the Latin script, numbers written as words.
ru-RUCyrillic, up to 40 words, numbers written as words. Stress and vowel reduction come from the transcription; no stress marks needed.
ko-KRHangul 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-JPKana 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 durationCharge
Up to 15 seconds1 credit
More than 15, up to 30 seconds2 credits
Each further started 15 seconds, up to 120 seconds+1 credit (8 at most)
Failed or unassessable request0 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

LimitValueWhen exceeded
Assessment requests per account60 per minute429 rate_limit
Assessments in progress per account2 at a time on every plan today; the per-plan values are in GET /v1/terms → limits.concurrent_requests_by_plan429 account_busy with Retry-After
Engine time per assessment90 seconds, every plan503 engine_unavailable, not charged
Request through api.toneperfect.app26 seconds (website proxy)504 from the proxy; retry with the same key
Reference checks (/v1/lint)120 per minute per IP address429 rate_limit
Audio duration0.15 to 120 seconds422 invalid_audio
Upload size10 MB413 upload_too_large
Upload time30 seconds408 upload_timeout
Reference text2,000 characters (Mandarin: 600 characters; other languages: 320 words)422 invalid_reference or invalid_assessment
Pinyin1,000 characters, 100 syllables422 invalid_reference or invalid_assessment
Active API keys20 per accountRevoke 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.

StatusCodeMeaningWhat to do
400invalid_multipartThe body is not readable multipart/form-data.Send multipart fields audio, text and language; let your HTTP client set the boundary.
401invalid_api_keyThe key is missing, unknown or revoked.Send Authorization: Bearer YOUR_KEY (or X-Api-Key). Create a key in the dashboard.
402subscription_requiredFree credits are used up and there is no active paid monthly plan.Start or renew a plan in the dashboard. Existing credits are kept.
402insufficient_creditsThe 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.
402payment_balance_dueA refunded or disputed purchase left a balance to settle.The next purchase settles it first.
408upload_timeoutThe upload took longer than 30 seconds. Nothing was charged.Retry with the same key.
409idempotency_conflictThis Idempotency-Key was used with different audio or fields.Use a new key for a new attempt.
409request_pendingThe assessment for this key is still running.Retry the same key shortly.
409idempotency_expiredThe stored result for this key is older than 24 hours.Use a new key. An expired key never charges again.
413upload_too_largeThe upload is over 10 MB.Send a shorter or compressed recording.
422invalid_idempotency_keyThe Idempotency-Key header is missing or malformed.Send 8–128 letters, digits, dots, dashes, underscores or colons.
422missing_audioNo file in the audio field.Attach the recording as a file part named audio.
422invalid_referenceNo text or pinyin, or a field is too long.Send text (up to 500 characters) or Mandarin pinyin (up to 1,000 characters).
422invalid_tasktask is not one of the accepted values.Use general, connected_speech or isolated_word.
422invalid_test_outcomeTonePerfect-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.
422invalid_inputtext, pinyin or language is not a string.Send them as plain form fields.
422unsupported_languageThe locale is not available.Choose an available locale from GET /v1/capabilities.
422invalid_audioThe 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.
422invalid_assessment · finalizedThe 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.
429rate_limitMore than 60 assessment requests in a minute for this account.Slow down and retry the same key after a short delay.
429account_busyThe 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.
429free_capacityFree 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.
503gateway_busyThe upload gateway is at capacity. Nothing was charged.Retry the same key after the Retry-After delay.
503decoder_busyThe audio decoder is at capacity.Retry the same key shortly.
503audio_decoder_unavailableAudio decoding is temporarily unavailable.Retry the same key shortly.
503engine_unavailable · finalizedThe 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.
503request_interrupted · finalizedThe 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 reasonMeaning
no_speech / no_audioSilence or no usable speech.
too_shortLess speech than the engine needs.
different_contentThe recording does not match the reference text.
language_model_not_installedThe language pack is not installed on this server.
unsupported_phonetic_symbolsEvery word uses sounds outside the pack’s inventory.
no_calibrated_unitsNo 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